From 4fce0cd7f04a57c3397b3ce2d6b6e1d11fa1d0b4 Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Thu, 23 Jul 2026 10:20:50 +0200 Subject: [PATCH] geoblock.py: vor Apply/Remove alle bestehenden geoblock-Regeln der Chain purgen Neue Funktion purge_geoblock_rules() entfernt per 'iptables -S ' ALLE Regeln mit '--match-set src' (LOG wie DROP), unabhaengig von Proto/Port/Variante. Das Loeschkommando wird per shlex direkt aus der -S-Ausgabe rekonstruiert, damit --log-prefix inkl. Leerzeichen exakt passt. - --apply purged jetzt vor dem Neu-Setzen der Regeln, aber erst NACH dem atomaren Befuellen des ipsets -> kein Traffic-Loch waehrend des ipdeny-Downloads. - --remove nutzt denselben Purge statt der bisherigen varianten-basierten Loeschung -> keine verwaisten Regeln mehr, wenn sich ports/protocol zwischen zwei Laeufen geaendert haben. Co-Authored-By: Claude Opus 4.8 (1M context) --- geoblock.py | 75 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/geoblock.py b/geoblock.py index f0d6768..6e22949 100755 --- a/geoblock.py +++ b/geoblock.py @@ -65,6 +65,7 @@ import argparse import configparser import datetime import ipaddress +import shlex import subprocess import sys import time @@ -286,6 +287,44 @@ def iptables_rule_exists(chain, ipset_name, interface, variant=None, dry_run=Fal return result.returncode == 0 +def _list_chain_rules(chain): + """Gibt die aktuellen Regeln einer iptables-Chain als '-A ...'-Zeilen zurueck + (Format von 'iptables -S ', eine Zeile pro Regel).""" + result = subprocess.run(["iptables", "-S", chain], capture_output=True, text=True) + if result.returncode != 0: + return [] + return [ln for ln in result.stdout.splitlines() if ln.startswith("-A ")] + + +def purge_geoblock_rules(cfg, dry_run=False): + """Loescht ALLE Regeln in der Chain, die auf das geoblock-ipset verweisen + (--match-set src) -- unabhaengig von Proto/Port/Variante. + + Dadurch werden auch alte Regeln aus frueheren --apply-Laeufen mit anderer + ini (z.B. andere Ports/Protokolle) sauber entfernt, die build_rule_variants(cfg) + fuer die AKTUELLE Config gar nicht mehr kennt. Wir rekonstruieren das Loesch- + Kommando direkt aus 'iptables -S' (per shlex, damit z.B. das --log-prefix mit + Leerzeichen erhalten bleibt), statt die Args selbst nachzubauen -- so passt das + -D immer exakt zur real existierenden Regel. + """ + name = cfg["ipset_name"] + chain = cfg["chain"] + needle = f"--match-set {name} src" + removed = 0 + for line in _list_chain_rules(chain): + if needle not in line: + continue + tokens = shlex.split(line) # z.B. ["-A", "INPUT", "-m", "set", ...] + # "-A " -> "iptables -D " + run(["iptables", "-D"] + tokens[1:], dry_run=dry_run, check=False) + removed += 1 + if removed: + log(f"{removed} bestehende geoblock-Regel(n) aus Chain '{chain}' entfernt (Set '{name}')") + else: + log(f"Keine bestehenden geoblock-Regeln in Chain '{chain}' gefunden") + return removed + + def apply_geoblock(cfg, dry_run=False): name = cfg["ipset_name"] @@ -316,7 +355,13 @@ def apply_geoblock(cfg, dry_run=False): log(f"ipset '{name}' befuellt mit {len(all_cidrs)} Bereichen " f"aus Laendern: {', '.join(c.upper() for c in cfg['countries'])}") - # 4) optionale LOG-Regel(n) + DROP-Regel(n) in iptables, nur wenn noch nicht vorhanden. + # 4) Vor dem Neu-Setzen ALLE bestehenden geoblock-Regeln der Chain entfernen + # (auch verwaiste aus frueheren Laeufen mit anderer ini, z.B. andere Ports). + # Erst JETZT (nach dem Befuellen des Sets), damit waehrend des ipdeny-Downloads + # der bestehende Block aktiv bleibt und kein Traffic-Loch entsteht. + purge_geoblock_rules(cfg, dry_run=dry_run) + + # 5) LOG-Regel(n) + DROP-Regel(n) frisch setzen. # Eine Variante pro Protokoll wenn 'ports' gesetzt ist, sonst eine Variante (alle Ports). set_match = ["-m", "set", "--match-set", name, "src"] iface_opt = ["-i", cfg["interface"]] if cfg["interface"] else [] @@ -353,30 +398,10 @@ def apply_geoblock(cfg, dry_run=False): def remove_geoblock(cfg, dry_run=False): name = cfg["ipset_name"] - iface_opt = ["-i", cfg["interface"]] if cfg["interface"] else [] - set_match = ["-m", "set", "--match-set", name, "src"] - # Entfernt fuer alle aktuell konfigurierten Varianten. Falls die .ini seit dem letzten - # --apply geaendert wurde (z.B. ports entfernt/hinzugefuegt), koennen dadurch alte - # Regeln mit anderer Variante stehen bleiben -- im Zweifel per 'iptables -L -n' - # pruefen und manuell aufraeumen. - for variant in build_rule_variants(cfg): - proto_args = _variant_match_args(variant) - port_args = _variant_port_args(variant) - - for _ in range(5): - result = run(["iptables", "-D", cfg["chain"]] + iface_opt + proto_args + set_match + - port_args + ["-j", "DROP"], dry_run=dry_run, check=False) - if dry_run or result is None or result.returncode != 0: - break - - if cfg["log"]: - for _ in range(5): - result = run(["iptables", "-D", cfg["chain"]] + iface_opt + proto_args + set_match + - port_args + ["-j", "LOG", "--log-prefix", cfg["log_prefix"] + " "], - dry_run=dry_run, check=False) - if dry_run or result is None or result.returncode != 0: - break - + # Loescht ALLE geoblock-Regeln der Chain (auch verwaiste aus frueheren Laeufen + # mit anderer ini), nicht nur die aktuell konfigurierten Varianten -- so bleiben + # keine Reste stehen, egal wie sich 'ports'/'protocol' zwischenzeitlich geaendert haben. + purge_geoblock_rules(cfg, dry_run=dry_run) run(["ipset", "destroy", name], dry_run=dry_run, check=False) log(f"Geoblock-Regeln entfernt, ipset '{name}' geloescht.")