Testscript check_openvpn.py: prueft OpenVPN-Server-Erreichbarkeit (UDP-Handshake-Probe / TCP-Connect)
This commit is contained in:
Executable
+181
@@ -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()
|
||||
Reference in New Issue
Block a user