geoblock: mehrere iptables-Chains unterstuetzen (chain = INPUT, FORWARD)
'chain' akzeptiert jetzt eine komma-getrennte Liste. Noetig, wenn der geschuetzte Dienst hinter einer Firewall/einem Router liegt (Portweiterleitung/DNAT): solcher Traffic laeuft durch die FORWARD-Chain und wird von einer reinen INPUT-Regel nie gesehen. "chain = INPUT, FORWARD" deckt beide Wege ab. - load_config: cfg["chain"] -> cfg["chains"] (Liste), Default ["INPUT"]. - apply/purge/set_iptables_rules/reassert/status/Watchdog arbeiten ueber alle konfigurierten Chains. geoblock_rules_on_top aggregiert (on_top nur, wenn in JEDER Chain oben; present, wenn in mindestens einer vorhanden) -- neuer Helper _chain_rules_position() prueft je Chain. --status zeigt Position pro Chain. - Doku: geoblock.ini.example + README-Abschnitt "INPUT vs FORWARD" (inkl. DNAT-Diagnose und Endian-Hinweis). 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
867a8a1c6f
commit
da83f351d7
@@ -297,6 +297,49 @@ die Regel-Reihenfolge auch ohne den Watchdog stabil. Ohne bestehendes ipset
|
|||||||
(z.B. bevor je ein `--apply` lief) macht `--reassert` bewusst nichts; das
|
(z.B. bevor je ein `--apply` lief) macht `--reassert` bewusst nichts; das
|
||||||
Befuellen uebernimmt der taegliche `geoblock.timer`.
|
Befuellen uebernimmt der taegliche `geoblock.timer`.
|
||||||
|
|
||||||
|
## INPUT vs FORWARD (Dienst hinter Firewall/Router)
|
||||||
|
|
||||||
|
`chain` entscheidet, welchen Traffic Geoblocking ueberhaupt zu sehen bekommt:
|
||||||
|
|
||||||
|
- **`INPUT`** — Pakete, die **an diesen Host selbst** gerichtet sind. Richtig,
|
||||||
|
wenn der geschuetzte Dienst (Webserver, VPN, …) **auf demselben Host** laeuft,
|
||||||
|
auf dem auch `geoblock.py` die Regeln setzt.
|
||||||
|
- **`FORWARD`** — Pakete, die dieser Host **weiterleitet/routet**, statt sie
|
||||||
|
selbst anzunehmen. Noetig, wenn `geoblock.py` auf einer **Firewall/einem
|
||||||
|
Router** laeuft und der eigentliche Dienst auf einem **dahinterliegenden Host**
|
||||||
|
sitzt (klassisch: Portweiterleitung/DNAT von der Firewall auf einen internen
|
||||||
|
Server). Solche Pakete laufen durch die FORWARD-Chain und werden von einer
|
||||||
|
INPUT-Regel **nie** gesehen.
|
||||||
|
|
||||||
|
**Typisches Symptom fuer den FORWARD-Fall:** Set stimmt (`ipset test` = is in),
|
||||||
|
die DROP-Regel steht in INPUT ganz oben — und trotzdem kommt die Verbindung
|
||||||
|
durch, weil das Paket gar nicht ueber INPUT laeuft. `tcpdump` zeigt dann als
|
||||||
|
Ziel eine **interne/private IP** (der Backend-Host), nicht die des Firewall-Hosts
|
||||||
|
selbst. Gegencheck auf dem Firewall-Host:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo iptables -t nat -L PREROUTING -n -v | grep <port> # DNAT-Portweiterleitung vorhanden?
|
||||||
|
sudo iptables -L FORWARD -n -v --line-numbers # laeuft der Traffic hier durch?
|
||||||
|
```
|
||||||
|
|
||||||
|
Gibt es eine DNAT-Weiterleitung, muss die Chain auf `FORWARD` (oder zur
|
||||||
|
Sicherheit beides):
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[geoblock]
|
||||||
|
chain = INPUT, FORWARD
|
||||||
|
```
|
||||||
|
|
||||||
|
Bei mehreren Chains legt `--apply` die Regeln in **jeder** an, `--remove` raeumt
|
||||||
|
in jeder auf, und `--reassert`/`--status`/der Watchdog pruefen/korrigieren jede
|
||||||
|
Chain einzeln. Der `src`-Match (Quell-IP = echter Client) funktioniert in FORWARD
|
||||||
|
genauso wie in INPUT, weil DNAT nur das Ziel umschreibt, nicht die Quelle.
|
||||||
|
|
||||||
|
> **Endian & Co.:** Laeuft der Dienst (z.B. der OpenVPN-Server) hinter der
|
||||||
|
> Appliance und wird per Portweiterleitung erreicht, ist FORWARD der richtige
|
||||||
|
> Ort. Kombiniere das am besten mit dem Reassert-Watchdog (Abschnitt oben), da
|
||||||
|
> die Appliance auch die FORWARD-Chain bei jedem Reload neu baut.
|
||||||
|
|
||||||
## Verbindungstest (Test-Webserver)
|
## Verbindungstest (Test-Webserver)
|
||||||
|
|
||||||
`geoblock_testserver.py` ist ein winziger HTTP-Server ohne
|
`geoblock_testserver.py` ist ein winziger HTTP-Server ohne
|
||||||
@@ -412,7 +455,10 @@ inkl. Perfdata (Alter in Sekunden, Anzahl geblockter IP-Bereiche).
|
|||||||
Siehe Kommentare in der Datei selbst — kurz:
|
Siehe Kommentare in der Datei selbst — kurz:
|
||||||
|
|
||||||
- `countries`: Komma-Liste ISO-3166-1-alpha-2 Codes (z.B. `RU, CN, KP, IR`)
|
- `countries`: Komma-Liste ISO-3166-1-alpha-2 Codes (z.B. `RU, CN, KP, IR`)
|
||||||
- `chain`: iptables-Chain (`INPUT` = eingehender Traffic zu diesem Host)
|
- `chain`: iptables-Chain(s), eine oder komma-getrennt mehrere. `INPUT` =
|
||||||
|
Traffic zu diesem Host (Standard); `FORWARD` = durch diesen Host **gerouteter**
|
||||||
|
Traffic (Dienst hinter Firewall/Router, per Portweiterleitung/DNAT). Beides
|
||||||
|
zugleich: `chain = INPUT, FORWARD`. Siehe Abschnitt "INPUT vs FORWARD" unten.
|
||||||
- `interface`: optional, nur ein Interface pruefen
|
- `interface`: optional, nur ein Interface pruefen
|
||||||
- `log`: iptables-LOG-Eintrag vor dem DROP (dmesg/kern.log) an/aus
|
- `log`: iptables-LOG-Eintrag vor dem DROP (dmesg/kern.log) an/aus
|
||||||
- `ipset_name`: Name des ipset-Sets
|
- `ipset_name`: Name des ipset-Sets
|
||||||
|
|||||||
@@ -4,8 +4,13 @@
|
|||||||
; grossgeschrieben, egal), die geblockt werden sollen.
|
; grossgeschrieben, egal), die geblockt werden sollen.
|
||||||
; Liste der Codes: https://www.ipdeny.com/ipblocks/
|
; Liste der Codes: https://www.ipdeny.com/ipblocks/
|
||||||
;
|
;
|
||||||
; chain: iptables-Chain, in die die DROP-Regel eingehaengt wird.
|
; chain: iptables-Chain(s), in die die DROP-Regel eingehaengt wird. Eine
|
||||||
|
; oder komma-getrennt mehrere.
|
||||||
; INPUT = Verbindungen ZU diesem Host (Standard-Anwendungsfall).
|
; INPUT = Verbindungen ZU diesem Host (Standard-Anwendungsfall).
|
||||||
|
; FORWARD = durch diesen Host GEROUTETER Traffic -- noetig, wenn der
|
||||||
|
; zu schuetzende Dienst hinter einer Firewall/einem Router
|
||||||
|
; liegt (z.B. Portweiterleitung/DNAT auf einen internen
|
||||||
|
; Host). Beides zugleich: "chain = INPUT, FORWARD".
|
||||||
;
|
;
|
||||||
; interface: optional, z.B. eth0 -- nur Traffic auf diesem Interface pruefen.
|
; interface: optional, z.B. eth0 -- nur Traffic auf diesem Interface pruefen.
|
||||||
; Leer lassen = alle Interfaces.
|
; Leer lassen = alle Interfaces.
|
||||||
|
|||||||
+49
-29
@@ -164,9 +164,16 @@ def load_config(path):
|
|||||||
if pr not in ("tcp", "udp"):
|
if pr not in ("tcp", "udp"):
|
||||||
raise ValueError(f"Ungueltiges Protokoll '{pr}' in 'protocol' (erlaubt: tcp, udp)")
|
raise ValueError(f"Ungueltiges Protokoll '{pr}' in 'protocol' (erlaubt: tcp, udp)")
|
||||||
|
|
||||||
|
# chain: eine oder (komma-getrennt) mehrere iptables-Chains. INPUT = Traffic ZU
|
||||||
|
# diesem Host, FORWARD = durch diesen Host GEROUTETER Traffic (z.B. Portweiter-
|
||||||
|
# leitung/DNAT auf einen internen Host hinter einer Firewall/Router). "INPUT, FORWARD"
|
||||||
|
# deckt beides ab. Default: INPUT.
|
||||||
|
chains_raw = sec.get("chain", "INPUT").strip()
|
||||||
|
chains = [c.strip() for c in chains_raw.split(",") if c.strip()] or ["INPUT"]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"countries": countries,
|
"countries": countries,
|
||||||
"chain": sec.get("chain", "INPUT").strip(),
|
"chains": chains,
|
||||||
"interface": sec.get("interface", "").strip(),
|
"interface": sec.get("interface", "").strip(),
|
||||||
"log": sec.getboolean("log", fallback=False),
|
"log": sec.getboolean("log", fallback=False),
|
||||||
"log_prefix": sec.get("log_prefix", "GEOBLOCK-DROP:").strip(),
|
"log_prefix": sec.get("log_prefix", "GEOBLOCK-DROP:").strip(),
|
||||||
@@ -301,8 +308,8 @@ def _list_chain_rules(chain):
|
|||||||
|
|
||||||
|
|
||||||
def purge_geoblock_rules(cfg, dry_run=False):
|
def purge_geoblock_rules(cfg, dry_run=False):
|
||||||
"""Loescht ALLE Regeln in der Chain, die auf das geoblock-ipset verweisen
|
"""Loescht in ALLEN konfigurierten Chains alle Regeln, die auf das geoblock-ipset
|
||||||
(--match-set <name> src) -- unabhaengig von Proto/Port/Variante.
|
verweisen (--match-set <name> src) -- unabhaengig von Proto/Port/Variante.
|
||||||
|
|
||||||
Dadurch werden auch alte Regeln aus frueheren --apply-Laeufen mit anderer
|
Dadurch werden auch alte Regeln aus frueheren --apply-Laeufen mit anderer
|
||||||
ini (z.B. andere Ports/Protokolle) sauber entfernt, die build_rule_variants(cfg)
|
ini (z.B. andere Ports/Protokolle) sauber entfernt, die build_rule_variants(cfg)
|
||||||
@@ -312,20 +319,22 @@ def purge_geoblock_rules(cfg, dry_run=False):
|
|||||||
-D immer exakt zur real existierenden Regel.
|
-D immer exakt zur real existierenden Regel.
|
||||||
"""
|
"""
|
||||||
name = cfg["ipset_name"]
|
name = cfg["ipset_name"]
|
||||||
chain = cfg["chain"]
|
|
||||||
needle = f"--match-set {name} src"
|
needle = f"--match-set {name} src"
|
||||||
removed = 0
|
removed = 0
|
||||||
|
for chain in cfg["chains"]:
|
||||||
|
n = 0
|
||||||
for line in _list_chain_rules(chain):
|
for line in _list_chain_rules(chain):
|
||||||
if needle not in line:
|
if needle not in line:
|
||||||
continue
|
continue
|
||||||
tokens = shlex.split(line) # z.B. ["-A", "INPUT", "-m", "set", ...]
|
tokens = shlex.split(line) # z.B. ["-A", "INPUT", "-m", "set", ...]
|
||||||
# "-A <chain> <rest>" -> "iptables -D <chain> <rest>"
|
# "-A <chain> <rest>" -> "iptables -D <chain> <rest>"
|
||||||
run(["iptables", "-D"] + tokens[1:], dry_run=dry_run, check=False)
|
run(["iptables", "-D"] + tokens[1:], dry_run=dry_run, check=False)
|
||||||
removed += 1
|
n += 1
|
||||||
if removed:
|
if n:
|
||||||
log(f"{removed} bestehende geoblock-Regel(n) aus Chain '{chain}' entfernt (Set '{name}')")
|
log(f"{n} bestehende geoblock-Regel(n) aus Chain '{chain}' entfernt (Set '{name}')")
|
||||||
else:
|
removed += n
|
||||||
log(f"Keine bestehenden geoblock-Regeln in Chain '{chain}' gefunden")
|
if not removed:
|
||||||
|
log(f"Keine bestehenden geoblock-Regeln in Chains {cfg['chains']} gefunden")
|
||||||
return removed
|
return removed
|
||||||
|
|
||||||
|
|
||||||
@@ -344,40 +353,50 @@ def set_iptables_rules(cfg, dry_run=False):
|
|||||||
name = cfg["ipset_name"]
|
name = cfg["ipset_name"]
|
||||||
set_match = ["-m", "set", "--match-set", name, "src"]
|
set_match = ["-m", "set", "--match-set", name, "src"]
|
||||||
iface_opt = ["-i", cfg["interface"]] if cfg["interface"] else []
|
iface_opt = ["-i", cfg["interface"]] if cfg["interface"] else []
|
||||||
|
for chain in cfg["chains"]:
|
||||||
for variant in build_rule_variants(cfg):
|
for variant in build_rule_variants(cfg):
|
||||||
proto_args = _variant_match_args(variant)
|
proto_args = _variant_match_args(variant)
|
||||||
port_args = _variant_port_args(variant)
|
port_args = _variant_port_args(variant)
|
||||||
label = _variant_label(variant)
|
label = _variant_label(variant)
|
||||||
|
|
||||||
run(["iptables", "-I", cfg["chain"]] + iface_opt + proto_args + set_match +
|
run(["iptables", "-I", chain] + iface_opt + proto_args + set_match +
|
||||||
port_args + ["-j", "DROP"], dry_run=dry_run)
|
port_args + ["-j", "DROP"], dry_run=dry_run)
|
||||||
log(f"DROP-Regel am Anfang von Chain '{cfg['chain']}' aktiv fuer Set '{name}' ({label})")
|
log(f"DROP-Regel am Anfang von Chain '{chain}' aktiv fuer Set '{name}' ({label})")
|
||||||
|
|
||||||
if cfg["log"]:
|
if cfg["log"]:
|
||||||
run(["iptables", "-I", cfg["chain"]] + iface_opt + proto_args + set_match +
|
run(["iptables", "-I", chain] + iface_opt + proto_args + set_match +
|
||||||
port_args + ["-j", "LOG", "--log-prefix", cfg["log_prefix"] + " "], dry_run=dry_run)
|
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})")
|
log(f"LOG-Regel am Anfang von Chain '{chain}' aktiv ({label})")
|
||||||
|
|
||||||
|
|
||||||
def geoblock_rules_on_top(cfg):
|
def _chain_rules_position(chain, ipset_name):
|
||||||
"""Prueft, ob die geoblock-Regeln (noch) ganz am Anfang der Chain stehen.
|
"""(on_top, present) fuer eine EINZELNE Chain.
|
||||||
|
|
||||||
Gibt (on_top, present) zurueck:
|
|
||||||
present : ob ueberhaupt geoblock-Regeln in der Chain vorhanden sind
|
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)
|
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"
|
needle = f"--match-set {ipset_name} src"
|
||||||
rules = _list_chain_rules(cfg["chain"]) # geordnete Liste der '-A ...'-Regeln
|
rules = _list_chain_rules(chain) # geordnete Liste der '-A ...'-Regeln
|
||||||
idx = [i for i, ln in enumerate(rules) if needle in ln]
|
idx = [i for i, ln in enumerate(rules) if needle in ln]
|
||||||
present = bool(idx)
|
present = bool(idx)
|
||||||
on_top = present and idx == list(range(len(idx)))
|
on_top = present and idx == list(range(len(idx)))
|
||||||
return on_top, present
|
return on_top, present
|
||||||
|
|
||||||
|
|
||||||
|
def geoblock_rules_on_top(cfg):
|
||||||
|
"""Aggregiert die Regel-Position ueber ALLE konfigurierten Chains.
|
||||||
|
|
||||||
|
Gibt (on_top, present) zurueck:
|
||||||
|
on_top : True nur, wenn in JEDER konfigurierten Chain die Regeln vorhanden
|
||||||
|
UND ganz oben sind (Index 0..k-1). Sonst False -> --reassert sollte
|
||||||
|
korrigieren (fehlende neu setzen, verrutschte wieder nach oben).
|
||||||
|
present : True, wenn mindestens eine Chain geoblock-Regeln hat.
|
||||||
|
"""
|
||||||
|
results = [_chain_rules_position(ch, cfg["ipset_name"]) for ch in cfg["chains"]]
|
||||||
|
on_top = all(on for on, _ in results)
|
||||||
|
present = any(pr for _, pr in results)
|
||||||
|
return on_top, present
|
||||||
|
|
||||||
|
|
||||||
def reassert_rules(cfg, dry_run=False):
|
def reassert_rules(cfg, dry_run=False):
|
||||||
"""Watchdog: stellt sicher, dass die geoblock-Regeln am ANFANG der Chain stehen
|
"""Watchdog: stellt sicher, dass die geoblock-Regeln am ANFANG der Chain stehen
|
||||||
-- OHNE die Zonefiles neu von ipdeny zu laden (nutzt das bestehende ipset).
|
-- OHNE die Zonefiles neu von ipdeny zu laden (nutzt das bestehende ipset).
|
||||||
@@ -475,17 +494,18 @@ def show_status(cfg):
|
|||||||
print(f"ipset '{name}' existiert nicht (noch nicht aktiv).")
|
print(f"ipset '{name}' existiert nicht (noch nicht aktiv).")
|
||||||
|
|
||||||
variants = build_rule_variants(cfg)
|
variants = build_rule_variants(cfg)
|
||||||
|
for chain in cfg["chains"]:
|
||||||
for variant in variants:
|
for variant in variants:
|
||||||
active = iptables_rule_exists(cfg["chain"], name, cfg["interface"], variant=variant)
|
active = iptables_rule_exists(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 '{chain}' ({_variant_label(variant)}) aktiv: {active}")
|
||||||
|
|
||||||
on_top, present = geoblock_rules_on_top(cfg)
|
on_top, present = _chain_rules_position(chain, name)
|
||||||
if not present:
|
if not present:
|
||||||
print(f"Regel-Position in Chain '{cfg['chain']}': keine geoblock-Regeln vorhanden")
|
print(f"Regel-Position in Chain '{chain}': keine geoblock-Regeln vorhanden")
|
||||||
elif on_top:
|
elif on_top:
|
||||||
print(f"Regel-Position in Chain '{cfg['chain']}': am Anfang (ok)")
|
print(f"Regel-Position in Chain '{chain}': am Anfang (ok)")
|
||||||
else:
|
else:
|
||||||
print(f"Regel-Position in Chain '{cfg['chain']}': NACH UNTEN GERUTSCHT "
|
print(f"Regel-Position in Chain '{chain}': NACH UNTEN GERUTSCHT "
|
||||||
f"-- '--reassert' oder '--apply' setzt sie wieder an den Anfang")
|
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'])}")
|
||||||
|
|||||||
Reference in New Issue
Block a user