From b5ff887ceaced431be6082a166440fb4a05c0c2e Mon Sep 17 00:00:00 2001 From: ARIA Date: Tue, 21 Jul 2026 12:36:27 +0000 Subject: [PATCH] Testscript check_openvpn.py: prueft OpenVPN-Server-Erreichbarkeit (UDP-Handshake-Probe / TCP-Connect) --- README.md | 44 ++++++++++++ check_openvpn.py | 181 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100755 check_openvpn.py diff --git a/README.md b/README.md index 14da6cb..b4abf6b 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ und droppt Pakete bereits auf Netzwerk-Ebene. | `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 @@ -385,3 +387,45 @@ ohne Proxy — das ist der Normalfall auf den meisten Servern. Betroffen ist **nur** der Abruf der Zonefiles beim Ausfuehren von `--apply`/`--update`. iptables/ipset selbst und der Testserver brauchen dafuer keinen Proxy — die filtern nur lokal am Kernel-Paketfilter. + +## Verbindungstest fuer OpenVPN-Server (`check_openvpn.py`) + +Eigenstaendiges Client-Tool (nur Python-Stdlib, keine Abhaengigkeiten), um +zu pruefen ob ein OpenVPN-Server unter einer Host/Port/Protokoll-Kombination +erreichbar ist — ohne dafuer eine echte VPN-Verbindung mit Zertifikaten +aufzubauen. Das Gegenstueck zu `geoblock_testserver.py`, wenn ihr das +Geoblocking (oder eine sonstige Firewall-Regel) gegen einen echten +OpenVPN-Server statt gegen den Test-Webserver pruefen wollt — z.B. genau +fuer den Fall "Geoblocking auf dem VPN-Server hinter dem NLB" von eben. + +**Wie es funktioniert:** +- **UDP** (Standard, meist Port `1194/udp`): schickt ein echtes, protokoll- + konformes OpenVPN-Handshake-Paket (`P_CONTROL_HARD_RESET_CLIENT_V2`, + der erste Schritt den auch ein echter Client macht). Antwortet der Server + mit seinem eigenen Hard-Reset-Paket, ist er zweifelsfrei erreichbar. +- **TCP** (falls der Server mit `proto tcp-server` laeuft): einfacher + TCP-Connect-Test. + +**Benutzung:** + +```bash +python3 check_openvpn.py --host vpn.example.com +# -> Standard: UDP, Port 1194, 3 Versuche, 3s Timeout je Versuch + +python3 check_openvpn.py --host vpn.example.com --port 443 --proto tcp + +python3 check_openvpn.py --host vpn.example.com --tries 5 --timeout 2 +``` + +Exit-Codes fuer Skripte/Monitoring: `0` = erreichbar, `1` = nicht erreichbar/ +Timeout, `2` = Aufruf-Fehler (z.B. Host nicht aufloesbar). + +**Wichtiger Hinweis zu `tls-auth`/`tls-crypt`:** Ist das auf dem Server +konfiguriert (haeufige, empfohlene Haertung), verwirft er JEDES Paket ohne +gueltiges HMAC/Verschluesselung bereits VOR jeder Antwort — komplett +stillschweigend, identisch zum Verhalten bei einer Firewall-Blockade. Ein +Timeout bei diesem Tool bedeutet also nicht zwingend "nicht erreichbar", +sondern kann auch "erreichbar, aber tls-auth/tls-crypt aktiv" heissen. Wer +das eindeutig unterscheiden will: testweise tls-auth/tls-crypt kurz +deaktivieren und erneut testen, oder direkt mit einem echten OpenVPN-Client +gegentesten. diff --git a/check_openvpn.py b/check_openvpn.py new file mode 100755 index 0000000..5d22463 --- /dev/null +++ b/check_openvpn.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +""" +check_openvpn.py — prueft ob ein OpenVPN-Server erreichbar ist + +ZWECK: + Eigenstaendiges Test-Tool (nur Python-Stdlib, keine Abhaengigkeiten), + um von einem Client aus zu pruefen, ob ein OpenVPN-Server unter einer + bestimmten Host/Port/Protokoll-Kombination erreichbar ist — ohne dafuer + eine echte VPN-Verbindung mit Zertifikaten aufzubauen. Nuetzlich als + Gegenstueck zu geoblock_testserver.py, wenn ihr das Geoblocking (oder + eine sonstige Firewall-Regel) gegen einen echten OpenVPN-Server statt + gegen den Test-Webserver pruefen wollt. + +WIE ES FUNKTIONIERT: + UDP (Standard-Modus von OpenVPN, meist Port 1194/udp): + Es wird ein echtes, protokollkonformes OpenVPN-Paket geschickt + (P_CONTROL_HARD_RESET_CLIENT_V2 — der allererste Handshake-Schritt, + den auch ein echter OpenVPN-Client schickt). Antwortet der Server + mit seinem eigenen Hard-Reset-Paket, ist er zweifelsfrei erreichbar + UND antwortet als OpenVPN. Kommt gar nichts zurueck, ist entweder + der Port/Host blockiert (Firewall/Geoblocking dropped) ODER der + Server hat `tls-auth`/`tls-crypt` aktiv (siehe Hinweis unten). + + TCP (falls der Server mit `proto tcp-server` laeuft): + Einfacher TCP-Connect-Test. Klappt der Connect, ist der Port offen + und erreichbar (OpenVPN-TCP-Server nehmen die Verbindung an und + warten dann auf den Handshake) — das reicht als Erreichbarkeits-Test. + +WICHTIGER HINWEIS zu tls-auth / tls-crypt: + Ist auf dem Server `tls-auth` oder `tls-crypt` konfiguriert (haeufige, + empfohlene Haertung), verwirft er JEDES Paket ohne gueltiges HMAC/ + Verschluesselung bereits VOR jeder Antwort — komplett stillschweigend, + identisch zum Verhalten bei einer Firewall-Blockade. Ein Timeout in + diesem Script bedeutet also NICHT zwingend "nicht erreichbar", sondern + kann auch "erreichbar, aber tls-auth/tls-crypt aktiv" heissen. Wer das + eindeutig unterscheiden will: testweise tls-auth/tls-crypt am Server + kurz deaktivieren, testen, wieder aktivieren — oder einfach einen + echten OpenVPN-Client mit der richtigen Config gegentesten. + +BENUTZUNG: + python3 check_openvpn.py --host vpn.example.com + -> Standard: UDP, Port 1194, 3 Versuche, 3s Timeout je Versuch + + python3 check_openvpn.py --host vpn.example.com --port 443 --proto tcp + -> TCP-Connect-Test auf Port 443 + + python3 check_openvpn.py --host vpn.example.com --tries 5 --timeout 2 + -> mehr Versuche, kuerzerer Timeout je Versuch (UDP kann Paketverlust + haben, deshalb mehrere Versuche sinnvoll) + +EXIT-CODES (fuer Skripte/Monitoring): + 0 = erreichbar (Antwort erhalten bzw. TCP-Connect erfolgreich) + 1 = nicht erreichbar / Timeout + 2 = Aufruf-Fehler (falscher Host, ungueltige Argumente, ...) +""" + +import argparse +import os +import socket +import struct +import sys +import time + + +def build_hard_reset_packet(): + """Baut ein echtes OpenVPN P_CONTROL_HARD_RESET_CLIENT_V2 Paket. + + Format (siehe OpenVPN-Protokoll): + 1 Byte : (opcode << 3) | key_id -- opcode 7 = HARD_RESET_CLIENT_V2 + 8 Bytes : Session-ID (frei waehlbar, hier zufaellig) + 1 Byte : Laenge des Packet-ID-Arrays fuer ACKs (hier 0, keine ACKs) + 4 Bytes : eigene Message-Packet-ID (big endian), hier 0 + """ + opcode = 7 # P_CONTROL_HARD_RESET_CLIENT_V2 + key_id = 0 + op_byte = bytes([(opcode << 3) | key_id]) + session_id = os.urandom(8) + ack_array_len = bytes([0]) + packet_id = struct.pack(">I", 0) + return op_byte + session_id + ack_array_len + packet_id + + +def check_udp(host, port, timeout, tries): + packet = build_hard_reset_packet() + last_err = None + for attempt in range(1, tries + 1): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(timeout) + try: + start = time.monotonic() + sock.sendto(packet, (host, port)) + data, _addr = sock.recvfrom(4096) + elapsed_ms = (time.monotonic() - start) * 1000 + looks_like_openvpn = bool(data) and (data[0] >> 3) in (7, 8, 9) + return True, elapsed_ms, looks_like_openvpn, attempt, None + except socket.timeout: + last_err = "Timeout" + except OSError as exc: + last_err = str(exc) + finally: + sock.close() + return False, None, False, tries, last_err + + +def check_tcp(host, port, timeout, tries): + last_err = None + for attempt in range(1, tries + 1): + try: + start = time.monotonic() + with socket.create_connection((host, port), timeout=timeout): + elapsed_ms = (time.monotonic() - start) * 1000 + return True, elapsed_ms, None, attempt, None + except socket.timeout: + last_err = "Timeout" + except OSError as exc: + last_err = str(exc) + return False, None, None, tries, last_err + + +def main(): + parser = argparse.ArgumentParser( + description="Prueft ob ein OpenVPN-Server erreichbar ist " + "(UDP-Handshake-Probe oder TCP-Connect-Test)." + ) + parser.add_argument("--host", required=True, help="Hostname oder IP des OpenVPN-Servers") + parser.add_argument("--port", type=int, default=1194, help="Port (Default 1194)") + parser.add_argument( + "--proto", choices=["udp", "tcp"], default="udp", + help="Protokoll wie beim OpenVPN-Server konfiguriert (Default udp)", + ) + parser.add_argument( + "--timeout", type=float, default=3.0, + help="Timeout je Versuch in Sekunden (Default 3.0)", + ) + parser.add_argument( + "--tries", type=int, default=3, + help="Anzahl Versuche bei Timeout, v.a. bei UDP sinnvoll da paketverlust-anfaellig (Default 3)", + ) + args = parser.parse_args() + + try: + socket.getaddrinfo(args.host, args.port) + except socket.gaierror as exc: + print(f"FEHLER: Host '{args.host}' konnte nicht aufgeloest werden: {exc}", file=sys.stderr) + sys.exit(2) + + print(f"Teste OpenVPN-Erreichbarkeit: {args.host}:{args.port}/{args.proto} " + f"(timeout={args.timeout}s, tries={args.tries}) ...") + + if args.proto == "udp": + ok, elapsed_ms, looks_like_openvpn, attempt, err = check_udp( + args.host, args.port, args.timeout, args.tries + ) + else: + ok, elapsed_ms, looks_like_openvpn, attempt, err = check_tcp( + args.host, args.port, args.timeout, args.tries + ) + + if ok: + latency = f"{elapsed_ms:.1f} ms" + if args.proto == "udp": + kind = "Antwort sieht wie gueltiges OpenVPN-Handshake-Paket aus" if looks_like_openvpn \ + else "Antwort erhalten, aber kein erkennbares OpenVPN-Opcode (evtl. anderer Dienst auf dem Port?)" + print(f"ERREICHBAR — {kind}. Antwortzeit: {latency} (Versuch {attempt}/{args.tries}).") + else: + print(f"ERREICHBAR — TCP-Connect erfolgreich. Verbindungszeit: {latency} " + f"(Versuch {attempt}/{args.tries}).") + sys.exit(0) + else: + print(f"NICHT ERREICHBAR — kein Handshake/Connect nach {args.tries} Versuch(en). " + f"Letzter Fehler: {err}") + if args.proto == "udp": + print("Hinweis: bei UDP kann das auch bedeuten, dass der Server per " + "tls-auth/tls-crypt konfiguriert ist und unbekannte Pakete " + "stillschweigend verwirft (siehe Docstring oben) — nicht zwingend " + "ein Erreichbarkeits-/Firewall-Problem.") + sys.exit(1) + + +if __name__ == "__main__": + main()