Testscript check_openvpn.py: prueft OpenVPN-Server-Erreichbarkeit (UDP-Handshake-Probe / TCP-Connect)
This commit is contained in:
@@ -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.py` | Kleiner Test-Webserver, um die Regeln von aussen zu pruefen |
|
||||||
| `geoblock-testserver.service` | systemd-Unit, startet den Test-Webserver dauerhaft |
|
| `geoblock-testserver.service` | systemd-Unit, startet den Test-Webserver dauerhaft |
|
||||||
| `uninstall_geoblock.sh` | Deinstaller: raeumt Timer/Service/Testserver/Regeln wieder ab (siehe unten) |
|
| `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
|
## 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
|
Betroffen ist **nur** der Abruf der Zonefiles beim Ausfuehren von
|
||||||
`--apply`/`--update`. iptables/ipset selbst und der Testserver brauchen dafuer
|
`--apply`/`--update`. iptables/ipset selbst und der Testserver brauchen dafuer
|
||||||
keinen Proxy — die filtern nur lokal am Kernel-Paketfilter.
|
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.
|
||||||
|
|||||||
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