diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/README.md b/README.md index 4eb76a4..0f228d6 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,10 @@ und droppt Pakete bereits auf Netzwerk-Ebene. | `aria_geoblock.ini` | Konfiguration (Laenderliste, Chain, Logging, ...) | | `aria-geoblock.service` | systemd-Unit, fuehrt `--apply` aus | | `aria-geoblock.timer` | systemd-Timer: beim Booten + einmal taeglich | -| `install_aria_geoblock.sh` | Installer: kopiert alles nach `/opt/aria-geoblock`, richtet Timer ein | +| `install_aria_geoblock.sh` | Installer: kopiert alles nach `/opt/aria-geoblock`, richtet Timer + Testserver ein | | `aria_geoblock_checkmk` | checkmk local-check Plugin (manuell kopieren) | +| `aria_geoblock_testserver.py` | Kleiner Test-Webserver, um die Regeln von aussen zu pruefen | +| `aria-geoblock-testserver.service` | systemd-Unit, startet den Test-Webserver dauerhaft | ## Installation @@ -35,6 +37,8 @@ Das: (bestehende `.ini` wird NICHT ueberschrieben, neue landet als `.example`) - legt `/var/log/aria_geoblock/` an - installiert + aktiviert `aria-geoblock.timer` (`systemctl enable --now`) +- installiert + startet `aria-geoblock-testserver.service` (Test-Webserver, + siehe Abschnitt "Verbindungstest" unten) Danach `/opt/aria-geoblock/aria_geoblock.ini` anpassen (Laenderliste!) und einmal manuell testen: @@ -67,6 +71,53 @@ 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) + +`aria_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. + +Laeuft nach `install_aria_geoblock.sh` automatisch als +`aria-geoblock-testserver.service` auf Port `8899` (einstellbar unter +`[testserver]` in `aria_geoblock.ini`, siehe unten). + +**Testablauf:** + +```bash +# 1) Von einem ERLAUBTEN Netz/Land aus aufrufen -> sollte laden +curl http://:8899/ + +# 2) Testweise den eigenen Laendercode (z.B. DE) in aria_geoblock.ini +# eintragen und aktivieren: +sudo python3 aria_geoblock.py --config aria_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://:8899/ + +# 4) Land wieder aus der .ini entfernen + --apply -> Zugriff geht wieder +``` + +Manuell ohne systemd starten: + +```bash +python3 aria_geoblock_testserver.py --config aria_geoblock.ini +# oder komplett ohne .ini: +python3 aria_geoblock_testserver.py --port 8899 --bind 0.0.0.0 \ + --log-file /var/log/aria_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). + ## Logging In `aria_geoblock.ini` unter `log_file` einen Pfad eintragen (Default: @@ -105,3 +156,9 @@ Siehe Kommentare in der Datei selbst — kurz: - `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) + +Abschnitt `[testserver]` (fuer `aria_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 diff --git a/aria-geoblock-testserver.service b/aria-geoblock-testserver.service new file mode 100644 index 0000000..a6e5937 --- /dev/null +++ b/aria-geoblock-testserver.service @@ -0,0 +1,21 @@ +[Unit] +Description=ARIA Geoblocking - Test-Webserver fuer Verbindungstests von aussen +Documentation=https://git.hacker-net.de/Aria-Software/geoblocking-python-script +After=network-online.target +Wants=network-online.target +ConditionPathExists=__INSTALL_DIR__/aria_geoblock.ini + +[Service] +Type=simple +User=root +ExecStart=/usr/bin/python3 __INSTALL_DIR__/aria_geoblock_testserver.py --config __INSTALL_DIR__/aria_geoblock.ini +Restart=on-failure +RestartSec=5 +# Laeuft dauerhaft (kein oneshot wie aria-geoblock.service) - beantwortet +# Requests solange der Host laeuft. Port/Bind/Log kommen aus [testserver] +# in der .ini. User=root nur wegen evtl. niedriger Portnummer/Log-Verzeichnis- +# Rechten - wer will kann hier einen unprivilegierten User eintragen, sofern +# Port >1024 ist und der User Schreibrechte auf das Log-Verzeichnis hat. + +[Install] +WantedBy=multi-user.target diff --git a/aria_geoblock_testserver.py b/aria_geoblock_testserver.py new file mode 100755 index 0000000..5b77479 --- /dev/null +++ b/aria_geoblock_testserver.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +""" +aria_geoblock_testserver.py — kleiner Test-Webserver fuer aria-geoblock + +ZWECK: + Ein minimaler HTTP-Server (nur Python-Stdlib, keine Abhaengigkeiten), + gegen den man von AUSSEN (Handy im Mobilfunknetz, VPN mit anderem Land, + Freund im Ausland, ...) connecten kann, um zu pruefen ob die Geoblocking- + Regeln (ipset/iptables aus aria_geoblock.py) wirklich greifen. + + Die Logik ist bewusst simpel: der Server selbst weiss NICHTS von Laendern + oder Blocklisten. Er beantwortet einfach jeden Request, der ihn erreicht, + mit einer kleinen HTML-Seite (Client-IP, Zeitstempel, User-Agent) und + loggt den Request in eine Datei. + + Der eigentliche TEST ist der Netzwerkeffekt: iptables droppt Pakete + geblockter Laender bereits VOR dem TCP-Handshake. Kommt eine Anfrage aus + einem geblockten Land, sieht dieser Server sie also gar nicht erst - + der Client bekommt stattdessen ein Timeout. Aus einem NICHT geblockten + Land/Netz laedt die Seite ganz normal. + + Testablauf (siehe auch README.md): + 1. Testserver starten (systemd-Service aria-geoblock-testserver, oder + manuell: siehe unten). + 2. Von einem Client aus einem ERLAUBTEN Land/Netz aufrufen: + curl http://:/ + -> sollte die Test-Seite liefern. + 3. Ziel-Land testweise in aria_geoblock.ini eintragen (z.B. den eigenen + Ländercode DE), aria_geoblock.py --apply ausfuehren, dann von einem + Client MIT dieser Landes-IP erneut connecten + -> sollte jetzt in ein Timeout laufen (kein TCP-Connect möglich). + 4. Land wieder aus der .ini entfernen, --apply, Zugriff geht wieder. + +BENUTZUNG: + python3 aria_geoblock_testserver.py --config aria_geoblock.ini + -> Port/Bind-Adresse/Log-Pfad kommen aus dem Abschnitt [testserver] + der .ini (siehe aria_geoblock.ini im gleichen Repo) + + python3 aria_geoblock_testserver.py --port 8899 --bind 0.0.0.0 \ + --log-file /var/log/aria_geoblock/testserver.log + -> alternativ komplett ohne .ini, per CLI-Argumenten + + CLI-Argumente ueberschreiben, falls angegeben, die Werte aus der .ini. + +LOGGING: + Jeder eingehende Request wird als eine Zeile geloggt (Zeitstempel, + Client-IP, Methode, Pfad, User-Agent, Statuscode). Ohne --log-file/ + ohne log_file in der .ini wird nur nach stderr (systemd-Journal) + geloggt. + +AUTOSTART: + Siehe aria-geoblock-testserver.service im gleichen Repo sowie + install_aria_geoblock.sh, das den Service optional mit einrichtet. +""" + +import argparse +import configparser +import datetime +import html +import ipaddress +import socket +import sys +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +DEFAULT_PORT = 8899 +DEFAULT_BIND = "0.0.0.0" + +# Wird in main() aus CLI/.ini gesetzt. None = keine Datei-Logs. +LOG_FILE = None + + +def _timestamp(): + return datetime.datetime.now().astimezone().isoformat(timespec="seconds") + + +def _write_log_file(line): + if not LOG_FILE: + return + try: + with open(LOG_FILE, "a", encoding="utf-8") as f: + f.write(f"{_timestamp()} {line}\n") + except OSError as e: + print(f"[geoblock-testserver] WARNUNG: konnte Log-Datei nicht schreiben " + f"({LOG_FILE}): {e}", file=sys.stderr) + + +def log(msg): + line = f"[geoblock-testserver] {msg}" + print(line, file=sys.stderr, flush=True) + _write_log_file(line) + + +def load_config(path): + """Liest [testserver] aus der aria_geoblock.ini. Fehlt der Abschnitt + komplett, werden die Defaults benutzt (die .ini ist fuer den Testserver + also optional, im Gegensatz zu aria_geoblock.py).""" + cfg = configparser.ConfigParser() + read_ok = cfg.read(path) + if not read_ok: + raise FileNotFoundError(f"Konnte .ini nicht lesen: {path}") + + if "testserver" not in cfg: + log("Abschnitt [testserver] nicht in der .ini gefunden - benutze Defaults " + f"(port={DEFAULT_PORT}, bind={DEFAULT_BIND}, kein log_file)") + return {"port": DEFAULT_PORT, "bind": DEFAULT_BIND, "log_file": ""} + + sec = cfg["testserver"] + return { + "port": sec.getint("port", fallback=DEFAULT_PORT), + "bind": sec.get("bind", fallback=DEFAULT_BIND).strip(), + "log_file": sec.get("log_file", fallback="").strip(), + } + + +def _client_ip(handler): + """Bevorzugt X-Forwarded-For (falls hinter einem Reverse-Proxy), sonst + die direkte Peer-IP der TCP-Verbindung.""" + xff = handler.headers.get("X-Forwarded-For") + if xff: + # erster Eintrag = urspruenglicher Client + candidate = xff.split(",")[0].strip() + try: + ipaddress.ip_address(candidate) + return candidate + except ValueError: + pass + return handler.client_address[0] + + +PAGE_TMPL = """ + + + +ARIA Geoblock — Verbindungstest + + + +
+

