Files
geoblocking-python-script/README.md
T

432 lines
19 KiB
Markdown

# Geoblocking
Blockt Verbindungen aus bestimmten Laendern auf Kernel-Ebene (iptables +
ipset). Die IP-Bereiche je Land kommen kostenlos (kein API-Key) von
[ipdeny.com](https://www.ipdeny.com/ipblocks/).
Kein Live-GeoIP-Lookup pro Verbindung (langsam, meist kostenpflichtig) —
stattdessen der Standardansatz von fail2ban/CSF: die komplette IP-Range-
Liste jedes Landes liegt im Kernel (`ipset`), `iptables` matched dagegen
und droppt Pakete bereits auf Netzwerk-Ebene.
## Dateien
| Datei | Zweck |
|---|---|
| `geoblock.py` | Hauptscript: laedt Laender-IPs, setzt/entfernt ipset+iptables-Regeln |
| `geoblock.ini.example` | Beispiel-Konfiguration (Vorlage im Repo, siehe unten) |
| `geoblock.ini` | Eure eigene, lokale Konfiguration (Laenderliste, Chain, Logging, ...) — **nicht** im Repo/Git, siehe unten |
| `geoblock.service` | systemd-Unit, fuehrt `--apply` aus |
| `geoblock.timer` | systemd-Timer: beim Booten + einmal taeglich |
| `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
Auf dem zu schuetzenden Host (root, `iptables`+`ipset`+`python3` installiert):
```bash
git clone https://git.hacker-net.de/Aria-Software/geoblocking-python-script.git
cd geoblocking-python-script
sudo ./install_geoblock.sh
```
Das:
- installiert `geoblock.py` + `geoblock.ini.example` nach `/opt/geoblock/`
und erzeugt daraus, falls noch keine vorhanden ist, `geoblock.ini`
(bestehende `geoblock.ini` wird NIE ueberschrieben)
- legt `/var/log/geoblock/` an
- installiert + aktiviert `geoblock.timer` (`systemctl enable --now`)
**Wichtig:** `geoblock.ini` ist eure lokale, echte Konfiguration (Laender,
Ports, ggf. Proxy-Zugangsdaten) — die Datei steht deshalb in `.gitignore`
und wird NIE ins Repo committed. Vorlage/Referenz ist `geoblock.ini.example`
(die landet im Repo und wird bei jeder Installation frisch mitkopiert).
Wer das Script direkt aus dem Repo-Verzeichnis heraus nutzt (ohne
`install_geoblock.sh`), muss die `.ini` einmal selbst anlegen:
```bash
cp geoblock.ini.example geoblock.ini
```
**Der Test-Webserver wird standardmaessig NICHT installiert.** Wer ihn
braucht (siehe Abschnitt "Verbindungstest" unten), haengt die Option
`--with-testserver` an:
```bash
sudo ./install_geoblock.sh --with-testserver
# optional mit eigenem Zielverzeichnis:
sudo ./install_geoblock.sh --with-testserver /opt/geoblock
```
Nur dann wird `geoblock_testserver.py` kopiert und
`geoblock-testserver.service` installiert + gestartet (offener Port
8899 standardmaessig). Ohne die Option bleibt der Port zu — nachtraeglich
installieren geht jederzeit per erneutem Aufruf mit `--with-testserver`.
Danach `/opt/geoblock/geoblock.ini` anpassen (Laenderliste!) und
einmal manuell testen:
```bash
sudo systemctl start geoblock.service
journalctl -u geoblock.service -f
```
Der Timer laeuft danach automatisch: **~2 Minuten nach jedem Systemstart**
und **einmal taeglich** (mit bis zu 15min Zufallsversatz, damit nicht alle
Hosts gleichzeitig ipdeny.com anfragen). Verpasste Laeufe (Host war aus)
werden dank `Persistent=true` beim naechsten Boot nachgeholt.
## Deinstallation
`uninstall_geoblock.sh` macht die Installation wieder rueckgaengig.
Zwei Modi:
```bash
# Alles runter: Timer/Service, Testserver (falls installiert),
# iptables/ipset-Regeln, Installationsverzeichnis
sudo ./uninstall_geoblock.sh
# Nur den Test-Webserver entfernen, Geoblocking selbst bleibt aktiv
sudo ./uninstall_geoblock.sh --testserver-only
```
Weitere Optionen:
```bash
sudo ./uninstall_geoblock.sh --purge-logs # zusaetzlich /var/log/geoblock loeschen
sudo ./uninstall_geoblock.sh /opt/geoblock # abweichendes Zielverzeichnis (Default: /opt/geoblock)
```
Ohne `--purge-logs` bleiben die bisherigen Logs unter
`/var/log/geoblock` erhalten. Bei der Komplett-Deinstallation wird
VOR dem Loeschen automatisch `geoblock.py --remove` ausgefuehrt, damit
die iptables-Regel und das `ipset` sauber aus dem Kernel entfernt werden
(kein verwaistes DROP, keine offene Verbindung bleibt blockiert). Fehlen
Script/Config bereits, wird gewarnt und man sollte manuell pruefen:
```bash
iptables -L -n | grep -i geoblock
ipset list -n | grep -i geoblock
```
## Manuelle Nutzung (ohne systemd)
```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 --status # Zustand pruefen
sudo python3 geoblock.py --config geoblock.ini --remove # rueckgaengig
```
`--apply` ist idempotent (kein doppeltes Anlegen von Regeln) und tauscht
das ipset atomar per `swap`, damit waehrend der Aktualisierung kein Loch
im Blocking entsteht.
**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.
## Verbindungstest (Test-Webserver)
`geoblock_testserver.py` ist ein winziger HTTP-Server ohne
Abhaengigkeiten (nur Python-Stdlib). Er beantwortet jeden Request mit einer
kleinen Info-Seite (Client-IP, Zeitstempel, User-Agent) und loggt jeden
Request. Er weiss selbst NICHTS von Laendern/Blocklisten — der eigentliche
Test ist der Netzwerkeffekt: **geblockte** IPs erreichen ihn gar nicht erst
(iptables droppt vor dem TCP-Handshake), **erlaubte** IPs bekommen die Seite.
Wird nur installiert, wenn `install_geoblock.sh` mit der Option
`--with-testserver` aufgerufen wurde (Default: nicht installiert, siehe
Abschnitt "Installation" oben). Ist er installiert, laeuft er als
`geoblock-testserver.service` auf Port `8899` (einstellbar unter
`[testserver]` in `geoblock.ini`, siehe unten).
**Testablauf:**
```bash
# 1) Von einem ERLAUBTEN Netz/Land aus aufrufen -> sollte laden
curl http://<host>:8899/
# 2) Testweise den eigenen Laendercode (z.B. DE) in geoblock.ini
# eintragen und aktivieren:
sudo python3 geoblock.py --config geoblock.ini --apply
# 3) Jetzt von einem Client MIT einer IP aus diesem Land connecten
# (z.B. Handy im Mobilfunknetz) -> sollte jetzt TIMEOUT geben
curl --max-time 5 http://<host>:8899/
# 4) Land wieder aus der .ini entfernen + --apply -> Zugriff geht wieder
```
Manuell ohne systemd starten:
```bash
python3 geoblock_testserver.py --config geoblock.ini
# oder komplett ohne .ini:
python3 geoblock_testserver.py --port 8899 --bind 0.0.0.0 \
--log-file /var/log/geoblock/testserver.log
```
Requests landen (Zeit, Client-IP, Methode/Pfad, User-Agent) in der unter
`log_file` im `[testserver]`-Abschnitt konfigurierten Datei.
**Achtung Firewall/Portfreigabe:** der Testserver selbst oeffnet nur den
Port lokal — ob er von aussen erreichbar ist, haengt von eurer sonstigen
Firewall/Portweiterleitung ab (das ist bewusst getrennt von den
Geoblock-DROP-Regeln, die ja genau diesen Port betreffen sollen).
## Gezieltes Blocken einzelner Ports (Selbstaussperr-Schutz)
Per Default blockt `--apply` **alle** Ports/Protokolle fuer die gelisteten
Laender — das ist klassisches Geoblocking auf Host-Ebene. Das ist beim
Testen riskant: testet man testweise den eigenen Laendercode (siehe oben),
sperrt man sich damit auch von SSH & Co. aus, falls der Test-Client zufaellig
aus demselben Land connectet.
Ueber `ports` (und optional `protocol`) in der `.ini` laesst sich das
Blocking auf einzelne Anwendungen eingrenzen. Beispiel: nur den
Testserver-Port blocken, SSH bleibt fuer alle Laender offen:
```ini
[geoblock]
countries = de
...
ports = 8899
protocol = tcp
```
`--apply` legt dann statt einer allgemeinen DROP-Regel eine mit
`-p tcp -m multiport --dports 8899` an — matched nur Pakete zu Port 8899,
alles andere (inkl. Port 22) bleibt unberuehrt. Mehrere Ports/Bereiche
gehen kommaseparat (`80,443,8000:9000`), mehrere Protokolle ebenso
(`protocol = tcp,udp`, legt dann eine Regel pro Protokoll an).
`--status` zeigt an, ob gerade eine Port-Einschraenkung aktiv ist und
welche. `--remove` entfernt automatisch die zur aktuellen `.ini` passenden
Regel-Varianten — falls `ports`/`protocol` zwischen zwei Laeufen geaendert
wurden, lohnt sich vor dem naechsten `--apply` ein Blick mit
`iptables -L INPUT -n --line-numbers`, ob noch Altregeln mit der vorherigen
Variante stehen.
## Logging
In `geoblock.ini` unter `log_file` einen Pfad eintragen (Default:
`/var/log/geoblock/geoblock.log`, wird vom Installer angelegt).
Bei jedem Lauf wird eine Zeile angehaengt, u.a. eine maschinenlesbare
`RESULT status=OK/ERROR ...`-Zeile — genau die wertet das checkmk-Plugin
aus. Leer lassen = keine Datei-Logs (nur stderr/systemd-Journal).
## checkmk-Monitoring
`geoblock_checkmk` ist ein klassisches checkmk **local check**-Plugin
(kein extra Agent-Plugin-Verzeichnis noetig). Manuell einrichten:
1. Im Script-Kopf `LOG_FILE` auf den Pfad aus der `.ini` (`log_file`) anpassen.
2. Ausfuehrbar machen und kopieren:
```bash
chmod 755 geoblock_checkmk
cp geoblock_checkmk /usr/lib/check_mk_agent/local/
```
(Pfad haengt vom Setup ab — bei OMD-Sites z.B.
`~/local/lib/check_mk_agent/local/`.)
3. Naechster Agent-Abruf zeigt den Service **Geoblock**.
Status-Logik: `ERROR` im letzten Lauf → CRIT. Letzter erfolgreicher Lauf
> 36h alt → WARN, > 72h alt → CRIT (Timer laeuft taeglich, das faengt
einen einzelnen verpassten Lauf ab, ohne sofort zu alarmieren). Sonst OK,
inkl. Perfdata (Alter in Sekunden, Anzahl geblockter IP-Bereiche).
## Konfiguration (`geoblock.ini`)
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)
- `interface`: optional, nur ein Interface pruefen
- `log`: iptables-LOG-Eintrag vor dem DROP (dmesg/kern.log) an/aus
- `ipset_name`: Name des ipset-Sets
- `log_file`: Pfad zur script-eigenen Log-Datei (fuer checkmk)
- `ports`: optional, Komma-Liste von Ports/Bereichen (z.B. `80,443,8000:9000`).
Leer = alle Ports/Protokolle werden geblockt (klassisches Geoblocking).
Gesetzt = nur diese Ports werden fuer die gelisteten Laender geblockt, der
Rest des Hosts bleibt erreichbar. Siehe Abschnitt "Gezieltes Blocken
einzelner Ports" unten.
- `protocol`: nur relevant wenn `ports` gesetzt ist — `tcp` (Default), `udp`
oder `tcp,udp`
- `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
ueber einen Proxy raus kann. Leer lassen = direkte Verbindung (Standard).
Siehe Abschnitt "Proxy fuer den Zonefile-Abruf" unten.
Abschnitt `[testserver]` (fuer `geoblock_testserver.py`):
- `port`: TCP-Port des Test-Webservers (Default `8899`)
- `bind`: Bind-Adresse (`0.0.0.0` = alle IPv4-Interfaces)
- `log_file`: Pfad zur Request-Log-Datei des Testservers
## Betrieb hinter einem Reverse-Proxy (z.B. nginx)
Wichtig zu verstehen, weil es leicht zu falschen Testergebnissen fuehrt:
**Geoblocking mit iptables/ipset arbeitet auf Netzwerk-Ebene (TCP), nicht auf
HTTP-Ebene.** Es sieht immer nur die IP, die die TCP-Verbindung tatsaechlich
zum jeweiligen Host aufbaut — unabhaengig davon, was in HTTP-Headern steht.
**Wo die echte Client-IP ankommt, haengt vom Aufbau ab:**
- **nginx ist der einzige "Proxy" (direkt am Internet, kein CDN/WAF davor):**
Die TCP-Verbindung vom Client landet direkt am nginx-Host. Dort sieht sowohl
iptables als auch nginx selbst die echte oeffentliche Client-IP. In diesem
Setup **muessen die Geoblock-Regeln auf dem nginx-Host greifen**, und zwar
auf dem Port, auf dem nginx nach aussen lauscht (z.B. 80/443) — NICHT auf
dem internen Port des dahinterliegenden Testservers/Backends.
- **Davor haengt zusaetzlich ein CDN/WAF (Cloudflare, Fastly, ...):** Dann
kommt am nginx-Host nur noch die IP des CDN-Rechenzentrums an, nicht die
echte Client-IP. iptables auf dem nginx-Host wuerde dann effektiv das CDN
blocken, nicht den Endnutzer — Geoblocking muesste in diesem Fall beim CDN
selbst konfiguriert werden (die meisten bieten das nativ an).
- **Der Testserver haengt NUR intern hinter nginx** (z.B. `proxy_pass` auf
`127.0.0.1:8899`): Der Testserver selbst sieht dann nur die IP von nginx
(`127.0.0.1`), nicht die des echten Clients — die Verbindung des Clients
wurde ja schon vorher bei nginx "verbraucht" und neu aufgebaut. Ein
iptables-DROP auf Port 8899 wuerde in diesem Aufbau **gar nichts bewirken**,
weil der Traffic zum Testserver nie ueber das oeffentliche Netz laeuft,
sondern nur lokal von nginx zum Backend.
**Praktische Konsequenz fuer den Verbindungstest:**
Um den Testserver (`geoblock_testserver.py`, siehe oben) sinnvoll von
aussen zu testen, muss er entweder:
1. selbst direkt mit einer oeffentlichen IP/Port erreichbar sein (kein Proxy
dazwischen) — z.B. indem die Testmaschine testweise eine eigene
oeffentliche IP bekommt, oder
2. hinter nginx haengen, wobei dann **nginx** (nicht der Testserver) der
Punkt ist, an dem die Geoblock-Regeln greifen muessen.
Fuer einen sauberen End-to-End-Test der iptables-Regeln selbst (ohne Fragen
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.**
Dort ging es darum, wo die *eingehende* Client-Verbindung ankommt. Hier geht
es darum, wie `geoblock.py` selbst *ausgehend* die Zonefiles von ipdeny.com
laedt.
**Wann noetig:** Manche Hosts sind zwar aus dem Internet erreichbar (z.B.
ueber einen vorgelagerten Reverse-Proxy oder eine Firewall mit Portweiterleitung),
haben aber selbst **keine direkte ausgehende Internetverbindung** — nur einen
Weg ueber einen internen HTTP(S)-Proxy nach draussen. Ohne diesen Proxy kann
`geoblock.py` `https://www.ipdeny.com/...` nicht erreichen und bricht mit
einem URL-Fehler ab.
**Konfiguration** in `geoblock.ini`, Abschnitt `[geoblock]`:
```ini
[geoblock]
countries = de
proxy = http://10.0.0.5:3128
; mit Basic-Auth am Proxy:
; proxy = http://user:pass@10.0.0.5:3128
```
Der Proxy wird sowohl fuer HTTP- als auch HTTPS-Abrufe genutzt (ipdeny liefert
per HTTPS aus). Leer lassen (Standard) = das Script verbindet sich direkt,
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.