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
@@ -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.ini` | Eure eigene, lokale Konfiguration (Laenderliste, Chain, Logging, ...) — **nicht** im Repo/Git, siehe unten |
|
||||||
| `geoblock.service` | systemd-Unit, fuehrt `--apply` aus |
|
| `geoblock.service` | systemd-Unit, fuehrt `--apply` aus |
|
||||||
| `geoblock.timer` | systemd-Timer: beim Booten + einmal taeglich |
|
| `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) |
|
| `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_checkmk` | checkmk local-check Plugin (manuell kopieren) |
|
||||||
| `geoblock_testserver.py` | Kleiner Test-Webserver, um die Regeln von aussen zu pruefen |
|
| `geoblock_testserver.py` | Kleiner Test-Webserver, um die Regeln von aussen zu pruefen |
|
||||||
@@ -120,10 +122,17 @@ ipset list -n | grep -i geoblock
|
|||||||
```bash
|
```bash
|
||||||
sudo python3 geoblock.py --config geoblock.ini --apply # aktivieren/aktualisieren
|
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 --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 --status # Zustand pruefen
|
||||||
sudo python3 geoblock.py --config geoblock.ini --remove # rueckgaengig
|
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
|
`--apply` ist idempotent (kein doppeltes Anlegen von Regeln) und tauscht
|
||||||
das ipset atomar per `swap`, damit waehrend der Aktualisierung kein Loch
|
das ipset atomar per `swap`, damit waehrend der Aktualisierung kein Loch
|
||||||
im Blocking entsteht.
|
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
|
nutzen — der systemd-Timer sorgt aber ohnehin bei jedem Boot fuer einen
|
||||||
frischen `--apply`-Lauf, das reicht in der Praxis meist aus.
|
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)
|
## Verbindungstest (Test-Webserver)
|
||||||
|
|
||||||
`geoblock_testserver.py` ist ein winziger HTTP-Server ohne
|
`geoblock_testserver.py` ist ein winziger HTTP-Server ohne
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
+109
-30
@@ -49,8 +49,12 @@ AUTOMATISCH AKTUELL HALTEN (Systemstart + taeglich):
|
|||||||
Siehe systemd/geoblock.service + geoblock.timer sowie
|
Siehe systemd/geoblock.service + geoblock.timer sowie
|
||||||
install_geoblock.sh im gleichen Repo — das richtet Timer fuer
|
install_geoblock.sh im gleichen Repo — das richtet Timer fuer
|
||||||
"beim Booten" + "einmal taeglich" automatisch ein.
|
"beim Booten" + "einmal taeglich" automatisch ein.
|
||||||
--apply ist idempotent: es flusht das bestehende Set und befuellt es neu,
|
--apply ist idempotent: es flusht das bestehende Set und befuellt es neu und
|
||||||
legt aber KEINE doppelte iptables-Regel an (wird vorher geprueft).
|
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):
|
PORT-EINSCHRAENKUNG (optional):
|
||||||
Per Default blockt das Script ALLE Ports/Protokolle fuer die gelisteten
|
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
|
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):
|
def apply_geoblock(cfg, dry_run=False):
|
||||||
name = cfg["ipset_name"]
|
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.
|
# der bestehende Block aktiv bleibt und kein Traffic-Loch entsteht.
|
||||||
purge_geoblock_rules(cfg, dry_run=dry_run)
|
purge_geoblock_rules(cfg, dry_run=dry_run)
|
||||||
|
|
||||||
# 5) LOG-Regel(n) + DROP-Regel(n) frisch setzen.
|
# 5) LOG-/DROP-Regel(n) frisch an den ANFANG der Chain setzen (siehe
|
||||||
# Eine Variante pro Protokoll wenn 'ports' gesetzt ist, sonst eine Variante (alle Ports).
|
# set_iptables_rules -- gleiche Logik nutzt auch --reassert). Der purge oben
|
||||||
set_match = ["-m", "set", "--match-set", name, "src"]
|
# hat die Chain vorher von alten geoblock-Regeln befreit, daher keine Dubletten.
|
||||||
iface_opt = ["-i", cfg["interface"]] if cfg["interface"] else []
|
set_iptables_rules(cfg, dry_run=dry_run)
|
||||||
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})")
|
|
||||||
|
|
||||||
log("Fertig. Hinweis: Regeln sind NICHT reboot-persistent — "
|
log("Fertig. Hinweis: Regeln sind NICHT reboot-persistent — "
|
||||||
"fuer Persistenz z.B. 'iptables-persistent' bzw. 'netfilter-persistent save' nutzen, "
|
"fuer Persistenz z.B. 'iptables-persistent' bzw. 'netfilter-persistent save' nutzen, "
|
||||||
@@ -418,6 +478,16 @@ def show_status(cfg):
|
|||||||
for variant in variants:
|
for variant in variants:
|
||||||
active = iptables_rule_exists(cfg["chain"], name, cfg["interface"], variant=variant)
|
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}")
|
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'])}")
|
print(f"Konfigurierte Laender: {', '.join(c.upper() for c in cfg['countries'])}")
|
||||||
if cfg["ports"]:
|
if cfg["ports"]:
|
||||||
print(f"Port-Einschraenkung: {', '.join(cfg['ports'])} ({'/'.join(cfg['protocols'])})")
|
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")
|
parser.add_argument("--config", required=True, help="Pfad zur .ini-Konfigurationsdatei")
|
||||||
group = parser.add_mutually_exclusive_group(required=True)
|
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("--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("--remove", action="store_true", help="Block-Regeln und ipset wieder entfernen")
|
||||||
group.add_argument("--status", action="store_true", help="Aktuellen Zustand anzeigen")
|
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")
|
parser.add_argument("--dry-run", action="store_true", help="Nur anzeigen was passieren wuerde, nichts aendern")
|
||||||
args = parser.parse_args()
|
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:
|
try:
|
||||||
cfg = load_config(args.config)
|
cfg = load_config(args.config)
|
||||||
@@ -447,6 +522,10 @@ def main():
|
|||||||
n_countries, n_entries = apply_geoblock(cfg, dry_run=args.dry_run)
|
n_countries, n_entries = apply_geoblock(cfg, dry_run=args.dry_run)
|
||||||
log_result("OK", action, countries=n_countries, entries=n_entries,
|
log_result("OK", action, countries=n_countries, entries=n_entries,
|
||||||
msg=f"Geoblock aktualisiert ({n_countries} Laender, {n_entries} IP-Bereiche)")
|
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:
|
elif args.remove:
|
||||||
remove_geoblock(cfg, dry_run=args.dry_run)
|
remove_geoblock(cfg, dry_run=args.dry_run)
|
||||||
log_result("OK", action, msg="Geoblock-Regeln entfernt")
|
log_result("OK", action, msg="Geoblock-Regeln entfernt")
|
||||||
|
|||||||
+36
-2
@@ -7,7 +7,7 @@
|
|||||||
# aktualisiert.
|
# aktualisiert.
|
||||||
#
|
#
|
||||||
# Nutzung (als root, im Verzeichnis mit allen Repo-Dateien):
|
# 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 +
|
# Der kleine Test-Webserver (geoblock_testserver.py +
|
||||||
# geoblock-testserver.service), gegen den man von aussen connecten
|
# geoblock-testserver.service), gegen den man von aussen connecten
|
||||||
@@ -16,10 +16,18 @@
|
|||||||
# in Sachen Testserver gar nichts (kein Copy, kein systemd-Unit, kein
|
# in Sachen Testserver gar nichts (kein Copy, kein systemd-Unit, kein
|
||||||
# offener Port).
|
# 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:
|
# Beispiele:
|
||||||
# sudo ./install_geoblock.sh
|
# sudo ./install_geoblock.sh
|
||||||
# sudo ./install_geoblock.sh --with-testserver
|
# 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
|
# sudo ./install_geoblock.sh /opt/geoblock
|
||||||
#
|
#
|
||||||
# Benoetigt im selben Verzeichnis:
|
# Benoetigt im selben Verzeichnis:
|
||||||
@@ -30,6 +38,9 @@
|
|||||||
# (nur bei --with-testserver:)
|
# (nur bei --with-testserver:)
|
||||||
# geoblock_testserver.py
|
# geoblock_testserver.py
|
||||||
# geoblock-testserver.service
|
# geoblock-testserver.service
|
||||||
|
# (nur bei --with-watchdog:)
|
||||||
|
# geoblock-reassert.service
|
||||||
|
# geoblock-reassert.timer
|
||||||
#
|
#
|
||||||
# Voraussetzungen auf dem Host: python3, iptables, ipset, systemd.
|
# Voraussetzungen auf dem Host: python3, iptables, ipset, systemd.
|
||||||
|
|
||||||
@@ -43,12 +54,16 @@ fi
|
|||||||
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
WITH_TESTSERVER=0
|
WITH_TESTSERVER=0
|
||||||
|
WITH_WATCHDOG=0
|
||||||
INSTALL_DIR=""
|
INSTALL_DIR=""
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
--with-testserver)
|
--with-testserver)
|
||||||
WITH_TESTSERVER=1
|
WITH_TESTSERVER=1
|
||||||
;;
|
;;
|
||||||
|
--with-watchdog)
|
||||||
|
WITH_WATCHDOG=1
|
||||||
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
grep '^# ' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
grep '^# ' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
||||||
exit 0
|
exit 0
|
||||||
@@ -65,6 +80,9 @@ REQUIRED_FILES=(geoblock.py geoblock.ini.example geoblock.service geoblock.timer
|
|||||||
if [[ "$WITH_TESTSERVER" -eq 1 ]]; then
|
if [[ "$WITH_TESTSERVER" -eq 1 ]]; then
|
||||||
REQUIRED_FILES+=(geoblock_testserver.py geoblock-testserver.service)
|
REQUIRED_FILES+=(geoblock_testserver.py geoblock-testserver.service)
|
||||||
fi
|
fi
|
||||||
|
if [[ "$WITH_WATCHDOG" -eq 1 ]]; then
|
||||||
|
REQUIRED_FILES+=(geoblock-reassert.service geoblock-reassert.timer)
|
||||||
|
fi
|
||||||
|
|
||||||
for f in "${REQUIRED_FILES[@]}"; do
|
for f in "${REQUIRED_FILES[@]}"; do
|
||||||
if [[ ! -f "$SRC_DIR/$f" ]]; then
|
if [[ ! -f "$SRC_DIR/$f" ]]; then
|
||||||
@@ -114,6 +132,15 @@ else
|
|||||||
echo "==> Test-Webserver wird NICHT installiert (Option --with-testserver nicht angegeben)."
|
echo "==> Test-Webserver wird NICHT installiert (Option --with-testserver nicht angegeben)."
|
||||||
fi
|
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 daemon-reload
|
||||||
systemctl enable --now geoblock.timer
|
systemctl enable --now geoblock.timer
|
||||||
|
|
||||||
@@ -121,12 +148,19 @@ if [[ "$WITH_TESTSERVER" -eq 1 ]]; then
|
|||||||
systemctl enable --now geoblock-testserver.service
|
systemctl enable --now geoblock-testserver.service
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "$WITH_WATCHDOG" -eq 1 ]]; then
|
||||||
|
systemctl enable --now geoblock-reassert.timer
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "==> Fertig."
|
echo "==> Fertig."
|
||||||
echo " Config anpassen: $INSTALL_DIR/geoblock.ini"
|
echo " Config anpassen: $INSTALL_DIR/geoblock.ini"
|
||||||
echo " Jetzt sofort testen: systemctl start geoblock.service && journalctl -u geoblock.service -f"
|
echo " Jetzt sofort testen: systemctl start geoblock.service && journalctl -u geoblock.service -f"
|
||||||
echo " Timer-Status: systemctl list-timers geoblock.timer"
|
echo " Timer-Status: systemctl list-timers geoblock.timer"
|
||||||
echo " Naechste/letzte Läufe: systemctl status 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"
|
echo " Log-Datei: $LOG_DIR/geoblock.log"
|
||||||
if [[ "$WITH_TESTSERVER" -eq 1 ]]; then
|
if [[ "$WITH_TESTSERVER" -eq 1 ]]; then
|
||||||
echo " Test-Webserver: systemctl status geoblock-testserver.service"
|
echo " Test-Webserver: systemctl status geoblock-testserver.service"
|
||||||
|
|||||||
@@ -84,6 +84,12 @@ echo "==> Deinstalliere Geoblocking komplett ($INSTALL_DIR)"
|
|||||||
stop_disable_rm_unit "geoblock-testserver.service"
|
stop_disable_rm_unit "geoblock-testserver.service"
|
||||||
rm -f "$INSTALL_DIR/geoblock_testserver.py"
|
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.
|
# iptables/ipset-Regeln sauber abbauen, SOLANGE Script+Config noch da sind.
|
||||||
if [[ -f "$INSTALL_DIR/geoblock.py" && -f "$INSTALL_DIR/geoblock.ini" ]]; then
|
if [[ -f "$INSTALL_DIR/geoblock.py" && -f "$INSTALL_DIR/geoblock.ini" ]]; then
|
||||||
echo "==> Entferne iptables/ipset-Regeln (geoblock.py --remove)"
|
echo "==> Entferne iptables/ipset-Regeln (geoblock.py --remove)"
|
||||||
|
|||||||
Reference in New Issue
Block a user