✓ Verbindung erfolgreich

+

Diese Anfrage kam beim Server an — die Quelle ist also NICHT geblockt.

+ + + + + + +
Client-IP{client_ip}
Zeitstempel{ts}
Methode / Pfad{method} {path}
User-Agent{ua}
Host-Header{host}
+

Kommt eine Anfrage aus einem in aria_geoblock.ini geblockten Land, + sieht dieser Server sie gar nicht erst — iptables droppt sie vorher. + Der Client bekommt dann ein Timeout statt dieser Seite.

+
+ + +""" + + +class TestHandler(BaseHTTPRequestHandler): + server_version = "ARIA-Geoblock-Testserver/1.0" + + def log_message(self, format, *args): + # Default-Logging von BaseHTTPRequestHandler unterdruecken, + # wir loggen selbst strukturiert in do_GET/do_HEAD. + pass + + def _handle(self, send_body): + client_ip = _client_ip(self) + ua = self.headers.get("User-Agent", "-") + host_hdr = self.headers.get("Host", "-") + log(f'request client_ip={client_ip} method={self.command} path={self.path} ' + f'host="{host_hdr}" ua="{ua}"') + + body = PAGE_TMPL.format( + client_ip=html.escape(client_ip), + ts=html.escape(_timestamp()), + method=html.escape(self.command), + path=html.escape(self.path), + ua=html.escape(ua), + host=html.escape(host_hdr), + ).encode("utf-8") + + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.send_header("Cache-Control", "no-store") + self.end_headers() + if send_body: + self.wfile.write(body) + + def do_GET(self): + self._handle(send_body=True) + + def do_HEAD(self): + self._handle(send_body=False) + + +class ThreadingHTTPServerV6Fallback(ThreadingHTTPServer): + """Erlaubt Bind auf IPv6-Adressen (z.B. '::'), faellt aber sauber + auf IPv4 zurueck wenn eine IPv4-Adresse konfiguriert ist.""" + def server_bind(self): + if ":" in self.server_address[0]: + self.address_family = socket.AF_INET6 + super().server_bind() + + +def main(): + global LOG_FILE + + parser = argparse.ArgumentParser( + description="Kleiner Test-Webserver um Geoblocking-Regeln von aussen zu pruefen") + parser.add_argument("--config", help="Pfad zur aria_geoblock.ini (Abschnitt [testserver])") + parser.add_argument("--port", type=int, help="TCP-Port (ueberschreibt .ini)") + parser.add_argument("--bind", help="Bind-Adresse (ueberschreibt .ini)") + parser.add_argument("--log-file", help="Pfad zur Request-Log-Datei (ueberschreibt .ini)") + args = parser.parse_args() + + if args.config: + cfg = load_config(args.config) + else: + cfg = {"port": DEFAULT_PORT, "bind": DEFAULT_BIND, "log_file": ""} + + port = args.port if args.port is not None else cfg["port"] + bind = args.bind if args.bind else cfg["bind"] + LOG_FILE = args.log_file if args.log_file else (cfg["log_file"] or None) + + log(f"Starte Test-Webserver auf {bind}:{port} " + f"(log_file={'aus' if not LOG_FILE else LOG_FILE})") + + httpd = ThreadingHTTPServerV6Fallback((bind, port), TestHandler) + try: + httpd.serve_forever() + except KeyboardInterrupt: + log("Beende (Strg+C).") + finally: + httpd.server_close() + + +if __name__ == "__main__": + main() diff --git a/install_aria_geoblock.sh b/install_aria_geoblock.sh index f368d96..2606368 100755 --- a/install_aria_geoblock.sh +++ b/install_aria_geoblock.sh @@ -9,11 +9,17 @@ # Nutzung (als root, im Verzeichnis mit allen Repo-Dateien): # sudo ./install_aria_geoblock.sh [ZIELVERZEICHNIS] # +# Installiert ausserdem optional einen kleinen Test-Webserver +# (aria_geoblock_testserver.py + aria-geoblock-testserver.service), gegen +# den man von aussen connecten kann um die Geoblocking-Regeln zu pruefen. +# # Benoetigt im selben Verzeichnis: # aria_geoblock.py # aria_geoblock.ini # aria-geoblock.service # aria-geoblock.timer +# aria_geoblock_testserver.py +# aria-geoblock-testserver.service # # Voraussetzungen auf dem Host: python3, iptables, ipset, systemd. @@ -28,7 +34,8 @@ SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" INSTALL_DIR="${1:-/opt/aria-geoblock}" LOG_DIR="/var/log/aria_geoblock" -for f in aria_geoblock.py aria_geoblock.ini aria-geoblock.service aria-geoblock.timer; do +for f in aria_geoblock.py aria_geoblock.ini aria-geoblock.service aria-geoblock.timer \ + aria_geoblock_testserver.py aria-geoblock-testserver.service; do if [[ ! -f "$SRC_DIR/$f" ]]; then echo "FEHLER: $f fehlt in $SRC_DIR - bitte komplettes Repo verwenden." >&2 exit 1 @@ -60,13 +67,20 @@ echo "==> Lege Log-Verzeichnis an: $LOG_DIR" mkdir -p "$LOG_DIR" chmod 750 "$LOG_DIR" +echo "==> Installiere Test-Webserver nach $INSTALL_DIR" +cp "$SRC_DIR/aria_geoblock_testserver.py" "$INSTALL_DIR/aria_geoblock_testserver.py" +chmod 755 "$INSTALL_DIR/aria_geoblock_testserver.py" + echo "==> Installiere systemd-Units" sed "s#__INSTALL_DIR__#$INSTALL_DIR#g" "$SRC_DIR/aria-geoblock.service" \ > /etc/systemd/system/aria-geoblock.service cp "$SRC_DIR/aria-geoblock.timer" /etc/systemd/system/aria-geoblock.timer +sed "s#__INSTALL_DIR__#$INSTALL_DIR#g" "$SRC_DIR/aria-geoblock-testserver.service" \ + > /etc/systemd/system/aria-geoblock-testserver.service systemctl daemon-reload systemctl enable --now aria-geoblock.timer +systemctl enable --now aria-geoblock-testserver.service echo "" echo "==> Fertig." @@ -75,4 +89,6 @@ echo " Jetzt sofort testen: systemctl start aria-geoblock.service && journal echo " Timer-Status: systemctl list-timers aria-geoblock.timer" echo " Naechste/letzte Läufe: systemctl status aria-geoblock.timer" echo " Log-Datei: $LOG_DIR/aria_geoblock.log" +echo " Test-Webserver: systemctl status aria-geoblock-testserver.service" +echo " Port/Bind/Log siehe [testserver] in $INSTALL_DIR/aria_geoblock.ini" echo " checkmk-Plugin: siehe aria_geoblock_checkmk im Repo (manuell kopieren, README beachten)"