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, entry.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(); });