geoblock: Regeln an den Chain-Anfang setzen + Reassert-Watchdog fuer gemanagte Firewalls
Problem: auf Hosts mit gemanagter Firewall (z.B. Endian-UTM) stand die DROP-Regel am Ende der INPUT-Chain (-A) und wurde von einer ACCEPT-Regel davor abgefangen -> Geoblocking griff nicht, obwohl Set/Config korrekt waren. geoblock.py: - DROP-Regel jetzt wie die LOG-Regel per 'iptables -I' an den ANFANG der Chain (Reihenfolge LOG -> DROP). Rule-Setzen in set_iptables_rules() refaktoriert. - Neue Aktion --reassert: leichtgewichtiger Watchdog, der die Regel-Position prueft und die Regeln nur bei Bedarf wieder nach oben setzt -- OHNE Zonefiles von ipdeny nachzuladen (nutzt bestehendes ipset). Kein Eingriff, wenn kein ipset existiert (dann erst voller --apply noetig). - geoblock_rules_on_top(): erkennt, ob die Regeln luueckenlos die ersten der Chain sind. --status zeigt die Position jetzt mit an. systemd: - geoblock-reassert.service + .timer (alle 2 Min, opt-in via install_geoblock.sh --with-watchdog). uninstall raeumt sie mit ab und stoppt sie vor --remove. README: Abschnitt "Regeln bleiben nicht oben (gemanagte Firewall)" + tcpdump- Diagnose (anonymisiert) + Fehlersuche-Abschnitt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4fce0cd7f0
commit
b170759657
+109
-30
@@ -49,8 +49,12 @@ AUTOMATISCH AKTUELL HALTEN (Systemstart + taeglich):
|
||||
Siehe systemd/geoblock.service + geoblock.timer sowie
|
||||
install_geoblock.sh im gleichen Repo — das richtet Timer fuer
|
||||
"beim Booten" + "einmal taeglich" automatisch ein.
|
||||
--apply ist idempotent: es flusht das bestehende Set und befuellt es neu,
|
||||
legt aber KEINE doppelte iptables-Regel an (wird vorher geprueft).
|
||||
--apply ist idempotent: es flusht das bestehende Set und befuellt es neu und
|
||||
entfernt vorher ALLE bestehenden geoblock-Regeln der Chain (auch verwaiste aus
|
||||
frueheren Laeufen mit anderer ini), bevor es die Regeln frisch setzt. Die
|
||||
DROP-/LOG-Regeln werden dabei per 'iptables -I' an den ANFANG der Chain gesetzt,
|
||||
damit sie vor etwaigen ACCEPT-Regeln greifen (wichtig auf Hosts mit gemanagter
|
||||
Firewall, die eigene ACCEPT-Regeln/Sub-Chains mitbringt).
|
||||
|
||||
PORT-EINSCHRAENKUNG (optional):
|
||||
Per Default blockt das Script ALLE Ports/Protokolle fuer die gelisteten
|
||||
@@ -325,6 +329,85 @@ def purge_geoblock_rules(cfg, dry_run=False):
|
||||
return removed
|
||||
|
||||
|
||||
def set_iptables_rules(cfg, dry_run=False):
|
||||
"""Setzt die LOG-/DROP-Regel(n) mit -I an den ANFANG der Chain, damit sie VOR
|
||||
etwaigen ACCEPT-Regeln greifen. Sonst akzeptiert eine weiter oben stehende
|
||||
ACCEPT-Regel das Paket und eine ans Ende (-A) gehaengte DROP-Regel kommt nie
|
||||
dran -- typisch auf Hosts mit gemanagter Firewall (Shorewall/firewalld/UFW/
|
||||
UTM-Appliances mit eigenen Sub-Chains).
|
||||
|
||||
Setzt voraus, dass das ipset bereits existiert/befuellt ist UND die Chain
|
||||
vorher per purge_geoblock_rules() bereinigt wurde (sonst entstehen Dubletten).
|
||||
Pro Variante wird DROP zuerst an den Anfang gesetzt, dann LOG davor -> die
|
||||
finale Reihenfolge in der Chain ist LOG vor DROP (erst loggen, dann droppen).
|
||||
"""
|
||||
name = cfg["ipset_name"]
|
||||
set_match = ["-m", "set", "--match-set", name, "src"]
|
||||
iface_opt = ["-i", cfg["interface"]] if cfg["interface"] else []
|
||||
for variant in build_rule_variants(cfg):
|
||||
proto_args = _variant_match_args(variant)
|
||||
port_args = _variant_port_args(variant)
|
||||
label = _variant_label(variant)
|
||||
|
||||
run(["iptables", "-I", cfg["chain"]] + iface_opt + proto_args + set_match +
|
||||
port_args + ["-j", "DROP"], dry_run=dry_run)
|
||||
log(f"DROP-Regel am Anfang von Chain '{cfg['chain']}' aktiv fuer Set '{name}' ({label})")
|
||||
|
||||
if cfg["log"]:
|
||||
run(["iptables", "-I", cfg["chain"]] + iface_opt + proto_args + set_match +
|
||||
port_args + ["-j", "LOG", "--log-prefix", cfg["log_prefix"] + " "], dry_run=dry_run)
|
||||
log(f"LOG-Regel am Anfang von Chain '{cfg['chain']}' aktiv ({label})")
|
||||
|
||||
|
||||
def geoblock_rules_on_top(cfg):
|
||||
"""Prueft, ob die geoblock-Regeln (noch) ganz am Anfang der Chain stehen.
|
||||
|
||||
Gibt (on_top, present) zurueck:
|
||||
present : ob ueberhaupt geoblock-Regeln in der Chain vorhanden sind
|
||||
on_top : ob sie luueckenlos die ERSTEN Regeln der Chain sind (Index 0..k-1)
|
||||
|
||||
Ist present=True aber on_top=False, sind unsere Regeln nach unten gerutscht
|
||||
(z.B. weil eine gemanagte Firewall die Chain neu gebaut und eigene Regeln
|
||||
davor eingehaengt hat) -- dann sollten sie neu an den Anfang gesetzt werden.
|
||||
"""
|
||||
needle = f"--match-set {cfg['ipset_name']} src"
|
||||
rules = _list_chain_rules(cfg["chain"]) # geordnete Liste der '-A ...'-Regeln
|
||||
idx = [i for i, ln in enumerate(rules) if needle in ln]
|
||||
present = bool(idx)
|
||||
on_top = present and idx == list(range(len(idx)))
|
||||
return on_top, present
|
||||
|
||||
|
||||
def reassert_rules(cfg, dry_run=False):
|
||||
"""Watchdog: stellt sicher, dass die geoblock-Regeln am ANFANG der Chain stehen
|
||||
-- OHNE die Zonefiles neu von ipdeny zu laden (nutzt das bestehende ipset).
|
||||
|
||||
Gedacht fuer einen haeufigen Timer auf Hosts mit gemanagter Firewall, die die
|
||||
Chain periodisch neu baut und dabei unsere Regeln nach unten schiebt oder
|
||||
entfernt. Fehlt das ipset komplett, wird NICHT eingegriffen -- dann muss erst
|
||||
ein voller --apply laufen, der das Set befuellt (der taegliche Timer erledigt das).
|
||||
|
||||
Rueckgabe: True, wenn korrigiert wurde; False, wenn nichts zu tun war.
|
||||
"""
|
||||
name = cfg["ipset_name"]
|
||||
if not ipset_exists(name):
|
||||
log(f"ipset '{name}' existiert nicht — reassert uebersprungen (erst voller --apply noetig).")
|
||||
return False
|
||||
|
||||
on_top, present = geoblock_rules_on_top(cfg)
|
||||
if on_top:
|
||||
log("geoblock-Regeln stehen bereits am Anfang der Chain — nichts zu tun.")
|
||||
return False
|
||||
|
||||
if present:
|
||||
log("geoblock-Regeln nach unten gerutscht — setze sie neu an den Anfang der Chain.")
|
||||
else:
|
||||
log("geoblock-Regeln fehlen in der Chain — setze sie neu an den Anfang.")
|
||||
purge_geoblock_rules(cfg, dry_run=dry_run) # evtl. verrutschte Reste einsammeln
|
||||
set_iptables_rules(cfg, dry_run=dry_run)
|
||||
return True
|
||||
|
||||
|
||||
def apply_geoblock(cfg, dry_run=False):
|
||||
name = cfg["ipset_name"]
|
||||
|
||||
@@ -361,33 +444,10 @@ def apply_geoblock(cfg, dry_run=False):
|
||||
# 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 []
|
||||
variants = build_rule_variants(cfg)
|
||||
|
||||
for variant in variants:
|
||||
proto_args = _variant_match_args(variant)
|
||||
port_args = _variant_port_args(variant)
|
||||
label = _variant_label(variant)
|
||||
|
||||
if cfg["log"]:
|
||||
log_check = (["iptables", "-C", cfg["chain"]] + iface_opt + proto_args + set_match +
|
||||
port_args + ["-j", "LOG", "--log-prefix", cfg["log_prefix"] + " "])
|
||||
exists = False if dry_run else subprocess.run(log_check, capture_output=True).returncode == 0
|
||||
if not exists:
|
||||
run(["iptables", "-I", cfg["chain"]] + iface_opt + proto_args + set_match +
|
||||
port_args + ["-j", "LOG", "--log-prefix", cfg["log_prefix"] + " "], dry_run=dry_run)
|
||||
else:
|
||||
log(f"LOG-Regel ({label}) existiert bereits, ueberspringe")
|
||||
|
||||
if iptables_rule_exists(cfg["chain"], name, cfg["interface"], variant=variant, dry_run=dry_run):
|
||||
log(f"DROP-Regel ({label}) existiert bereits, ueberspringe (idempotent)")
|
||||
else:
|
||||
run(["iptables", "-A", cfg["chain"]] + iface_opt + proto_args + set_match +
|
||||
port_args + ["-j", "DROP"], dry_run=dry_run)
|
||||
log(f"DROP-Regel in Chain '{cfg['chain']}' aktiv fuer Set '{name}' ({label})")
|
||||
# 5) LOG-/DROP-Regel(n) frisch an den ANFANG der Chain setzen (siehe
|
||||
# set_iptables_rules -- gleiche Logik nutzt auch --reassert). Der purge oben
|
||||
# hat die Chain vorher von alten geoblock-Regeln befreit, daher keine Dubletten.
|
||||
set_iptables_rules(cfg, dry_run=dry_run)
|
||||
|
||||
log("Fertig. Hinweis: Regeln sind NICHT reboot-persistent — "
|
||||
"fuer Persistenz z.B. 'iptables-persistent' bzw. 'netfilter-persistent save' nutzen, "
|
||||
@@ -418,6 +478,16 @@ def show_status(cfg):
|
||||
for variant in variants:
|
||||
active = iptables_rule_exists(cfg["chain"], name, cfg["interface"], variant=variant)
|
||||
print(f"iptables DROP-Regel in Chain '{cfg['chain']}' ({_variant_label(variant)}) aktiv: {active}")
|
||||
|
||||
on_top, present = geoblock_rules_on_top(cfg)
|
||||
if not present:
|
||||
print(f"Regel-Position in Chain '{cfg['chain']}': keine geoblock-Regeln vorhanden")
|
||||
elif on_top:
|
||||
print(f"Regel-Position in Chain '{cfg['chain']}': am Anfang (ok)")
|
||||
else:
|
||||
print(f"Regel-Position in Chain '{cfg['chain']}': NACH UNTEN GERUTSCHT "
|
||||
f"-- '--reassert' oder '--apply' setzt sie wieder an den Anfang")
|
||||
|
||||
print(f"Konfigurierte Laender: {', '.join(c.upper() for c in cfg['countries'])}")
|
||||
if cfg["ports"]:
|
||||
print(f"Port-Einschraenkung: {', '.join(cfg['ports'])} ({'/'.join(cfg['protocols'])})")
|
||||
@@ -432,12 +502,17 @@ def main():
|
||||
parser.add_argument("--config", required=True, help="Pfad zur .ini-Konfigurationsdatei")
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("--apply", action="store_true", help="Laender laden und Block-Regeln setzen/aktualisieren")
|
||||
group.add_argument("--reassert", action="store_true",
|
||||
help="Nur pruefen ob die Block-Regeln noch am Anfang der Chain stehen und sie "
|
||||
"ggf. wieder nach oben setzen -- OHNE Zonefiles neu zu laden (schneller Watchdog "
|
||||
"fuer gemanagte Firewalls, die die Chain neu bauen). Setzt ein bestehendes ipset voraus.")
|
||||
group.add_argument("--remove", action="store_true", help="Block-Regeln und ipset wieder entfernen")
|
||||
group.add_argument("--status", action="store_true", help="Aktuellen Zustand anzeigen")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Nur anzeigen was passieren wuerde, nichts aendern")
|
||||
args = parser.parse_args()
|
||||
|
||||
action = "apply" if args.apply else ("remove" if args.remove else "status")
|
||||
action = ("apply" if args.apply else "reassert" if args.reassert
|
||||
else "remove" if args.remove else "status")
|
||||
|
||||
try:
|
||||
cfg = load_config(args.config)
|
||||
@@ -447,6 +522,10 @@ def main():
|
||||
n_countries, n_entries = apply_geoblock(cfg, dry_run=args.dry_run)
|
||||
log_result("OK", action, countries=n_countries, entries=n_entries,
|
||||
msg=f"Geoblock aktualisiert ({n_countries} Laender, {n_entries} IP-Bereiche)")
|
||||
elif args.reassert:
|
||||
corrected = reassert_rules(cfg, dry_run=args.dry_run)
|
||||
log_result("OK", action,
|
||||
msg="Regeln neu an den Anfang gesetzt" if corrected else "Regeln bereits am Anfang, nichts zu tun")
|
||||
elif args.remove:
|
||||
remove_geoblock(cfg, dry_run=args.dry_run)
|
||||
log_result("OK", action, msg="Geoblock-Regeln entfernt")
|
||||
|
||||
Reference in New Issue
Block a user