Compare commits
9
Commits
16b7eaf3bd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
830e9a61f6 | ||
|
|
161d90de45 | ||
|
|
af2a75c443 | ||
|
|
da83f351d7 | ||
|
|
867a8a1c6f | ||
|
|
b170759657 | ||
|
|
4fce0cd7f0 | ||
|
|
b5ff887cea | ||
|
|
1ed113a866 |
@@ -18,11 +18,15 @@ 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 |
|
||||
| `geoblock-testserver.service` | systemd-Unit, startet den Test-Webserver dauerhaft |
|
||||
| `uninstall_geoblock.sh` | Deinstaller: raeumt Timer/Service/Testserver/Regeln wieder ab (siehe unten) |
|
||||
| `check_openvpn.py` | Eigenstaendiges Client-Tool: prueft ob ein OpenVPN-Server erreichbar ist (siehe unten) |
|
||||
| `LAENDERCODES.md` | Referenzliste ISO-Laendercode -> ausgeschriebener Name fuer `countries` in `geoblock.ini` |
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -67,6 +71,27 @@ Nur dann wird `geoblock_testserver.py` kopiert und
|
||||
8899 standardmaessig). Ohne die Option bleibt der Port zu — nachtraeglich
|
||||
installieren geht jederzeit per erneutem Aufruf mit `--with-testserver`.
|
||||
|
||||
**Der Reassert-Watchdog wird standardmaessig ebenfalls NICHT installiert.** Er
|
||||
wird nur auf Hosts mit einer gemanagten Firewall gebraucht (Endian, Shorewall,
|
||||
firewalld, UFW, UTM-Appliances), die die iptables-Chain periodisch neu baut und
|
||||
dabei fremde Regeln nach unten schiebt (siehe Abschnitt "Regeln bleiben nicht
|
||||
oben" unten). Aktiviert per `--with-watchdog`:
|
||||
|
||||
```bash
|
||||
sudo ./install_geoblock.sh --with-watchdog
|
||||
# kombinierbar mit --with-testserver und Zielverzeichnis:
|
||||
sudo ./install_geoblock.sh --with-testserver --with-watchdog /opt/geoblock
|
||||
```
|
||||
|
||||
Nur dann werden `geoblock-reassert.service` + `geoblock-reassert.timer`
|
||||
installiert und aktiviert (prueft alle 2 Min die Regel-Position). Auch das
|
||||
laesst sich jederzeit per erneutem Aufruf mit `--with-watchdog` nachziehen.
|
||||
|
||||
> **Zusammengefasst:** `install_geoblock.sh` **ohne** Optionen installiert
|
||||
> **weder** Testserver **noch** Watchdog — nur das eigentliche Geoblocking
|
||||
> (`geoblock.py` + `geoblock.timer`, Apply beim Boot + taeglich). Testserver und
|
||||
> Watchdog sind jeweils opt-in ueber ihre Option.
|
||||
|
||||
Danach `/opt/geoblock/geoblock.ini` anpassen (Laenderliste!) und
|
||||
einmal manuell testen:
|
||||
|
||||
@@ -118,19 +143,269 @@ 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.
|
||||
|
||||
**Aufraeumen bei jedem Lauf:** `--apply` (und damit auch jeder Neustart von
|
||||
`geoblock.service`) entfernt zuerst **alle** bestehenden geoblock-Regeln und
|
||||
setzt sie danach frisch. Gescannt werden dabei die konfigurierten Chains **plus**
|
||||
die Standard-Filter-Chains INPUT/FORWARD/OUTPUT — es werden ausschliesslich
|
||||
Regeln geloescht, die auf das eigene `ipset` verweisen (fremde Regeln bleiben
|
||||
unberuehrt). Dadurch verschwinden automatisch auch Reste aus frueheren Laeufen
|
||||
mit **anderer Config**: geaenderte `ports`/`protocol` ebenso wie eine geaenderte
|
||||
`chain`. Konkret heisst das: Stellst du z.B. von `chain = INPUT` auf
|
||||
`chain = FORWARD` um und startest den Dienst neu, werden die alten INPUT-Regeln
|
||||
automatisch entfernt — du musst nicht von Hand aufraeumen. (Gleiches gilt fuer
|
||||
`--remove`, das ebenfalls diesen Purge nutzt.)
|
||||
|
||||
**Achtung:** iptables-Regeln sind nicht reboot-persistent. Wer das
|
||||
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** (Endian, Shorewall, firewalld, UFW, UTM-Appliances 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`.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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)
|
||||
|
||||
`geoblock_testserver.py` ist ein winziger HTTP-Server ohne
|
||||
@@ -246,7 +521,10 @@ inkl. Perfdata (Alter in Sekunden, Anzahl geblockter IP-Bereiche).
|
||||
Siehe Kommentare in der Datei selbst — kurz:
|
||||
|
||||
- `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
|
||||
- `log`: iptables-LOG-Eintrag vor dem DROP (dmesg/kern.log) an/aus
|
||||
- `ipset_name`: Name des ipset-Sets
|
||||
@@ -258,6 +536,16 @@ Siehe Kommentare in der Datei selbst — kurz:
|
||||
einzelner Ports" unten.
|
||||
- `protocol`: nur relevant wenn `ports` gesetzt ist — `tcp` (Default), `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
|
||||
(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
|
||||
@@ -314,6 +602,46 @@ zu Proxy-Ketten/CDN reinzumischen) ist Variante 1 — Testmaschine direkt mit
|
||||
eigener oeffentlicher IP, Geoblocking direkt auf dieser Maschine — der
|
||||
einfachste und eindeutigste Weg.
|
||||
|
||||
## Betrieb hinter einem AWS Load Balancer (ALB vs. NLB)
|
||||
|
||||
Gleiches Grundproblem wie beim Reverse-Proxy-Abschnitt oben, nur mit
|
||||
AWS-spezifischer Antwort — **ob die echte Client-IP am Server ankommt, haengt
|
||||
vom Load-Balancer-Typ ab:**
|
||||
|
||||
- **Application Load Balancer (ALB):** Arbeitet auf **Layer 7 (HTTP)**. Der
|
||||
ALB terminiert die TCP-Verbindung vom Client komplett und baut eine
|
||||
**neue** TCP-Verbindung zum Backend auf — mit der **IP des ALB-Nodes**
|
||||
(private VPC-IP) als Absender, nicht der des Clients. Die echte Client-IP
|
||||
steckt nur noch im HTTP-Header `X-Forwarded-For`. iptables/ipset sehen aber
|
||||
ausschliesslich die TCP-Ebene, also die ALB-Node-IP — **Geoblocking auf dem
|
||||
Backend bringt hinter einem ALB nichts**, es wuerde effektiv die eigene
|
||||
AWS-Infrastruktur "blocken" bzw. gar nichts filtern. Optionen in diesem
|
||||
Fall: AWS WAF mit Geo-Match-Regel direkt am ALB (die vorgesehene
|
||||
AWS-Loesung dafuer), oder auf NLB wechseln, falls auf L7-Features wie
|
||||
Pfad-Routing verzichtet werden kann.
|
||||
- **Network Load Balancer (NLB):** Arbeitet auf **Layer 4**, terminiert die
|
||||
TCP-Verbindung NICHT, sondern leitet sie durch. Die Quell-IP im TCP/IP-Paket,
|
||||
das am Server ankommt, ist die **echte Client-IP** — genau das, was
|
||||
iptables/ipset zur Filterung brauchen. `geoblock.py` funktioniert hinter
|
||||
einem NLB in der Standard-Konfiguration also **wie auf einem Server ohne
|
||||
Load Balancer davor**.
|
||||
- **Standardfall (funktioniert direkt):** Targets liegen im selben VPC und
|
||||
sind als **Instance-Target** registriert — die echte Client-IP kommt
|
||||
transparent im TCP-Paket an, nichts weiter zu konfigurieren.
|
||||
- **Vorbehalt:** Bei **IP-Targets** oder wenn der NLB ueber
|
||||
**PrivateLink**/Cross-VPC/Cross-Account angesprochen wird, bewahrt AWS
|
||||
die Client-IP nur, wenn am NLB **Proxy Protocol v2** aktiviert ist. Ist
|
||||
das der Fall, kommt die IP nicht mehr "nackt" im TCP-Paket an, sondern
|
||||
als PROXY-Header vor dem eigentlichen Payload — iptables sieht dann
|
||||
weiterhin nur die NLB-Node-IP, die echte IP muesste applikationsseitig
|
||||
aus dem PROXY-Header ausgewertet werden, nicht von iptables. In diesem
|
||||
Fall vorher pruefen, ob Proxy Protocol aktiv ist.
|
||||
|
||||
**Kurz zusammengefasst:** ALB → Geoblocking auf dem Backend funktioniert
|
||||
nicht (WAF am ALB nutzen). NLB mit Instance-Targets im selben VPC (Standard)
|
||||
→ `geoblock.py` funktioniert unveraendert. NLB mit IP-Targets/PrivateLink →
|
||||
vorher pruefen, ob Proxy Protocol v2 aktiv ist.
|
||||
|
||||
## Proxy fuer den Zonefile-Abruf (`proxy` in `geoblock.ini`)
|
||||
|
||||
**Wichtig — das ist ein ANDERES Thema als der Reverse-Proxy-Abschnitt oben.**
|
||||
@@ -345,3 +673,45 @@ ohne Proxy — das ist der Normalfall auf den meisten Servern.
|
||||
Betroffen ist **nur** der Abruf der Zonefiles beim Ausfuehren von
|
||||
`--apply`/`--update`. iptables/ipset selbst und der Testserver brauchen dafuer
|
||||
keinen Proxy — die filtern nur lokal am Kernel-Paketfilter.
|
||||
|
||||
## Verbindungstest fuer OpenVPN-Server (`check_openvpn.py`)
|
||||
|
||||
Eigenstaendiges Client-Tool (nur Python-Stdlib, keine Abhaengigkeiten), um
|
||||
zu pruefen ob ein OpenVPN-Server unter einer Host/Port/Protokoll-Kombination
|
||||
erreichbar ist — ohne dafuer eine echte VPN-Verbindung mit Zertifikaten
|
||||
aufzubauen. Das Gegenstueck zu `geoblock_testserver.py`, wenn ihr das
|
||||
Geoblocking (oder eine sonstige Firewall-Regel) gegen einen echten
|
||||
OpenVPN-Server statt gegen den Test-Webserver pruefen wollt — z.B. genau
|
||||
fuer den Fall "Geoblocking auf dem VPN-Server hinter dem NLB" von eben.
|
||||
|
||||
**Wie es funktioniert:**
|
||||
- **UDP** (Standard, meist Port `1194/udp`): schickt ein echtes, protokoll-
|
||||
konformes OpenVPN-Handshake-Paket (`P_CONTROL_HARD_RESET_CLIENT_V2`,
|
||||
der erste Schritt den auch ein echter Client macht). Antwortet der Server
|
||||
mit seinem eigenen Hard-Reset-Paket, ist er zweifelsfrei erreichbar.
|
||||
- **TCP** (falls der Server mit `proto tcp-server` laeuft): einfacher
|
||||
TCP-Connect-Test.
|
||||
|
||||
**Benutzung:**
|
||||
|
||||
```bash
|
||||
python3 check_openvpn.py --host vpn.example.com
|
||||
# -> Standard: UDP, Port 1194, 3 Versuche, 3s Timeout je Versuch
|
||||
|
||||
python3 check_openvpn.py --host vpn.example.com --port 443 --proto tcp
|
||||
|
||||
python3 check_openvpn.py --host vpn.example.com --tries 5 --timeout 2
|
||||
```
|
||||
|
||||
Exit-Codes fuer Skripte/Monitoring: `0` = erreichbar, `1` = nicht erreichbar/
|
||||
Timeout, `2` = Aufruf-Fehler (z.B. Host nicht aufloesbar).
|
||||
|
||||
**Wichtiger Hinweis zu `tls-auth`/`tls-crypt`:** Ist das auf dem Server
|
||||
konfiguriert (haeufige, empfohlene Haertung), verwirft er JEDES Paket ohne
|
||||
gueltiges HMAC/Verschluesselung bereits VOR jeder Antwort — komplett
|
||||
stillschweigend, identisch zum Verhalten bei einer Firewall-Blockade. Ein
|
||||
Timeout bei diesem Tool bedeutet also nicht zwingend "nicht erreichbar",
|
||||
sondern kann auch "erreichbar, aber tls-auth/tls-crypt aktiv" heissen. Wer
|
||||
das eindeutig unterscheiden will: testweise tls-auth/tls-crypt kurz
|
||||
deaktivieren und erneut testen, oder direkt mit einem echten OpenVPN-Client
|
||||
gegentesten.
|
||||
|
||||
Executable
+181
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
check_openvpn.py — prueft ob ein OpenVPN-Server erreichbar ist
|
||||
|
||||
ZWECK:
|
||||
Eigenstaendiges Test-Tool (nur Python-Stdlib, keine Abhaengigkeiten),
|
||||
um von einem Client aus zu pruefen, ob ein OpenVPN-Server unter einer
|
||||
bestimmten Host/Port/Protokoll-Kombination erreichbar ist — ohne dafuer
|
||||
eine echte VPN-Verbindung mit Zertifikaten aufzubauen. Nuetzlich als
|
||||
Gegenstueck zu geoblock_testserver.py, wenn ihr das Geoblocking (oder
|
||||
eine sonstige Firewall-Regel) gegen einen echten OpenVPN-Server statt
|
||||
gegen den Test-Webserver pruefen wollt.
|
||||
|
||||
WIE ES FUNKTIONIERT:
|
||||
UDP (Standard-Modus von OpenVPN, meist Port 1194/udp):
|
||||
Es wird ein echtes, protokollkonformes OpenVPN-Paket geschickt
|
||||
(P_CONTROL_HARD_RESET_CLIENT_V2 — der allererste Handshake-Schritt,
|
||||
den auch ein echter OpenVPN-Client schickt). Antwortet der Server
|
||||
mit seinem eigenen Hard-Reset-Paket, ist er zweifelsfrei erreichbar
|
||||
UND antwortet als OpenVPN. Kommt gar nichts zurueck, ist entweder
|
||||
der Port/Host blockiert (Firewall/Geoblocking dropped) ODER der
|
||||
Server hat `tls-auth`/`tls-crypt` aktiv (siehe Hinweis unten).
|
||||
|
||||
TCP (falls der Server mit `proto tcp-server` laeuft):
|
||||
Einfacher TCP-Connect-Test. Klappt der Connect, ist der Port offen
|
||||
und erreichbar (OpenVPN-TCP-Server nehmen die Verbindung an und
|
||||
warten dann auf den Handshake) — das reicht als Erreichbarkeits-Test.
|
||||
|
||||
WICHTIGER HINWEIS zu tls-auth / tls-crypt:
|
||||
Ist auf dem Server `tls-auth` oder `tls-crypt` konfiguriert (haeufige,
|
||||
empfohlene Haertung), verwirft er JEDES Paket ohne gueltiges HMAC/
|
||||
Verschluesselung bereits VOR jeder Antwort — komplett stillschweigend,
|
||||
identisch zum Verhalten bei einer Firewall-Blockade. Ein Timeout in
|
||||
diesem Script bedeutet also NICHT zwingend "nicht erreichbar", sondern
|
||||
kann auch "erreichbar, aber tls-auth/tls-crypt aktiv" heissen. Wer das
|
||||
eindeutig unterscheiden will: testweise tls-auth/tls-crypt am Server
|
||||
kurz deaktivieren, testen, wieder aktivieren — oder einfach einen
|
||||
echten OpenVPN-Client mit der richtigen Config gegentesten.
|
||||
|
||||
BENUTZUNG:
|
||||
python3 check_openvpn.py --host vpn.example.com
|
||||
-> Standard: UDP, Port 1194, 3 Versuche, 3s Timeout je Versuch
|
||||
|
||||
python3 check_openvpn.py --host vpn.example.com --port 443 --proto tcp
|
||||
-> TCP-Connect-Test auf Port 443
|
||||
|
||||
python3 check_openvpn.py --host vpn.example.com --tries 5 --timeout 2
|
||||
-> mehr Versuche, kuerzerer Timeout je Versuch (UDP kann Paketverlust
|
||||
haben, deshalb mehrere Versuche sinnvoll)
|
||||
|
||||
EXIT-CODES (fuer Skripte/Monitoring):
|
||||
0 = erreichbar (Antwort erhalten bzw. TCP-Connect erfolgreich)
|
||||
1 = nicht erreichbar / Timeout
|
||||
2 = Aufruf-Fehler (falscher Host, ungueltige Argumente, ...)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
def build_hard_reset_packet():
|
||||
"""Baut ein echtes OpenVPN P_CONTROL_HARD_RESET_CLIENT_V2 Paket.
|
||||
|
||||
Format (siehe OpenVPN-Protokoll):
|
||||
1 Byte : (opcode << 3) | key_id -- opcode 7 = HARD_RESET_CLIENT_V2
|
||||
8 Bytes : Session-ID (frei waehlbar, hier zufaellig)
|
||||
1 Byte : Laenge des Packet-ID-Arrays fuer ACKs (hier 0, keine ACKs)
|
||||
4 Bytes : eigene Message-Packet-ID (big endian), hier 0
|
||||
"""
|
||||
opcode = 7 # P_CONTROL_HARD_RESET_CLIENT_V2
|
||||
key_id = 0
|
||||
op_byte = bytes([(opcode << 3) | key_id])
|
||||
session_id = os.urandom(8)
|
||||
ack_array_len = bytes([0])
|
||||
packet_id = struct.pack(">I", 0)
|
||||
return op_byte + session_id + ack_array_len + packet_id
|
||||
|
||||
|
||||
def check_udp(host, port, timeout, tries):
|
||||
packet = build_hard_reset_packet()
|
||||
last_err = None
|
||||
for attempt in range(1, tries + 1):
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(timeout)
|
||||
try:
|
||||
start = time.monotonic()
|
||||
sock.sendto(packet, (host, port))
|
||||
data, _addr = sock.recvfrom(4096)
|
||||
elapsed_ms = (time.monotonic() - start) * 1000
|
||||
looks_like_openvpn = bool(data) and (data[0] >> 3) in (7, 8, 9)
|
||||
return True, elapsed_ms, looks_like_openvpn, attempt, None
|
||||
except socket.timeout:
|
||||
last_err = "Timeout"
|
||||
except OSError as exc:
|
||||
last_err = str(exc)
|
||||
finally:
|
||||
sock.close()
|
||||
return False, None, False, tries, last_err
|
||||
|
||||
|
||||
def check_tcp(host, port, timeout, tries):
|
||||
last_err = None
|
||||
for attempt in range(1, tries + 1):
|
||||
try:
|
||||
start = time.monotonic()
|
||||
with socket.create_connection((host, port), timeout=timeout):
|
||||
elapsed_ms = (time.monotonic() - start) * 1000
|
||||
return True, elapsed_ms, None, attempt, None
|
||||
except socket.timeout:
|
||||
last_err = "Timeout"
|
||||
except OSError as exc:
|
||||
last_err = str(exc)
|
||||
return False, None, None, tries, last_err
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Prueft ob ein OpenVPN-Server erreichbar ist "
|
||||
"(UDP-Handshake-Probe oder TCP-Connect-Test)."
|
||||
)
|
||||
parser.add_argument("--host", required=True, help="Hostname oder IP des OpenVPN-Servers")
|
||||
parser.add_argument("--port", type=int, default=1194, help="Port (Default 1194)")
|
||||
parser.add_argument(
|
||||
"--proto", choices=["udp", "tcp"], default="udp",
|
||||
help="Protokoll wie beim OpenVPN-Server konfiguriert (Default udp)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout", type=float, default=3.0,
|
||||
help="Timeout je Versuch in Sekunden (Default 3.0)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tries", type=int, default=3,
|
||||
help="Anzahl Versuche bei Timeout, v.a. bei UDP sinnvoll da paketverlust-anfaellig (Default 3)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
socket.getaddrinfo(args.host, args.port)
|
||||
except socket.gaierror as exc:
|
||||
print(f"FEHLER: Host '{args.host}' konnte nicht aufgeloest werden: {exc}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
print(f"Teste OpenVPN-Erreichbarkeit: {args.host}:{args.port}/{args.proto} "
|
||||
f"(timeout={args.timeout}s, tries={args.tries}) ...")
|
||||
|
||||
if args.proto == "udp":
|
||||
ok, elapsed_ms, looks_like_openvpn, attempt, err = check_udp(
|
||||
args.host, args.port, args.timeout, args.tries
|
||||
)
|
||||
else:
|
||||
ok, elapsed_ms, looks_like_openvpn, attempt, err = check_tcp(
|
||||
args.host, args.port, args.timeout, args.tries
|
||||
)
|
||||
|
||||
if ok:
|
||||
latency = f"{elapsed_ms:.1f} ms"
|
||||
if args.proto == "udp":
|
||||
kind = "Antwort sieht wie gueltiges OpenVPN-Handshake-Paket aus" if looks_like_openvpn \
|
||||
else "Antwort erhalten, aber kein erkennbares OpenVPN-Opcode (evtl. anderer Dienst auf dem Port?)"
|
||||
print(f"ERREICHBAR — {kind}. Antwortzeit: {latency} (Versuch {attempt}/{args.tries}).")
|
||||
else:
|
||||
print(f"ERREICHBAR — TCP-Connect erfolgreich. Verbindungszeit: {latency} "
|
||||
f"(Versuch {attempt}/{args.tries}).")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(f"NICHT ERREICHBAR — kein Handshake/Connect nach {args.tries} Versuch(en). "
|
||||
f"Letzter Fehler: {err}")
|
||||
if args.proto == "udp":
|
||||
print("Hinweis: bei UDP kann das auch bedeuten, dass der Server per "
|
||||
"tls-auth/tls-crypt konfiguriert ist und unbekannte Pakete "
|
||||
"stillschweigend verwirft (siehe Docstring oben) — nicht zwingend "
|
||||
"ein Erreichbarkeits-/Firewall-Problem.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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
|
||||
+25
-1
@@ -4,8 +4,13 @@
|
||||
; grossgeschrieben, egal), die geblockt werden sollen.
|
||||
; 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).
|
||||
; 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.
|
||||
; Leer lassen = alle Interfaces.
|
||||
@@ -41,6 +46,22 @@
|
||||
; oder "tcp,udp" fuer beides. Fuer die meisten Web-/App-Ports
|
||||
; 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
|
||||
; erreicht -- fuer Hosts, die zwar aus dem Internet erreichbar
|
||||
; sind (z.B. hinter Reverse-Proxy/Firewall), selbst aber KEINE
|
||||
@@ -62,6 +83,9 @@ log_file = /var/log/geoblock/geoblock.log
|
||||
; z.B. "8899" setzen (nur der Testserver wird geblockt, SSH bleibt offen).
|
||||
ports =
|
||||
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.
|
||||
proxy =
|
||||
|
||||
|
||||
+253
-67
@@ -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
|
||||
@@ -65,6 +69,7 @@ import argparse
|
||||
import configparser
|
||||
import datetime
|
||||
import ipaddress
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
@@ -73,6 +78,13 @@ import urllib.error
|
||||
|
||||
ZONE_URL_TMPL = "https://www.ipdeny.com/ipblocks/data/countries/{cc}.zone"
|
||||
|
||||
# Standard-Filter-Chains, die beim Aufraeumen IMMER mitgescannt werden -- auch
|
||||
# wenn sie gerade nicht in 'chain' konfiguriert sind. So werden verwaiste
|
||||
# geoblock-Regeln entfernt, wenn 'chain' zwischen zwei Laeufen geaendert wurde
|
||||
# (z.B. von INPUT auf FORWARD umgestellt). Rein additiv zum Scan: es werden nur
|
||||
# Regeln geloescht, die auf UNSER ipset verweisen -- fremde Regeln bleiben unberuehrt.
|
||||
STANDARD_FILTER_CHAINS = ("INPUT", "FORWARD", "OUTPUT")
|
||||
|
||||
# Wird in main() aus der .ini gesetzt (log_file). None = keine Datei-Logs.
|
||||
LOG_FILE = None
|
||||
|
||||
@@ -159,9 +171,35 @@ def load_config(path):
|
||||
if pr not in ("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"]
|
||||
|
||||
# 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 {
|
||||
"countries": countries,
|
||||
"chain": sec.get("chain", "INPUT").strip(),
|
||||
"chains": chains,
|
||||
"interface": sec.get("interface", "").strip(),
|
||||
"log": sec.getboolean("log", fallback=False),
|
||||
"log_prefix": sec.get("log_prefix", "GEOBLOCK-DROP:").strip(),
|
||||
@@ -169,6 +207,8 @@ def load_config(path):
|
||||
"log_file": sec.get("log_file", "").strip(),
|
||||
"ports": ports,
|
||||
"protocols": protocols,
|
||||
"match_orig_dport": match_orig_dport,
|
||||
"block_ips": block_ips,
|
||||
"proxy": sec.get("proxy", "").strip(),
|
||||
}
|
||||
|
||||
@@ -187,14 +227,25 @@ def build_rule_variants(cfg):
|
||||
"""Baut die Liste der (proto, dports)-Kombinationen fuer iptables-Regeln.
|
||||
|
||||
Ohne 'ports' in der .ini: eine Variante ohne Proto-/Port-Einschraenkung
|
||||
(= klassisches Geoblocking, alle Ports/Protokolle). Mit 'ports': eine
|
||||
Variante pro konfiguriertem Protokoll (multiport braucht -p tcp ODER -p udp,
|
||||
nicht beides gleichzeitig).
|
||||
(= klassisches Geoblocking, alle Ports/Protokolle).
|
||||
|
||||
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"]:
|
||||
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"])
|
||||
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):
|
||||
@@ -205,15 +256,19 @@ def _variant_match_args(variant):
|
||||
|
||||
|
||||
def _variant_port_args(variant):
|
||||
if variant["dports"]:
|
||||
return ["-m", "multiport", "--dports", variant["dports"]]
|
||||
if not variant["dports"]:
|
||||
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):
|
||||
if not variant["dports"]:
|
||||
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):
|
||||
@@ -286,6 +341,142 @@ def iptables_rule_exists(chain, ipset_name, interface, variant=None, dry_run=Fal
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def _list_chain_rules(chain):
|
||||
"""Gibt die aktuellen Regeln einer iptables-Chain als '-A ...'-Zeilen zurueck
|
||||
(Format von 'iptables -S <chain>', eine Zeile pro Regel)."""
|
||||
result = subprocess.run(["iptables", "-S", chain], capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
return []
|
||||
return [ln for ln in result.stdout.splitlines() if ln.startswith("-A ")]
|
||||
|
||||
|
||||
def purge_geoblock_rules(cfg, dry_run=False):
|
||||
"""Loescht in allen konfigurierten Chains PLUS den Standard-Filter-Chains
|
||||
(INPUT/FORWARD/OUTPUT) alle Regeln, die auf das geoblock-ipset verweisen
|
||||
(--match-set <name> src) -- unabhaengig von Proto/Port/Variante.
|
||||
|
||||
Dadurch werden auch alte Regeln aus frueheren --apply-Laeufen mit anderer
|
||||
ini sauber entfernt: andere Ports/Protokolle (kennt build_rule_variants(cfg)
|
||||
nicht mehr) UND andere Chains (z.B. nach Umstellung von INPUT auf FORWARD --
|
||||
die verwaisten INPUT-Regeln werden dank Standard-Chain-Scan trotzdem gefunden).
|
||||
Wir rekonstruieren das Loesch-Kommando direkt aus 'iptables -S' (per shlex,
|
||||
damit z.B. das --log-prefix mit Leerzeichen erhalten bleibt), statt die Args
|
||||
selbst nachzubauen -- so passt das -D immer exakt zur real existierenden Regel.
|
||||
"""
|
||||
name = cfg["ipset_name"]
|
||||
needle = f"--match-set {name} src"
|
||||
# Konfigurierte Chains PLUS die Standard-Filter-Chains scannen (dedupliziert,
|
||||
# Reihenfolge erhalten). Faengt auch verwaiste Regeln ab, wenn 'chain' seit dem
|
||||
# letzten Lauf geaendert wurde (z.B. INPUT -> FORWARD): die alten INPUT-Regeln
|
||||
# werden trotzdem gefunden und entfernt.
|
||||
scan_chains = list(dict.fromkeys(list(cfg["chains"]) + list(STANDARD_FILTER_CHAINS)))
|
||||
removed = 0
|
||||
for chain in scan_chains:
|
||||
n = 0
|
||||
for line in _list_chain_rules(chain):
|
||||
if needle not in line:
|
||||
continue
|
||||
tokens = shlex.split(line) # z.B. ["-A", "INPUT", "-m", "set", ...]
|
||||
# "-A <chain> <rest>" -> "iptables -D <chain> <rest>"
|
||||
run(["iptables", "-D"] + tokens[1:], dry_run=dry_run, check=False)
|
||||
n += 1
|
||||
if n:
|
||||
log(f"{n} bestehende geoblock-Regel(n) aus Chain '{chain}' entfernt (Set '{name}')")
|
||||
removed += n
|
||||
if not removed:
|
||||
log(f"Keine bestehenden geoblock-Regeln gefunden (gescannt: {', '.join(scan_chains)})")
|
||||
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 chain in cfg["chains"]:
|
||||
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", chain] + iface_opt + proto_args + set_match +
|
||||
port_args + ["-j", "DROP"], dry_run=dry_run)
|
||||
log(f"DROP-Regel am Anfang von Chain '{chain}' aktiv fuer Set '{name}' ({label})")
|
||||
|
||||
if cfg["log"]:
|
||||
run(["iptables", "-I", 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 '{chain}' aktiv ({label})")
|
||||
|
||||
|
||||
def _chain_rules_position(chain, ipset_name):
|
||||
"""(on_top, present) fuer eine EINZELNE Chain.
|
||||
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)
|
||||
"""
|
||||
needle = f"--match-set {ipset_name} src"
|
||||
rules = _list_chain_rules(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 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):
|
||||
"""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"]
|
||||
|
||||
@@ -303,6 +494,12 @@ def apply_geoblock(cfg, dry_run=False):
|
||||
if not all_cidrs and not dry_run:
|
||||
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)
|
||||
tmp_name = f"{name}_tmp"
|
||||
run(["ipset", "create", tmp_name, "hash:net", "-exist"], dry_run=dry_run)
|
||||
@@ -313,36 +510,20 @@ def apply_geoblock(cfg, dry_run=False):
|
||||
run(["ipset", "swap", tmp_name, name], dry_run=dry_run)
|
||||
run(["ipset", "destroy", tmp_name], dry_run=dry_run, check=False)
|
||||
|
||||
log(f"ipset '{name}' befuellt mit {len(all_cidrs)} Bereichen "
|
||||
f"aus Laendern: {', '.join(c.upper() for c in cfg['countries'])}")
|
||||
extra = f" + {n_block_ips} eigene(n) Block-IP(s)/CIDR(s)" if n_block_ips else ""
|
||||
log(f"ipset '{name}' befuellt mit {len(all_cidrs)} Eintraegen "
|
||||
f"(Laender: {', '.join(c.upper() for c in cfg['countries'])}{extra})")
|
||||
|
||||
# 4) optionale LOG-Regel(n) + DROP-Regel(n) in iptables, nur wenn noch nicht vorhanden.
|
||||
# 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)
|
||||
# 4) Vor dem Neu-Setzen ALLE bestehenden geoblock-Regeln der Chain entfernen
|
||||
# (auch verwaiste aus frueheren Laeufen mit anderer ini, z.B. andere Ports).
|
||||
# Erst JETZT (nach dem Befuellen des Sets), damit waehrend des ipdeny-Downloads
|
||||
# der bestehende Block aktiv bleibt und kein Traffic-Loch entsteht.
|
||||
purge_geoblock_rules(cfg, dry_run=dry_run)
|
||||
|
||||
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, "
|
||||
@@ -353,30 +534,10 @@ def apply_geoblock(cfg, dry_run=False):
|
||||
|
||||
def remove_geoblock(cfg, dry_run=False):
|
||||
name = cfg["ipset_name"]
|
||||
iface_opt = ["-i", cfg["interface"]] if cfg["interface"] else []
|
||||
set_match = ["-m", "set", "--match-set", name, "src"]
|
||||
# Entfernt fuer alle aktuell konfigurierten Varianten. Falls die .ini seit dem letzten
|
||||
# --apply geaendert wurde (z.B. ports entfernt/hinzugefuegt), koennen dadurch alte
|
||||
# Regeln mit anderer Variante stehen bleiben -- im Zweifel per 'iptables -L <chain> -n'
|
||||
# pruefen und manuell aufraeumen.
|
||||
for variant in build_rule_variants(cfg):
|
||||
proto_args = _variant_match_args(variant)
|
||||
port_args = _variant_port_args(variant)
|
||||
|
||||
for _ in range(5):
|
||||
result = run(["iptables", "-D", cfg["chain"]] + iface_opt + proto_args + set_match +
|
||||
port_args + ["-j", "DROP"], dry_run=dry_run, check=False)
|
||||
if dry_run or result is None or result.returncode != 0:
|
||||
break
|
||||
|
||||
if cfg["log"]:
|
||||
for _ in range(5):
|
||||
result = run(["iptables", "-D", cfg["chain"]] + iface_opt + proto_args + set_match +
|
||||
port_args + ["-j", "LOG", "--log-prefix", cfg["log_prefix"] + " "],
|
||||
dry_run=dry_run, check=False)
|
||||
if dry_run or result is None or result.returncode != 0:
|
||||
break
|
||||
|
||||
# Loescht ALLE geoblock-Regeln der Chain (auch verwaiste aus frueheren Laeufen
|
||||
# mit anderer ini), nicht nur die aktuell konfigurierten Varianten -- so bleiben
|
||||
# keine Reste stehen, egal wie sich 'ports'/'protocol' zwischenzeitlich geaendert haben.
|
||||
purge_geoblock_rules(cfg, dry_run=dry_run)
|
||||
run(["ipset", "destroy", name], dry_run=dry_run, check=False)
|
||||
log(f"Geoblock-Regeln entfernt, ipset '{name}' geloescht.")
|
||||
|
||||
@@ -390,12 +551,28 @@ def show_status(cfg):
|
||||
print(f"ipset '{name}' existiert nicht (noch nicht aktiv).")
|
||||
|
||||
variants = build_rule_variants(cfg)
|
||||
for chain in cfg["chains"]:
|
||||
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}")
|
||||
active = iptables_rule_exists(chain, name, cfg["interface"], variant=variant)
|
||||
print(f"iptables DROP-Regel in Chain '{chain}' ({_variant_label(variant)}) aktiv: {active}")
|
||||
|
||||
on_top, present = _chain_rules_position(chain, name)
|
||||
if not present:
|
||||
print(f"Regel-Position in Chain '{chain}': keine geoblock-Regeln vorhanden")
|
||||
elif on_top:
|
||||
print(f"Regel-Position in Chain '{chain}': am Anfang (ok)")
|
||||
else:
|
||||
print(f"Regel-Position in Chain '{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.get("block_ips"):
|
||||
print(f"Eigene Block-IPs/CIDRs ({len(cfg['block_ips'])}): {', '.join(cfg['block_ips'])}")
|
||||
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:
|
||||
print("Port-Einschraenkung: keine (alle Ports/Protokolle werden geblockt)")
|
||||
|
||||
@@ -407,12 +584,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)
|
||||
@@ -422,6 +604,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")
|
||||
|
||||
+36
-2
@@ -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"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
Reference in New Issue
Block a user