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:
duffyduck
2026-07-23 11:35:38 +02:00
co-authored by Claude Opus 4.8
parent 4fce0cd7f0
commit b170759657
6 changed files with 323 additions and 32 deletions
+143
View File
@@ -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 <public-ip-des-clients>
```
- `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 <N> Bereichen ... <LAND>` 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 <quelle>.<port> > …`)
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