From b170759657ad7a9c038e44ddd3c4c5681c7b0a32 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Thu, 23 Jul 2026 11:35:38 +0200 Subject: [PATCH] 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) --- README.md | 143 ++++++++++++++++++++++++++++++++++++++ geoblock-reassert.service | 14 ++++ geoblock-reassert.timer | 15 ++++ geoblock.py | 139 ++++++++++++++++++++++++++++-------- install_geoblock.sh | 38 +++++++++- uninstall_geoblock.sh | 6 ++ 6 files changed, 323 insertions(+), 32 deletions(-) create mode 100644 geoblock-reassert.service create mode 100644 geoblock-reassert.timer diff --git a/README.md b/README.md index b4abf6b..bb0c388 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ und droppt Pakete bereits auf Netzwerk-Ebene. | `geoblock.ini` | Eure eigene, lokale Konfiguration (Laenderliste, Chain, Logging, ...) — **nicht** im Repo/Git, siehe unten | | `geoblock.service` | systemd-Unit, fuehrt `--apply` aus | | `geoblock.timer` | systemd-Timer: beim Booten + einmal taeglich | +| `geoblock-reassert.service` | systemd-Unit, fuehrt `--reassert` aus (Watchdog, optional) | +| `geoblock-reassert.timer` | systemd-Timer: alle 2 Min pruefen ob die Regeln noch oben stehen (optional, nur bei `--with-watchdog`) | | `install_geoblock.sh` | Installer: kopiert alles nach `/opt/geoblock`, richtet Timer ein (Testserver optional, siehe unten) | | `geoblock_checkmk` | checkmk local-check Plugin (manuell kopieren) | | `geoblock_testserver.py` | Kleiner Test-Webserver, um die Regeln von aussen zu pruefen | @@ -120,10 +122,17 @@ ipset list -n | grep -i geoblock ```bash sudo python3 geoblock.py --config geoblock.ini --apply # aktivieren/aktualisieren sudo python3 geoblock.py --config geoblock.ini --apply --dry-run # nur simulieren +sudo python3 geoblock.py --config geoblock.ini --reassert # Regel-Position pruefen/korrigieren (ohne Download) sudo python3 geoblock.py --config geoblock.ini --status # Zustand pruefen sudo python3 geoblock.py --config geoblock.ini --remove # rueckgaengig ``` +`--reassert` ist ein leichtgewichtiger Watchdog: es laedt **keine** Zonefiles +von ipdeny (nutzt das bestehende ipset) und setzt die Block-Regeln nur dann neu +an den Anfang der Chain, wenn sie fehlen oder nach unten gerutscht sind. Gedacht +fuer Hosts mit gemanagter Firewall — siehe Abschnitt "Regeln bleiben nicht oben" +unten. + `--apply` ist idempotent (kein doppeltes Anlegen von Regeln) und tauscht das ipset atomar per `swap`, damit waehrend der Aktualisierung kein Loch im Blocking entsteht. @@ -133,6 +142,140 @@ zusaetzlich braucht: `iptables-persistent` / `netfilter-persistent save` nutzen — der systemd-Timer sorgt aber ohnehin bei jedem Boot fuer einen frischen `--apply`-Lauf, das reicht in der Praxis meist aus. +## Fehlersuche: "es wird nicht geblockt, obwohl konfiguriert" + +Typisches Symptom: Land ist in `countries`, `--apply` lief fehlerfrei — und +trotzdem kommt die Verbindung durch. Die Ursache laesst sich in drei Schritten +eindeutig eingrenzen (jeder Schritt schliesst eine Ursache aus): + +**1. Ist die IP ueberhaupt im ipset?** Das Blocking matcht gegen den *Inhalt* +des Sets, nicht gegen das Zonefile. Mit der echten oeffentlichen Quell-IP des +Test-Clients: + +```bash +sudo ipset test geoblock +``` + +- `is NOT in set` / `set geoblock does not exist` → das Set ist leer/veraltet. + Meist erreicht der Server ipdeny.com nicht (kein direkter Internetzugang → + `proxy` in der `.ini` setzen, siehe unten). Die `--apply`-Ausgabe pruefen: + dort muss `ipset 'geoblock' befuellt mit Bereichen ... ` stehen. +- `is in set` → Set ist ok, weiter mit Schritt 2. + +**2. Erreichen die Pakete die DROP-Regel — oder faengt sie vorher eine +ACCEPT-Regel ab?** Zaehler nullen, testen, ansehen: + +```bash +sudo iptables -Z INPUT # alle Paketzaehler auf 0 +# jetzt vom Client aus verbinden (z.B. check_openvpn.py, siehe unten) +sudo iptables -L INPUT -n -v --line-numbers # Zaehler ablesen +``` + +- **DROP-Zeile (`... match-set geoblock src ... -j DROP`) zaehlt hoch** → + Blocking greift, alles korrekt. +- **DROP-Zeile bleibt 0, aber eine LOG-Zeile weiter oben zaehlt hoch** → + klassisches **Reihenfolge-Problem**: das Paket wird von einer ACCEPT-Regel + *ueber* der DROP-Regel akzeptiert, bevor es dort ankommt. `geoblock.py` setzt + seine LOG-/DROP-Regeln zwar per `iptables -I` an den **Anfang** der Chain, + damit genau das nicht passiert — aber auf Hosts mit einer **gemanagten + Firewall** (Shorewall, firewalld, UFW, UTM-Appliances mit eigenen Sub-Chains + wie `CUSTOMINPUT`/`PROXYIN`/…) generiert die Firewall-Software die Chain bei + jedem Reload komplett neu und schiebt die extern eingefuegte Regel wieder nach + unten (oder wirft sie raus). Dann hilft entweder, das Geoblocking **im Regelwerk + der Firewall-Software selbst** einzuhaengen — oder der mitgelieferte + **Reassert-Watchdog**, der die Regeln periodisch wieder nach oben setzt (siehe + Abschnitt "Regeln bleiben nicht oben" unten). +- **Auch die LOG-Zeile bleibt 0** → das Paket erreicht diese Chain gar nicht + mit der erwarteten Quell-IP/Port. Weiter mit Schritt 3. + +**3. Kommt die echte Client-IP ueberhaupt am Server an? (tcpdump)** Auf dem +Geoblock-Host mitschneiden, waehrend der Client verbindet — hier am Beispiel +OpenVPN auf `1194/udp`, Port/Proto entsprechend anpassen: + +```bash +sudo tcpdump -ni any udp port 1194 +# fuer einen TCP-Dienst z.B.: sudo tcpdump -ni any tcp port 443 +``` + +Beispiel-Ausgabe (IPs anonymisiert; `203.0.113.7` = oeffentliche Client-IP, +`10.0.0.10` = private Adresse des Servers selbst): + +``` +10:40:34.253787 IP 203.0.113.7.33807 > 10.0.0.10.1194: UDP, length 14 # Client-Handshake kommt an +10:40:34.254013 IP 10.0.0.10.1194 > 203.0.113.7.33807: UDP, length 26 # Server ANTWORTET -> nicht geblockt +``` + +Kommt die **echte** Client-IP als Quelle an *und* antwortet der Server (zweite +Zeile), obwohl das Land geblockt sein sollte, ist der Paketweg sauber — dann +liegt es an der Regel-Reihenfolge (Schritt 2) oder am Set (Schritt 1), nicht am +Netzwerkpfad. + +Dann vom Client aus verbinden und die **Quell-IP** (`IP . > …`) +in der Ausgabe ansehen: + +- **Es erscheint die echte oeffentliche Client-IP** → Paketweg ist sauber, das + Problem liegt bei der Regel (Schritt 2, Reihenfolge) oder am Set (Schritt 1). +- **Es erscheint eine *andere* IP** (private Adresse, die eines Reverse-Proxys/ + Load Balancers) → davor sitzt ein NAT/Proxy, der die Quell-IP umschreibt. + Dann kann iptables/ipset per `src` prinzipiell nicht die echte Client-IP + filtern — Geoblocking muss dann eine Ebene weiter vorne greifen (siehe die + Abschnitte "Betrieb hinter einem Reverse-Proxy" und "…AWS Load Balancer" + unten). +- **Es erscheint gar nichts** → der Traffic laeuft nicht ueber diesen Host/Port + (falscher Server, anderer Port, oder eine vorgelagerte Firewall droppt schon + vorher). + +## Regeln bleiben nicht oben (gemanagte Firewall) — Reassert-Watchdog + +`geoblock.py` setzt seine Block-Regeln per `iptables -I` an den **Anfang** der +Chain, damit sie vor etwaigen ACCEPT-Regeln greifen. Auf Hosts mit einer +**gemanagten Firewall** (Shorewall, firewalld, UFW, UTM-Appliances wie +Securepoint/Kerio mit eigenen Sub-Chains) reicht das aber nicht dauerhaft: Die +Firewall-Software baut die Chain bei jedem eigenen Reload komplett neu und +schiebt fremde Regeln dabei nach unten — oder entfernt sie ganz. Ab dann greift +das Geoblocking nicht mehr (siehe Fehlersuche-Abschnitt oben, "Fall Reihenfolge"). + +Dagegen gibt es den **Reassert-Watchdog**: ein zusaetzlicher Timer, der +regelmaessig prueft, ob die Regeln noch ganz oben stehen, und sie ggf. wieder +nach oben setzt. Der Check ist billig — er laedt **nichts** von ipdeny nach, +sondern nutzt das bestehende ipset und fasst iptables nur an, wenn wirklich +etwas verrutscht ist. + +**Manuell:** + +```bash +sudo python3 geoblock.py --config geoblock.ini --reassert +# --status zeigt die aktuelle Position an: +sudo python3 geoblock.py --config geoblock.ini --status +# -> "Regel-Position in Chain 'INPUT': am Anfang (ok)" +# -> oder "... NACH UNTEN GERUTSCHT -- '--reassert' oder '--apply' setzt sie wieder an den Anfang" +``` + +**Automatisch (systemd-Timer, alle 2 Minuten):** bei der Installation die Option +`--with-watchdog` mitgeben: + +```bash +sudo ./install_geoblock.sh --with-watchdog +# ggf. kombiniert: +sudo ./install_geoblock.sh --with-testserver --with-watchdog /opt/geoblock +``` + +Das installiert und aktiviert `geoblock-reassert.timer` (+ `.service`). Takt +anpassen: `OnUnitActiveSec` in `/etc/systemd/system/geoblock-reassert.timer` +(z.B. `1min` bei Firewalls, die oft reloaden). Kontrolle: + +```bash +systemctl list-timers geoblock-reassert.timer +journalctl -u geoblock-reassert.service -n 20 +``` + +**Hinweis:** Der Watchdog ist ein pragmatischer Reparaturmechanismus, kein +Ersatz fuer eine saubere Integration. Wenn moeglich, ist es robuster, das +Geoblocking direkt im Regelwerk der Firewall-Software einzuhaengen — dann bleibt +die Regel-Reihenfolge auch ohne den Watchdog stabil. Ohne bestehendes ipset +(z.B. bevor je ein `--apply` lief) macht `--reassert` bewusst nichts; das +Befuellen uebernimmt der taegliche `geoblock.timer`. + ## Verbindungstest (Test-Webserver) `geoblock_testserver.py` ist ein winziger HTTP-Server ohne diff --git a/geoblock-reassert.service b/geoblock-reassert.service new file mode 100644 index 0000000..a2119b4 --- /dev/null +++ b/geoblock-reassert.service @@ -0,0 +1,14 @@ +[Unit] +Description=Geoblocking Watchdog - Block-Regeln am Anfang der Chain halten (prueft/korrigiert Position, ohne ipdeny-Download) +Documentation=https://git.hacker-net.de/Aria-Software/geoblocking-python-script +ConditionPathExists=__INSTALL_DIR__/geoblock.ini + +[Service] +Type=oneshot +User=root +# --reassert laedt KEINE Zonefiles (kein Netz noetig), nutzt das bestehende ipset +# und setzt die Regeln nur dann neu an den Anfang der Chain, wenn sie nach unten +# gerutscht sind oder fehlen -- z.B. weil eine gemanagte Firewall die Chain neu +# gebaut hat. Ist noch kein ipset vorhanden, tut es nichts (der geoblock.timer +# fuellt es beim naechsten --apply). +ExecStart=/usr/bin/python3 __INSTALL_DIR__/geoblock.py --config __INSTALL_DIR__/geoblock.ini --reassert diff --git a/geoblock-reassert.timer b/geoblock-reassert.timer new file mode 100644 index 0000000..d02d38c --- /dev/null +++ b/geoblock-reassert.timer @@ -0,0 +1,15 @@ +[Unit] +Description=Geoblocking Watchdog - regelmaessig pruefen, ob die Block-Regeln noch am Anfang der Chain stehen + +[Timer] +# Kurz nach dem Booten einmal, danach alle 2 Minuten. Der Lauf ist billig +# (nur ein paar iptables-Reads, kein ipdeny-Download) und greift nur ein, wenn +# die Regeln tatsaechlich nach unten gerutscht sind. Wer schneller/traeger will, +# passt OnUnitActiveSec an (z.B. 1min fuer Firewalls, die oft reloaden). +OnBootSec=3min +OnUnitActiveSec=2min +AccuracySec=30s +Unit=geoblock-reassert.service + +[Install] +WantedBy=timers.target diff --git a/geoblock.py b/geoblock.py index 6e22949..3e0d94f 100755 --- a/geoblock.py +++ b/geoblock.py @@ -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") diff --git a/install_geoblock.sh b/install_geoblock.sh index 722dc6f..c85e9ea 100755 --- a/install_geoblock.sh +++ b/install_geoblock.sh @@ -7,7 +7,7 @@ # aktualisiert. # # Nutzung (als root, im Verzeichnis mit allen Repo-Dateien): -# sudo ./install_geoblock.sh [--with-testserver] [ZIELVERZEICHNIS] +# sudo ./install_geoblock.sh [--with-testserver] [--with-watchdog] [ZIELVERZEICHNIS] # # Der kleine Test-Webserver (geoblock_testserver.py + # geoblock-testserver.service), gegen den man von aussen connecten @@ -16,10 +16,18 @@ # in Sachen Testserver gar nichts (kein Copy, kein systemd-Unit, kein # offener Port). # +# Der Reassert-Watchdog (geoblock-reassert.service + .timer) prueft alle +# 2 Minuten, ob die Block-Regeln noch am Anfang der iptables-Chain stehen, +# und setzt sie ggf. wieder nach oben (ohne ipdeny-Download). Nur noetig auf +# Hosts mit einer gemanagten Firewall (Shorewall/firewalld/UFW/UTM-Appliance), +# die die Chain neu baut und dabei fremde Regeln nach unten schiebt. Wird NUR +# bei --with-watchdog installiert/aktiviert. +# # Beispiele: # sudo ./install_geoblock.sh # sudo ./install_geoblock.sh --with-testserver -# sudo ./install_geoblock.sh --with-testserver /opt/geoblock +# sudo ./install_geoblock.sh --with-watchdog +# sudo ./install_geoblock.sh --with-testserver --with-watchdog /opt/geoblock # sudo ./install_geoblock.sh /opt/geoblock # # Benoetigt im selben Verzeichnis: @@ -30,6 +38,9 @@ # (nur bei --with-testserver:) # geoblock_testserver.py # geoblock-testserver.service +# (nur bei --with-watchdog:) +# geoblock-reassert.service +# geoblock-reassert.timer # # Voraussetzungen auf dem Host: python3, iptables, ipset, systemd. @@ -43,12 +54,16 @@ fi SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WITH_TESTSERVER=0 +WITH_WATCHDOG=0 INSTALL_DIR="" for arg in "$@"; do case "$arg" in --with-testserver) WITH_TESTSERVER=1 ;; + --with-watchdog) + WITH_WATCHDOG=1 + ;; -h|--help) grep '^# ' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' exit 0 @@ -65,6 +80,9 @@ REQUIRED_FILES=(geoblock.py geoblock.ini.example geoblock.service geoblock.timer if [[ "$WITH_TESTSERVER" -eq 1 ]]; then REQUIRED_FILES+=(geoblock_testserver.py geoblock-testserver.service) fi +if [[ "$WITH_WATCHDOG" -eq 1 ]]; then + REQUIRED_FILES+=(geoblock-reassert.service geoblock-reassert.timer) +fi for f in "${REQUIRED_FILES[@]}"; do if [[ ! -f "$SRC_DIR/$f" ]]; then @@ -114,6 +132,15 @@ else echo "==> Test-Webserver wird NICHT installiert (Option --with-testserver nicht angegeben)." fi +if [[ "$WITH_WATCHDOG" -eq 1 ]]; then + echo "==> --with-watchdog gesetzt: installiere Reassert-Watchdog (haelt die Regeln am Chain-Anfang)" + sed "s#__INSTALL_DIR__#$INSTALL_DIR#g" "$SRC_DIR/geoblock-reassert.service" \ + > /etc/systemd/system/geoblock-reassert.service + cp "$SRC_DIR/geoblock-reassert.timer" /etc/systemd/system/geoblock-reassert.timer +else + echo "==> Reassert-Watchdog wird NICHT installiert (Option --with-watchdog nicht angegeben)." +fi + systemctl daemon-reload systemctl enable --now geoblock.timer @@ -121,12 +148,19 @@ if [[ "$WITH_TESTSERVER" -eq 1 ]]; then systemctl enable --now geoblock-testserver.service fi +if [[ "$WITH_WATCHDOG" -eq 1 ]]; then + systemctl enable --now geoblock-reassert.timer +fi + echo "" echo "==> Fertig." echo " Config anpassen: $INSTALL_DIR/geoblock.ini" echo " Jetzt sofort testen: systemctl start geoblock.service && journalctl -u geoblock.service -f" echo " Timer-Status: systemctl list-timers geoblock.timer" echo " Naechste/letzte Läufe: systemctl status geoblock.timer" +if [[ "$WITH_WATCHDOG" -eq 1 ]]; then + echo " Watchdog-Status: systemctl list-timers geoblock-reassert.timer" +fi echo " Log-Datei: $LOG_DIR/geoblock.log" if [[ "$WITH_TESTSERVER" -eq 1 ]]; then echo " Test-Webserver: systemctl status geoblock-testserver.service" diff --git a/uninstall_geoblock.sh b/uninstall_geoblock.sh index f8510a0..7abd4de 100755 --- a/uninstall_geoblock.sh +++ b/uninstall_geoblock.sh @@ -84,6 +84,12 @@ echo "==> Deinstalliere Geoblocking komplett ($INSTALL_DIR)" stop_disable_rm_unit "geoblock-testserver.service" rm -f "$INSTALL_DIR/geoblock_testserver.py" +# Reassert-Watchdog (falls installiert) zuerst stoppen, damit er nicht direkt +# nach dem --remove die Regeln neu setzt. (stop_disable_rm_unit ignoriert +# nicht vorhandene Units.) +stop_disable_rm_unit "geoblock-reassert.timer" +stop_disable_rm_unit "geoblock-reassert.service" + # iptables/ipset-Regeln sauber abbauen, SOLANGE Script+Config noch da sind. if [[ -f "$INSTALL_DIR/geoblock.py" && -f "$INSTALL_DIR/geoblock.ini" ]]; then echo "==> Entferne iptables/ipset-Regeln (geoblock.py --remove)"