Testscript check_openvpn.py: prueft OpenVPN-Server-Erreichbarkeit (UDP-Handshake-Probe / TCP-Connect)

This commit is contained in:
ARIA
2026-07-21 12:36:27 +00:00
parent 1ed113a866
commit b5ff887cea
2 changed files with 225 additions and 0 deletions
+181
View File
@@ -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()