121 lines
2.6 KiB
JavaScript
121 lines
2.6 KiB
JavaScript
import { readFileSync, realpathSync } from 'node:fs';
|
|
import { parse_socket_state } from './parser.mjs';
|
|
import { spawn } from 'node:child_process';
|
|
|
|
|
|
// Configuration
|
|
const network_interface = 'ens3';
|
|
const update_frequency = 1; // Hz
|
|
const seen_threshold = 5; // times
|
|
const max_age = 20; // seconds
|
|
const max_ban_time = 300; // seconds
|
|
const dry_run = true;
|
|
|
|
// Use IGNORE_IPS in environment for comma separated list of IPs that will never be banned
|
|
const ignore_ips = new Set(process.env.IGNORE_IPS?.split(',').map(e => e.trim()));
|
|
|
|
// Application
|
|
|
|
const peer_map = new Map();
|
|
const ban_map = new Map();
|
|
|
|
const ban_script = realpathSync('./ban.sh');
|
|
const unban_script = realpathSync('./unban.sh');
|
|
|
|
function sudo(argv) {
|
|
if (dry_run) {
|
|
console.log('sudo', ['-n', ...argv]);
|
|
} else {
|
|
spawn('sudo', ['-n', ...argv], { stdio: 'inherit', detached: true });
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function ban(ip) {
|
|
const ts = performance.now();
|
|
if (ignore_ips.has(ip)) {
|
|
return;
|
|
}
|
|
|
|
const existing = ban_map.get(ip);
|
|
|
|
if (existing) {
|
|
existing.ts = ts;
|
|
} else {
|
|
ban_map.set(ip, { ip, ts });
|
|
console.log(new Date().toISOString(), '**BAN**', ip);
|
|
sudo([ban_script, network_interface, ip]);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function check_ban_list() {
|
|
const ts = performance.now();
|
|
for (const entry of ban_map.values()) {
|
|
const age = (ts - entry.ts) / 1000;
|
|
if (age > max_ban_time) {
|
|
ban_map.delete(entry.ip);
|
|
console.log(new Date().toISOString(), 'UNBAN', entry.ip);
|
|
sudo([unban_script, network_interface, ip]);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function run_at_rate(rate, cb) {
|
|
let monotime = performance.now();
|
|
|
|
function run_once() {
|
|
monotime += 1000 / rate;
|
|
cb();
|
|
const remaining = Math.max(0, monotime - performance.now());
|
|
setTimeout(run_once, remaining);
|
|
}
|
|
run_once();
|
|
}
|
|
|
|
|
|
run_at_rate(update_frequency, () => {
|
|
|
|
// Update once
|
|
const socket_state = readFileSync('/proc/net/tcp', 'utf-8');
|
|
for (const entry of parse_socket_state(socket_state)) {
|
|
if ((entry.st & 0x3) === 0x3) {
|
|
const ts = performance.now();
|
|
const existing = peer_map.get(entry.rem_address);
|
|
if (existing) {
|
|
existing.count++;
|
|
existing.ts = ts;
|
|
} else {
|
|
peer_map.set(entry.rem_address, { entry, ts, count: 1 });
|
|
console.log(new Date().toISOString(), 'ADD', entry.rem_address);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Purge old
|
|
const ts = performance.now();
|
|
for (const entry of peer_map.values()) {
|
|
const age = (ts - entry.ts) / 1000;
|
|
if (age > max_age) {
|
|
peer_map.delete(entry.entry.rem_address);
|
|
console.log(new Date().toISOString(), 'DROP', entry.entry.rem_address);
|
|
}
|
|
}
|
|
|
|
// Check limits
|
|
for (const entry of peer_map.values()) {
|
|
if (entry.count > seen_threshold) {
|
|
ban(entry.entry.rem_address);
|
|
}
|
|
|
|
}
|
|
|
|
check_ban_list();
|
|
|
|
|
|
});
|