Neue Option 'block_ips': komma-getrennte Liste eigener IPv4-IPs/CIDRs, die zusaetzlich zu den ipdeny-Laender-Ranges ins selbe ipset kommen (z.B. bekannte Angreifer, einzelne Hosts/Netze). Werden von denselben iptables-Regeln/Ports erfasst. - load_config validiert + normalisiert jede Angabe (ip_network strict=False, Host -> /32); IPv6 wird mit klarer Meldung abgelehnt (ipset ist IPv4). - apply_geoblock nimmt sie nach den Laender-CIDRs mit ins Set auf; Fuell-Log und --status weisen die Anzahl/Liste aus. - Doku: geoblock.ini.example (Kommentar + block_ips-Zeile) + README-Optionsliste. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
718 lines
34 KiB
Markdown
718 lines
34 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 |
|
|
| `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
|
|
|
|
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`.
|
|
|
|
**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:
|
|
|
|
```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 --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
|
|
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(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
|
|
- `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`
|
|
- `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
|
|
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.
|