From 322ee99666c5dbbb4299a09db40aeb8b3e53e5c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20L=C3=B6vqvist?= Date: Fri, 13 Feb 2026 00:30:12 +0100 Subject: [PATCH] Initial commit --- autoban.mjs | 120 ++++++++++++++++++++++++++++++++++++++++ ban.sh | 1 + notes.txt | 9 +++ parser.mjs | 42 ++++++++++++++ sample-proc-net-tcp.txt | 39 +++++++++++++ setup.sh | 3 + unban.sh | 1 + 7 files changed, 215 insertions(+) create mode 100644 autoban.mjs create mode 100644 ban.sh create mode 100644 notes.txt create mode 100644 parser.mjs create mode 100644 sample-proc-net-tcp.txt create mode 100644 setup.sh create mode 100644 unban.sh diff --git a/autoban.mjs b/autoban.mjs new file mode 100644 index 0000000..e8e63d6 --- /dev/null +++ b/autoban.mjs @@ -0,0 +1,120 @@ +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(); + + +}); diff --git a/ban.sh b/ban.sh new file mode 100644 index 0000000..9bdc3d7 --- /dev/null +++ b/ban.sh @@ -0,0 +1 @@ +iptables-nft -C INPUT -i "$1" -s "$2" -j DROP 2>/dev/null || iptables-nft -A INPUT -i "$1" -s "$2" -j DROP \ No newline at end of file diff --git a/notes.txt b/notes.txt new file mode 100644 index 0000000..9e76b73 --- /dev/null +++ b/notes.txt @@ -0,0 +1,9 @@ + +sudo chown root:root /usr/local/bin/ban.sh /usr/local/bin/unban.sh +sudo chmod 700 /usr/local/bin/ban.sh /usr/local/bin/unban.sh # only root can edit +sudo chattr +i /usr/local/bin/ban.sh /usr/local/bin/unban.sh # make files immutable + + +sudoers: + + ubuntu ALL=(root) NOPASSWD: /home/ubuntu/Projekt/ids/ban.sh, /home/ubuntu/Projekt/ids/unban.sh \ No newline at end of file diff --git a/parser.mjs b/parser.mjs new file mode 100644 index 0000000..c283543 --- /dev/null +++ b/parser.mjs @@ -0,0 +1,42 @@ +export function parse_ipv4(value) { + const D = parseInt(value.slice(0, 2), 16); + const C = parseInt(value.slice(2, 4), 16); + const B = parseInt(value.slice(4, 6), 16); + const A = parseInt(value.slice(6, 8), 16); + return `${A}.${B}.${C}.${D}`; +} + +export function parse_socket_state(socket_state) { + const ss_lines = socket_state.split('\n').map(line => line.trim()); + const fields = ss_lines[0].split(/\s+/); + const result = []; + + const field_parsers = { + 'st': (value => parseInt(value, 16)), + 'local_address': parse_ipv4, + 'rem_address': parse_ipv4, + }; + + for (const line of ss_lines.slice(1)) { + const values = line.split(/\s+/); + const v_iter = values[Symbol.iterator](); + const pending = {}; + for (const column of fields.slice(0, -1)) { + pending[column] = v_iter.next().value; + } + pending[fields.at(-1)] = [...v_iter].join(' '); + + for (const [name, parser] of Object.entries(field_parsers)) { + const local_value = pending[name]; + if (local_value) { + pending[name] = parser(local_value); + } + } + + result.push(pending); + + } + return result; +} + + diff --git a/sample-proc-net-tcp.txt b/sample-proc-net-tcp.txt new file mode 100644 index 0000000..9fa231e --- /dev/null +++ b/sample-proc-net-tcp.txt @@ -0,0 +1,39 @@ + sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode + 0: 01234560:0050 01234560:0000 0A 00000000:00000000 00:00000000 00000000 0 0 42854 1 0000000000000000 100 0 0 10 0 + 1: 01234560:0016 01234560:0000 0A 00000000:00000000 00:00000000 00000000 0 0 60668 1 0000000000000000 100 0 0 10 0 + 2: 01234560:01BB 01234560:0000 0A 00000000:00000000 00:00000000 00000000 0 0 42856 26 0000000000000000 100 0 0 10 0 + 3: 01234560:0B7A 01234560:0000 0A 00000000:00000000 00:00000000 00000000 0 0 21963 1 0000000000000000 100 0 0 10 0 + 4: 3123456F:0035 01234560:0000 0A 00000000:00000000 00:00000000 00000000 101 0 15827 1 0000000000000000 100 0 0 10 5 + 5: 0123456F:0CEA 01234560:0000 0A 00000000:00000000 00:00000000 00000000 113 0 23714 1 0000000000000000 100 0 0 10 0 + 6: 0123456F:0BB8 01234560:0000 0A 00000000:00000000 00:00000000 00000000 0 0 60686 1 0000000000000000 100 0 0 10 0 + 7: 0123456F:18EB 01234560:0000 0A 00000000:00000000 00:00000000 00000000 114 0 22650 1 0000000000000000 100 0 0 10 0 + 8: E1234565:01BB F1234561:AAA8 03 00000000:00000000 01:00000B07 00000005 0 0 0 0 0000000000000000 + 9: E1234565:01BB 41234561:1A9B 03 00000000:00000000 01:0000046E 00000005 0 0 0 0 0000000000000000 + 10: E1234565:01BB 51234561:FBA9 03 00000000:00000000 01:000003AE 00000004 0 0 0 0 0000000000000000 + 11: E1234565:01BB 41234561:B82C 03 00000000:00000000 01:00000174 00000003 0 0 0 0 0000000000000000 + 12: E1234565:01BB 91234561:4541 03 00000000:00000000 01:00000074 00000003 0 0 0 0 0000000000000000 + 13: E1234565:01BB 61234561:D859 03 00000000:00000000 01:00000347 00000004 0 0 0 0 0000000000000000 + 14: E1234565:01BB 21234561:1C96 03 00000000:00000000 01:00000274 00000003 0 0 0 0 0000000000000000 + 15: 0123456F:CB3A 0123456F:0BB8 01 00000000:00000000 00:00000000 00000000 33 0 341759 1 0000000000000000 20 4 30 10 -1 + 16: E1234565:01BB 81234561:3A2E 03 00000000:00000000 01:00000B6E 00000005 0 0 0 0 0000000000000000 + 17: E1234565:01BB 21234561:9277 03 00000000:00000000 01:000001D4 00000005 0 0 0 0 0000000000000000 + 18: 0123456F:0BB8 0123456F:CB3A 01 00000000:00000000 02:0000054D 00000000 0 0 340925 2 0000000000000000 20 4 31 10 -1 + 19: E1234565:01BB 11234561:77CF 03 00000000:00000000 01:00000B21 00000005 0 0 0 0 0000000000000000 + 20: E1234565:01BB E1234561:F3AA 03 00000000:00000000 01:000003BA 00000005 0 0 0 0 0000000000000000 + 21: E1234565:01BB 71234561:4165 03 00000000:00000000 01:00000721 00000005 0 0 0 0 0000000000000000 + 22: E1234565:01BB 91234561:5A44 03 00000000:00000000 01:00000114 00000004 0 0 0 0 0000000000000000 + 23: E1234565:01BB D1234561:E389 03 00000000:00000000 01:000003EE 00000005 0 0 0 0 0000000000000000 + 24: E1234565:01BB 91234561:CF3B 03 00000000:00000000 01:00000000 00000005 0 0 0 0 0000000000000000 + 25: E1234565:01BB F1234561:B713 03 00000000:00000000 01:00000241 00000003 0 0 0 0 0000000000000000 + 26: 0123456C:8522 0123456C:0BB8 01 00000000:00000000 02:0000054A 00000000 0 0 340927 2 0000000000000000 20 4 30 10 -1 + 27: E1234565:01BB 31234561:399F 03 00000000:00000000 01:00000000 00000005 0 0 0 0 0000000000000000 + 28: E1234565:01BB B1234561:E266 03 00000000:00000000 01:0000055E 00000004 0 0 0 0 0000000000000000 + 29: E1234565:01BB 61234563:512B 01 00000000:00000000 00:00000000 00000000 33 0 339859 1 0000000000000000 24 4 29 8 7 + 30: E1234565:01BB D1234561:930C 03 00000000:00000000 01:00000024 00000003 0 0 0 0 0000000000000000 + 31: E1234565:01BB B1234561:21A3 03 00000000:00000000 01:0000008A 00000003 0 0 0 0 0000000000000000 + 32: E1234565:0B7A F123456E:759B 01 00000000:00000000 02:000AFBFD 00000000 0 0 368411 4 0000000000000000 23 6 19 10 -1 + 33: E1234565:01BB 91234561:A9FA 03 00000000:00000000 01:00000650 00000005 0 0 0 0 0000000000000000 + 34: E1234565:01BB D1234561:92DD 03 00000000:00000000 01:00000637 00000005 0 0 0 0 0000000000000000 + 35: E1234565:01BB 51234561:D9C3 03 00000000:00000000 01:00000005 00000001 0 0 0 0 0000000000000000 + 36: E1234565:01BB 91234561:6A2F 03 00000000:00000000 01:00000BEA 00000005 0 0 0 0 0000000000000000 + 37: E1234565:01BB F1234561:1544 03 00000000:00000000 01:000004D0 00000005 0 0 0 0 0000000000000000 diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..43d7b34 --- /dev/null +++ b/setup.sh @@ -0,0 +1,3 @@ +chmod 700 ban.sh unban.sh +chown root:root ban.sh unban.sh +#chattr +i ban.sh unban.sh # make files immutable diff --git a/unban.sh b/unban.sh new file mode 100644 index 0000000..c51567b --- /dev/null +++ b/unban.sh @@ -0,0 +1 @@ +iptables-nft -D INPUT -i "$1" -s "$2" -j DROP \ No newline at end of file