Compare commits
2
Commits
af2a75c443
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
830e9a61f6 | ||
|
|
161d90de45 |
@@ -352,6 +352,60 @@ genauso wie in INPUT, weil DNAT nur das Ziel umschreibt, nicht die Quelle.
|
|||||||
> Ort. Kombiniere das am besten mit dem Reassert-Watchdog (Abschnitt oben), da
|
> Ort. Kombiniere das am besten mit dem Reassert-Watchdog (Abschnitt oben), da
|
||||||
> die Appliance auch die FORWARD-Chain bei jedem Reload neu baut.
|
> die Appliance auch die FORWARD-Chain bei jedem Reload neu baut.
|
||||||
|
|
||||||
|
## Dienst hinter Portweiterleitung/DNAT (`match_original_dport`)
|
||||||
|
|
||||||
|
Noch eine Stolperfalle bei Firewalls/Routern mit Portweiterleitung — und zwar
|
||||||
|
eine gemeine, weil Chain und Regel-Position stimmen koennen und es **trotzdem**
|
||||||
|
nicht blockt: **der Zielport wird durch DNAT umgeschrieben, bevor der Filter
|
||||||
|
laeuft.**
|
||||||
|
|
||||||
|
Reihenfolge im Kernel:
|
||||||
|
|
||||||
|
```
|
||||||
|
PREROUTING (nat/DNAT: schreibt Ziel-IP/Port um) -> Routing -> INPUT/FORWARD (filter: unsere DROP-Regel)
|
||||||
|
```
|
||||||
|
|
||||||
|
Beispiel Endian-OpenVPN: aussen kommt `udp/1194` an, die Appliance macht per DNAT
|
||||||
|
`1194 -> 6666-6667` (interner OpenVPN-Daemon). Wenn das Paket die FORWARD-/INPUT-
|
||||||
|
Chain erreicht, hat es als Zielport laengst **6666/6667**, nicht mehr 1194. Eine
|
||||||
|
Regel mit `ports = 1194` matcht dann **nie** — obwohl Set, Chain und Position
|
||||||
|
stimmen. (Gleiches Muster fuer Web: `443 -> 30443` o.ae.)
|
||||||
|
|
||||||
|
Wie man das erkennt — auf dem Firewall-Host die DNAT-Regeln ansehen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo iptables -t nat -L PREROUTING -n -v # zeigt meist nur Spruenge in Sub-Chains
|
||||||
|
sudo iptables -t nat -L PORTFW -n -v # (Endian) Portweiterleitungen
|
||||||
|
sudo iptables -t nat -L PROXIES -n -v # (Endian) weitere DNAT/Proxy-Regeln
|
||||||
|
# gesucht: eine DNAT-Zeile mit 'dpt:<oeffentlicher-port>' und 'to:<intern>:<anderer-port>'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zwei Loesungen:**
|
||||||
|
|
||||||
|
1. **Den umgeschriebenen Port matchen** — `ports` auf den *internen* Port setzen
|
||||||
|
(im Beispiel `6666:6667`). Funktioniert, ist aber fragil: aendert die Appliance
|
||||||
|
den internen Port (Update/Umbau), musst du es nachziehen.
|
||||||
|
2. **Empfohlen: `match_original_dport = true`** — matcht per `conntrack
|
||||||
|
--ctorigdstport` den Port, den der **Client** gewaehlt hat (vor DNAT). Dann
|
||||||
|
traegst du in `ports` die **oeffentlichen** Ports ein und bist immun gegen den
|
||||||
|
internen Remap:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[geoblock]
|
||||||
|
chain = INPUT, FORWARD
|
||||||
|
ports = 1194, 443
|
||||||
|
protocol = tcp, udp
|
||||||
|
match_original_dport = true
|
||||||
|
```
|
||||||
|
|
||||||
|
Der `src`-Match (echte Client-IP) funktioniert in beiden Faellen, weil DNAT nur
|
||||||
|
das Ziel umschreibt, nicht die Quelle — vorausgesetzt es ist echtes DNAT und kein
|
||||||
|
terminierender L7-Proxy (der wuerde die Verbindung neu aufbauen, dann sieht der
|
||||||
|
Filter die Proxy-IP statt der des Clients; siehe Reverse-Proxy-Abschnitt).
|
||||||
|
|
||||||
|
`--status` zeigt den aktiven Match-Modus an (`Zielport (dports)` vs.
|
||||||
|
`Original-Zielport vor DNAT (conntrack)`).
|
||||||
|
|
||||||
## Verbindungstest (Test-Webserver)
|
## Verbindungstest (Test-Webserver)
|
||||||
|
|
||||||
`geoblock_testserver.py` ist ein winziger HTTP-Server ohne
|
`geoblock_testserver.py` ist ein winziger HTTP-Server ohne
|
||||||
@@ -482,6 +536,16 @@ Siehe Kommentare in der Datei selbst — kurz:
|
|||||||
einzelner Ports" unten.
|
einzelner Ports" unten.
|
||||||
- `protocol`: nur relevant wenn `ports` gesetzt ist — `tcp` (Default), `udp`
|
- `protocol`: nur relevant wenn `ports` gesetzt ist — `tcp` (Default), `udp`
|
||||||
oder `tcp,udp`
|
oder `tcp,udp`
|
||||||
|
- `match_original_dport`: `true`/`false` (Default `false`), nur relevant wenn
|
||||||
|
`ports` gesetzt ist. Fuer Dienste hinter einer Portweiterleitung/DNAT: matcht
|
||||||
|
den **urspruenglichen** Zielport (vor DNAT) statt des umgeschriebenen. Dann in
|
||||||
|
`ports` die oeffentlichen Ports eintragen. Siehe Abschnitt "INPUT vs FORWARD"
|
||||||
|
bzw. "Dienst hinter Portweiterleitung/DNAT" unten.
|
||||||
|
- `block_ips`: optional, komma-getrennte Liste **eigener** IPs/CIDRs (IPv4), die
|
||||||
|
**zusaetzlich** zu den Laender-Ranges geblockt werden (z.B. bekannte Angreifer,
|
||||||
|
einzelne Hosts/Netze). Einzel-IP (`203.0.113.7`) oder CIDR (`203.0.113.0/24`).
|
||||||
|
Kommt ins selbe `ipset` wie die Laender, wird also von denselben Regeln/Ports
|
||||||
|
erfasst. Leer = keine.
|
||||||
- `proxy`: optional, HTTP(S)-Proxy fuer den Abruf der Zonefiles von ipdeny.com
|
- `proxy`: optional, HTTP(S)-Proxy fuer den Abruf der Zonefiles von ipdeny.com
|
||||||
(z.B. `http://10.0.0.5:3128`, mit Auth `http://user:pass@host:port`). Nur
|
(z.B. `http://10.0.0.5:3128`, mit Auth `http://user:pass@host:port`). Nur
|
||||||
noetig, wenn der Host selbst keine direkte Internetverbindung hat, aber
|
noetig, wenn der Host selbst keine direkte Internetverbindung hat, aber
|
||||||
|
|||||||
@@ -46,6 +46,22 @@
|
|||||||
; oder "tcp,udp" fuer beides. Fuer die meisten Web-/App-Ports
|
; oder "tcp,udp" fuer beides. Fuer die meisten Web-/App-Ports
|
||||||
; reicht "tcp".
|
; reicht "tcp".
|
||||||
;
|
;
|
||||||
|
; match_original_dport:
|
||||||
|
; optional, true/false (Default false). Nur relevant wenn "ports"
|
||||||
|
; gesetzt ist. Fuer Dienste hinter einer Portweiterleitung/DNAT
|
||||||
|
; (z.B. Endian: public 1194/udp -> intern 6666): iptables sieht im
|
||||||
|
; Filter sonst den UMGESCHRIEBENEN Port, nicht den, den der Client
|
||||||
|
; gewaehlt hat -- die Regel greift dann nicht. Mit true wird per
|
||||||
|
; conntrack der ORIGINAL-Zielport (vor DNAT) gematcht. Dann in "ports"
|
||||||
|
; die OEFFENTLICHEN Ports eintragen (z.B. 1194, 443), nicht die
|
||||||
|
; internen -- immun gegen den Remap. Meist zusammen mit chain=FORWARD.
|
||||||
|
;
|
||||||
|
; block_ips: optional, komma-getrennte Liste EIGENER IPs/CIDRs, die zusaetzlich
|
||||||
|
; zu den Laender-Ranges geblockt werden (z.B. bekannte Angreifer,
|
||||||
|
; einzelne Hosts/Netze). Einzel-IP ("203.0.113.7") oder CIDR
|
||||||
|
; ("203.0.113.0/24"). Nur IPv4. Kommt ins selbe ipset wie die Laender,
|
||||||
|
; wird also von denselben Regeln/Ports erfasst. Leer lassen = keine.
|
||||||
|
;
|
||||||
; proxy: optional, HTTP(S)-Proxy-URL, ueber den das Script ipdeny.com
|
; proxy: optional, HTTP(S)-Proxy-URL, ueber den das Script ipdeny.com
|
||||||
; erreicht -- fuer Hosts, die zwar aus dem Internet erreichbar
|
; erreicht -- fuer Hosts, die zwar aus dem Internet erreichbar
|
||||||
; sind (z.B. hinter Reverse-Proxy/Firewall), selbst aber KEINE
|
; sind (z.B. hinter Reverse-Proxy/Firewall), selbst aber KEINE
|
||||||
@@ -67,6 +83,9 @@ log_file = /var/log/geoblock/geoblock.log
|
|||||||
; z.B. "8899" setzen (nur der Testserver wird geblockt, SSH bleibt offen).
|
; z.B. "8899" setzen (nur der Testserver wird geblockt, SSH bleibt offen).
|
||||||
ports =
|
ports =
|
||||||
protocol = tcp
|
protocol = tcp
|
||||||
|
; Eigene Block-IPs/CIDRs zusaetzlich zu den Laendern (komma-getrennt, IPv4).
|
||||||
|
; Leer lassen = keine. Beispiel: block_ips = 203.0.113.7, 198.51.100.0/24
|
||||||
|
block_ips =
|
||||||
; Leer lassen, ausser der Host hat keine direkte Internetverbindung.
|
; Leer lassen, ausser der Host hat keine direkte Internetverbindung.
|
||||||
proxy =
|
proxy =
|
||||||
|
|
||||||
|
|||||||
+59
-11
@@ -178,6 +178,25 @@ def load_config(path):
|
|||||||
chains_raw = sec.get("chain", "INPUT").strip()
|
chains_raw = sec.get("chain", "INPUT").strip()
|
||||||
chains = [c.strip() for c in chains_raw.split(",") if c.strip()] or ["INPUT"]
|
chains = [c.strip() for c in chains_raw.split(",") if c.strip()] or ["INPUT"]
|
||||||
|
|
||||||
|
# match_original_dport: bei Diensten hinter einer Portweiterleitung/DNAT matcht
|
||||||
|
# 'ports' sonst den UMGESCHRIEBENEN Port (nach DNAT), nicht den, den der Client
|
||||||
|
# gewaehlt hat. Mit true wird per conntrack der ORIGINAL-Zielport gematcht -> in
|
||||||
|
# 'ports' die oeffentlichen Ports (z.B. 1194, 443) eintragen, immun gegen Remap.
|
||||||
|
match_orig_dport = sec.getboolean("match_original_dport", fallback=False)
|
||||||
|
|
||||||
|
# block_ips: optionale, komma-getrennte Liste EIGENER IPs/CIDRs, die zusaetzlich
|
||||||
|
# zu den Laender-Ranges ins selbe ipset kommen (z.B. bekannte Angreifer, einzelne
|
||||||
|
# Hosts/Netze). Nur IPv4 (das ipset ist IPv4). Wird hier validiert + normalisiert.
|
||||||
|
block_ips = []
|
||||||
|
for entry in (e.strip() for e in sec.get("block_ips", "").split(",") if e.strip()):
|
||||||
|
try:
|
||||||
|
net = ipaddress.ip_network(entry, strict=False) # akzeptiert Host (->/32) und CIDR
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(f"Ungueltige IP/CIDR in 'block_ips': '{entry}'")
|
||||||
|
if net.version != 4:
|
||||||
|
raise ValueError(f"'block_ips': nur IPv4 unterstuetzt (ipset ist IPv4), ungueltig: '{entry}'")
|
||||||
|
block_ips.append(str(net))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"countries": countries,
|
"countries": countries,
|
||||||
"chains": chains,
|
"chains": chains,
|
||||||
@@ -188,6 +207,8 @@ def load_config(path):
|
|||||||
"log_file": sec.get("log_file", "").strip(),
|
"log_file": sec.get("log_file", "").strip(),
|
||||||
"ports": ports,
|
"ports": ports,
|
||||||
"protocols": protocols,
|
"protocols": protocols,
|
||||||
|
"match_orig_dport": match_orig_dport,
|
||||||
|
"block_ips": block_ips,
|
||||||
"proxy": sec.get("proxy", "").strip(),
|
"proxy": sec.get("proxy", "").strip(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,14 +227,25 @@ def build_rule_variants(cfg):
|
|||||||
"""Baut die Liste der (proto, dports)-Kombinationen fuer iptables-Regeln.
|
"""Baut die Liste der (proto, dports)-Kombinationen fuer iptables-Regeln.
|
||||||
|
|
||||||
Ohne 'ports' in der .ini: eine Variante ohne Proto-/Port-Einschraenkung
|
Ohne 'ports' in der .ini: eine Variante ohne Proto-/Port-Einschraenkung
|
||||||
(= klassisches Geoblocking, alle Ports/Protokolle). Mit 'ports': eine
|
(= klassisches Geoblocking, alle Ports/Protokolle).
|
||||||
Variante pro konfiguriertem Protokoll (multiport braucht -p tcp ODER -p udp,
|
|
||||||
nicht beides gleichzeitig).
|
Mit 'ports' und match_original_dport=False (Standard): eine Variante pro
|
||||||
|
Protokoll, jeweils mit allen Ports als multiport --dports.
|
||||||
|
|
||||||
|
Mit 'ports' und match_original_dport=True: gematcht wird der URSPRUENGLICHE
|
||||||
|
Zielport (vor DNAT) per 'conntrack --ctorigdstport'. Das ist einwertig (kein
|
||||||
|
multiport), daher eine Variante pro (Protokoll, Port). Fuer Dienste hinter
|
||||||
|
einer Portweiterleitung/DNAT (z.B. Endian: public 1194/udp -> intern 6666),
|
||||||
|
wo im Filter der umgeschriebene Port ankommt -- so bleibt die Config auf dem
|
||||||
|
oeffentlichen Port und ist immun gegen den internen Remap.
|
||||||
"""
|
"""
|
||||||
if not cfg["ports"]:
|
if not cfg["ports"]:
|
||||||
return [{"proto": None, "dports": None}]
|
return [{"proto": None, "dports": None, "orig": False}]
|
||||||
|
if cfg.get("match_orig_dport"):
|
||||||
|
return [{"proto": proto, "dports": port, "orig": True}
|
||||||
|
for proto in cfg["protocols"] for port in cfg["ports"]]
|
||||||
dports = ",".join(cfg["ports"])
|
dports = ",".join(cfg["ports"])
|
||||||
return [{"proto": proto, "dports": dports} for proto in cfg["protocols"]]
|
return [{"proto": proto, "dports": dports, "orig": False} for proto in cfg["protocols"]]
|
||||||
|
|
||||||
|
|
||||||
def _variant_match_args(variant):
|
def _variant_match_args(variant):
|
||||||
@@ -224,15 +256,19 @@ def _variant_match_args(variant):
|
|||||||
|
|
||||||
|
|
||||||
def _variant_port_args(variant):
|
def _variant_port_args(variant):
|
||||||
if variant["dports"]:
|
if not variant["dports"]:
|
||||||
return ["-m", "multiport", "--dports", variant["dports"]]
|
|
||||||
return []
|
return []
|
||||||
|
if variant.get("orig"):
|
||||||
|
# conntrack matcht den ORIGINAL-Zielport (vor DNAT); einwertig, kein multiport.
|
||||||
|
return ["-m", "conntrack", "--ctorigdstport", variant["dports"]]
|
||||||
|
return ["-m", "multiport", "--dports", variant["dports"]]
|
||||||
|
|
||||||
|
|
||||||
def _variant_label(variant):
|
def _variant_label(variant):
|
||||||
if not variant["dports"]:
|
if not variant["dports"]:
|
||||||
return "alle Ports"
|
return "alle Ports"
|
||||||
return f"{variant['proto']}/{variant['dports']}"
|
base = f"{variant['proto']}/{variant['dports']}"
|
||||||
|
return base + " [orig-dport]" if variant.get("orig") else base
|
||||||
|
|
||||||
|
|
||||||
def _build_url_opener(proxy):
|
def _build_url_opener(proxy):
|
||||||
@@ -458,6 +494,12 @@ def apply_geoblock(cfg, dry_run=False):
|
|||||||
if not all_cidrs and not dry_run:
|
if not all_cidrs and not dry_run:
|
||||||
raise RuntimeError("Keine IP-Bereiche geladen — Abbruch, um kein leeres Set zu aktivieren")
|
raise RuntimeError("Keine IP-Bereiche geladen — Abbruch, um kein leeres Set zu aktivieren")
|
||||||
|
|
||||||
|
# 2b) eigene Block-IPs/CIDRs aus der .ini zusaetzlich aufnehmen (bereits in
|
||||||
|
# load_config validiert/normalisiert). Kommen ins selbe Set wie die Laender.
|
||||||
|
n_block_ips = len(cfg.get("block_ips", []))
|
||||||
|
if n_block_ips:
|
||||||
|
all_cidrs.extend(cfg["block_ips"])
|
||||||
|
|
||||||
# 3) Set atomar neu befuellen: temp-Set bauen, dann swap (kein Traffic-Loch)
|
# 3) Set atomar neu befuellen: temp-Set bauen, dann swap (kein Traffic-Loch)
|
||||||
tmp_name = f"{name}_tmp"
|
tmp_name = f"{name}_tmp"
|
||||||
run(["ipset", "create", tmp_name, "hash:net", "-exist"], dry_run=dry_run)
|
run(["ipset", "create", tmp_name, "hash:net", "-exist"], dry_run=dry_run)
|
||||||
@@ -468,8 +510,9 @@ def apply_geoblock(cfg, dry_run=False):
|
|||||||
run(["ipset", "swap", tmp_name, name], dry_run=dry_run)
|
run(["ipset", "swap", tmp_name, name], dry_run=dry_run)
|
||||||
run(["ipset", "destroy", tmp_name], dry_run=dry_run, check=False)
|
run(["ipset", "destroy", tmp_name], dry_run=dry_run, check=False)
|
||||||
|
|
||||||
log(f"ipset '{name}' befuellt mit {len(all_cidrs)} Bereichen "
|
extra = f" + {n_block_ips} eigene(n) Block-IP(s)/CIDR(s)" if n_block_ips else ""
|
||||||
f"aus Laendern: {', '.join(c.upper() for c in cfg['countries'])}")
|
log(f"ipset '{name}' befuellt mit {len(all_cidrs)} Eintraegen "
|
||||||
|
f"(Laender: {', '.join(c.upper() for c in cfg['countries'])}{extra})")
|
||||||
|
|
||||||
# 4) Vor dem Neu-Setzen ALLE bestehenden geoblock-Regeln der Chain entfernen
|
# 4) Vor dem Neu-Setzen ALLE bestehenden geoblock-Regeln der Chain entfernen
|
||||||
# (auch verwaiste aus frueheren Laeufen mit anderer ini, z.B. andere Ports).
|
# (auch verwaiste aus frueheren Laeufen mit anderer ini, z.B. andere Ports).
|
||||||
@@ -523,8 +566,13 @@ def show_status(cfg):
|
|||||||
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'])}")
|
||||||
|
if cfg.get("block_ips"):
|
||||||
|
print(f"Eigene Block-IPs/CIDRs ({len(cfg['block_ips'])}): {', '.join(cfg['block_ips'])}")
|
||||||
if cfg["ports"]:
|
if cfg["ports"]:
|
||||||
print(f"Port-Einschraenkung: {', '.join(cfg['ports'])} ({'/'.join(cfg['protocols'])})")
|
modus = "Original-Zielport vor DNAT (conntrack)" if cfg.get("match_orig_dport") \
|
||||||
|
else "Zielport (dports)"
|
||||||
|
print(f"Port-Einschraenkung: {', '.join(cfg['ports'])} ({'/'.join(cfg['protocols'])}) "
|
||||||
|
f"-- gematcht: {modus}")
|
||||||
else:
|
else:
|
||||||
print("Port-Einschraenkung: keine (alle Ports/Protokolle werden geblockt)")
|
print("Port-Einschraenkung: keine (alle Ports/Protokolle werden geblockt)")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user