Files
duffyduckandClaude Opus 4.8 830e9a61f6 geoblock: eigene Block-IPs/CIDRs zusaetzlich zu den Laendern (block_ips)
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>
2026-07-28 10:37:40 +02:00

34 KiB

Geoblocking

Blockt Verbindungen aus bestimmten Laendern auf Kernel-Ebene (iptables + ipset). Die IP-Bereiche je Land kommen kostenlos (kein API-Key) von ipdeny.com.

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):

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:

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:

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:

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:

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:

# 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:

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:

iptables -L -n | grep -i geoblock
ipset list -n | grep -i geoblock

Manuelle Nutzung (ohne systemd)

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:

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:

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:

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:

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:

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:

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:

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):

[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:

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 matchenports 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:

    [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:

# 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:

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:

[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:
    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]:

[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:

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.