#!/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()