geoblock.py: vor Apply/Remove alle bestehenden geoblock-Regeln der Chain purgen

Neue Funktion purge_geoblock_rules() entfernt per 'iptables -S <chain>' ALLE
Regeln mit '--match-set <name> 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) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-07-23 10:20:50 +02:00
co-authored by Claude Opus 4.8
parent b5ff887cea
commit 4fce0cd7f0
+50 -25
View File
@@ -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 <chain>', 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 <name> 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 <chain> <rest>" -> "iptables -D <chain> <rest>"
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 <chain> -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.")