|
|
|
|
@ -1,5 +1,6 @@
|
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Stellt genau den TLS-Verbindungsaufbau nach, an dem guacd/FreeRDP scheitert.
|
|
|
|
|
"""Stellt genau den TLS-Verbindungsaufbau nach, an dem guacd/FreeRDP scheitert,
|
|
|
|
|
und ermittelt die MILDESTE Lockerung, die ihn gelingen laesst.
|
|
|
|
|
|
|
|
|
|
Hintergrund (Live-Test 3.9.):
|
|
|
|
|
|
|
|
|
|
@ -14,27 +15,28 @@ Dass es NICHT am Zertifikat liegt, zeigt das guacd-Journal selbst: guacd
|
|
|
|
|
protokolliert beim Pruefen eines Zertifikats immer eine der beiden Zeilen
|
|
|
|
|
"Certificate validation bypassed" (ignore-cert greift) oder "Certificate
|
|
|
|
|
validation failed". Steht KEINE von beiden im Journal, wurde der
|
|
|
|
|
Zertifikats-Rueckruf nie erreicht -- der Handshake ist also schon vorher
|
|
|
|
|
gescheitert, bei der Aushandlung von Protokollversion und Cipher. Die
|
|
|
|
|
Hosteinstellung "Zertifikat ignorieren" kann daran nichts aendern.
|
|
|
|
|
Zertifikats-Rueckruf nie erreicht -- der Handshake ist schon vorher
|
|
|
|
|
gescheitert, bei der Aushandlung von Protokollversion, Cipher-Suite und
|
|
|
|
|
Signaturverfahren. Die Hosteinstellung "Zertifikat ignorieren" kann daran
|
|
|
|
|
nichts aendern.
|
|
|
|
|
|
|
|
|
|
Genau das prueft dieses Skript. Es macht, was FreeRDP macht:
|
|
|
|
|
Was dieses Skript macht -- dasselbe wie FreeRDP:
|
|
|
|
|
|
|
|
|
|
1. TCP zu <ziel>:3389
|
|
|
|
|
2. X.224-Verbindungsanfrage mit RDP_NEG_REQ (MS-RDPBCGR 2.2.1.1) --
|
|
|
|
|
das ist die RDP-eigene Vorstufe, ohne die kein RDP-Server TLS spricht.
|
|
|
|
|
Deshalb funktioniert "openssl s_client -connect ziel:3389" hier auch
|
|
|
|
|
nicht: es faengt sofort mit ClientHello an und laeuft ins Leere.
|
|
|
|
|
3. Auswerten, welche Sicherheitsstufe der Server auswaehlt
|
|
|
|
|
4. TLS-Handshake auf derselben Verbindung -- und zwar MEHRFACH, mit
|
|
|
|
|
verschiedenen OpenSSL-Vorgaben:
|
|
|
|
|
a) System-Standard: exakt das, was guacd/FreeRDP auf diesem Host
|
|
|
|
|
bekommen. Debian 12 setzt in /etc/ssl/openssl.cnf
|
|
|
|
|
MinProtocol = TLSv1.2 und CipherString = DEFAULT@SECLEVEL=2.
|
|
|
|
|
b) Alles erlaubt (SECLEVEL=0, ab TLS 1.0)
|
|
|
|
|
c/d) TLS 1.0 bzw. TLS 1.2 einzeln erzwungen
|
|
|
|
|
Der Vergleich sagt, ob die OpenSSL-Vorgaben des JUMPHOSTS die Ursache
|
|
|
|
|
sind (a scheitert, b klappt) oder das Ziel selbst (alles scheitert).
|
|
|
|
|
2. X.224-Verbindungsanfrage mit RDP_NEG_REQ (MS-RDPBCGR 2.2.1.1). Das ist
|
|
|
|
|
die RDP-eigene Vorstufe, ohne die kein RDP-Server TLS spricht -- deshalb
|
|
|
|
|
taugt "openssl s_client -connect ziel:3389" hier NICHT als Gegentest:
|
|
|
|
|
es faengt sofort mit dem ClientHello an und laeuft ins Leere.
|
|
|
|
|
3. Auswerten, welche Sicherheitsstufe der Server waehlt.
|
|
|
|
|
4. TLS-Handshake auf derselben Verbindung, mehrfach, mit aufsteigend
|
|
|
|
|
staerkerer Lockerung der OpenSSL-Vorgaben -- von "unveraendert" bis
|
|
|
|
|
"alles erlaubt". Die Zertifikatspruefung ist ueberall AUS (wie
|
|
|
|
|
ignore-cert), damit kein Ergebnis daran haengen kann.
|
|
|
|
|
|
|
|
|
|
Die Reihenfolge ist Absicht: gemeldet wird die ERSTE (= mildeste) Variante,
|
|
|
|
|
die funktioniert, zusammen mit den passenden Ansible-Variablen. Auf einem
|
|
|
|
|
Jumphost pauschal auf SECLEVEL=0 zu gehen, wenn SECLEVEL=1 genuegt, waere
|
|
|
|
|
unnoetig.
|
|
|
|
|
|
|
|
|
|
Braucht nichts ausser Python 3 -- kein X11, kein xfreerdp, kein nmap.
|
|
|
|
|
Aufruf: python3 scripts/diagnose_rdp_tls.py 10.0.0.12
|
|
|
|
|
@ -46,6 +48,12 @@ import socket
|
|
|
|
|
import ssl
|
|
|
|
|
import struct
|
|
|
|
|
import sys
|
|
|
|
|
import warnings
|
|
|
|
|
|
|
|
|
|
# ssl.TLSVersion.TLSv1/TLSv1_1 sind in neueren Python-Fassungen als veraltet
|
|
|
|
|
# markiert. Hier werden sie absichtlich benutzt -- um festzustellen, OB das
|
|
|
|
|
# Ziel sie noch spricht. Die Warnungen wuerden die Ausgabe nur zumuellen.
|
|
|
|
|
warnings.filterwarnings("ignore", category=DeprecationWarning, module=__name__)
|
|
|
|
|
|
|
|
|
|
# MS-RDPBCGR 2.2.1.1.1 -- requestedProtocols
|
|
|
|
|
PROTOCOL_RDP = 0x00000000 # "Native RDP", ohne TLS
|
|
|
|
|
@ -72,6 +80,85 @@ _FAILURE_CODES = {
|
|
|
|
|
0x06: "SSL_WITH_USER_AUTH_REQUIRED_BY_SERVER",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_VERSIONS = {
|
|
|
|
|
"TLSv1": ssl.TLSVersion.TLSv1,
|
|
|
|
|
"TLSv1.1": ssl.TLSVersion.TLSv1_1,
|
|
|
|
|
"TLSv1.2": ssl.TLSVersion.TLSv1_2,
|
|
|
|
|
"TLSv1.3": ssl.TLSVersion.TLSv1_3,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Variante:
|
|
|
|
|
"""Eine zu testende OpenSSL-Einstellung.
|
|
|
|
|
|
|
|
|
|
`ansible` ist None, wenn die Variante keine Empfehlung darstellt
|
|
|
|
|
(System-Standard, oder reine Versionstests zur Information).
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, key, label, *, ciphers=None, min_version=None,
|
|
|
|
|
max_version=None, ansible=None, kandidat=False):
|
|
|
|
|
self.key = key
|
|
|
|
|
self.label = label
|
|
|
|
|
self.ciphers = ciphers
|
|
|
|
|
self.min_version = min_version
|
|
|
|
|
self.max_version = max_version
|
|
|
|
|
self.ansible = ansible
|
|
|
|
|
self.kandidat = kandidat # kommt als Empfehlung in Frage?
|
|
|
|
|
|
|
|
|
|
def context(self) -> ssl.SSLContext:
|
|
|
|
|
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
|
|
|
# Wie guacd mit ignore-cert: keinerlei Pruefung. Damit ist
|
|
|
|
|
# ausgeschlossen, dass ein Ergebnis an der Zertifikatspruefung haengt.
|
|
|
|
|
ctx.check_hostname = False
|
|
|
|
|
ctx.verify_mode = ssl.CERT_NONE
|
|
|
|
|
if self.ciphers:
|
|
|
|
|
ctx.set_ciphers(self.ciphers)
|
|
|
|
|
if self.min_version:
|
|
|
|
|
ctx.minimum_version = _VERSIONS[self.min_version]
|
|
|
|
|
if self.max_version:
|
|
|
|
|
ctx.maximum_version = _VERSIONS[self.max_version]
|
|
|
|
|
return ctx
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Reihenfolge = aufsteigende Staerke der Lockerung. Die erste funktionierende
|
|
|
|
|
# Variante mit kandidat=True ist die Empfehlung.
|
|
|
|
|
VARIANTEN = [
|
|
|
|
|
Variante("system", "System-Standard (genau das, was guacd/FreeRDP heute bekommt)"),
|
|
|
|
|
|
|
|
|
|
Variante("max12", "unveraendert streng, aber hoechstens TLS 1.2",
|
|
|
|
|
max_version="TLSv1.2", kandidat=True,
|
|
|
|
|
ansible={"guacd_tls_security_level": 2, "guacd_tls_min_protocol": "TLSv1.2",
|
|
|
|
|
"guacd_tls_max_protocol": "TLSv1.2"}),
|
|
|
|
|
|
|
|
|
|
Variante("sec1", "SECLEVEL=1 (SHA-1-Signaturen wieder erlaubt), bis TLS 1.3",
|
|
|
|
|
ciphers="DEFAULT@SECLEVEL=1", kandidat=True,
|
|
|
|
|
ansible={"guacd_tls_security_level": 1, "guacd_tls_min_protocol": "TLSv1.2",
|
|
|
|
|
"guacd_tls_max_protocol": ""}),
|
|
|
|
|
|
|
|
|
|
Variante("sec1_max12", "SECLEVEL=1 und hoechstens TLS 1.2",
|
|
|
|
|
ciphers="DEFAULT@SECLEVEL=1", max_version="TLSv1.2", kandidat=True,
|
|
|
|
|
ansible={"guacd_tls_security_level": 1, "guacd_tls_min_protocol": "TLSv1.2",
|
|
|
|
|
"guacd_tls_max_protocol": "TLSv1.2"}),
|
|
|
|
|
|
|
|
|
|
Variante("sec0_max12", "SECLEVEL=0, ab TLS 1.2, hoechstens TLS 1.2",
|
|
|
|
|
ciphers="DEFAULT@SECLEVEL=0", min_version="TLSv1.2", max_version="TLSv1.2",
|
|
|
|
|
kandidat=True,
|
|
|
|
|
ansible={"guacd_tls_security_level": 0, "guacd_tls_min_protocol": "TLSv1.2",
|
|
|
|
|
"guacd_tls_max_protocol": "TLSv1.2"}),
|
|
|
|
|
|
|
|
|
|
Variante("relaxed", "alles erlaubt: ALL@SECLEVEL=0, ab TLS 1.0",
|
|
|
|
|
ciphers="ALL@SECLEVEL=0", min_version="TLSv1", kandidat=True,
|
|
|
|
|
ansible={"guacd_tls_security_level": 0, "guacd_tls_min_protocol": "TLSv1",
|
|
|
|
|
"guacd_tls_max_protocol": ""}),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# Nur zur Information: welche Versionen spricht das Ziel ueberhaupt?
|
|
|
|
|
VERSIONSTESTS = [
|
|
|
|
|
Variante(f"nur {name}", f"nur {name}", ciphers="ALL@SECLEVEL=0",
|
|
|
|
|
min_version=name, max_version=name)
|
|
|
|
|
for name in ("TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3")
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _x224_connection_request(requested: int) -> bytes:
|
|
|
|
|
"""TPKT + X.224 Connection Request mit angehaengtem RDP_NEG_REQ."""
|
|
|
|
|
@ -81,14 +168,6 @@ def _x224_connection_request(requested: int) -> bytes:
|
|
|
|
|
return struct.pack(">BBH", 0x03, 0x00, 4 + len(x224)) + x224
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _read_tpkt(sock: socket.socket) -> bytes:
|
|
|
|
|
header = _recv_exact(sock, 4)
|
|
|
|
|
if header[0] != 0x03:
|
|
|
|
|
raise ValueError(f"Keine TPKT-Antwort (erstes Byte 0x{header[0]:02x}) -- spricht dort wirklich RDP?")
|
|
|
|
|
(length,) = struct.unpack(">H", header[2:4])
|
|
|
|
|
return header + _recv_exact(sock, length - 4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _recv_exact(sock: socket.socket, count: int) -> bytes:
|
|
|
|
|
buf = b""
|
|
|
|
|
while len(buf) < count:
|
|
|
|
|
@ -99,11 +178,18 @@ def _recv_exact(sock: socket.socket, count: int) -> bytes:
|
|
|
|
|
return buf
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _read_tpkt(sock: socket.socket) -> bytes:
|
|
|
|
|
header = _recv_exact(sock, 4)
|
|
|
|
|
if header[0] != 0x03:
|
|
|
|
|
raise ValueError(f"Keine TPKT-Antwort (erstes Byte 0x{header[0]:02x}) -- spricht dort wirklich RDP?")
|
|
|
|
|
(length,) = struct.unpack(">H", header[2:4])
|
|
|
|
|
return header + _recv_exact(sock, length - 4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def negotiate(sock: socket.socket, requested: int) -> int:
|
|
|
|
|
"""Fuehrt die X.224-Aushandlung durch, gibt das gewaehlte Protokoll zurueck."""
|
|
|
|
|
"""X.224-Aushandlung; gibt das vom Server gewaehlte Protokoll zurueck."""
|
|
|
|
|
sock.sendall(_x224_connection_request(requested))
|
|
|
|
|
response = _read_tpkt(sock)
|
|
|
|
|
body = response[4:]
|
|
|
|
|
body = _read_tpkt(sock)[4:]
|
|
|
|
|
if len(body) < 7:
|
|
|
|
|
raise ValueError("X.224-Antwort zu kurz")
|
|
|
|
|
payload = body[7:]
|
|
|
|
|
@ -113,56 +199,33 @@ def negotiate(sock: socket.socket, requested: int) -> int:
|
|
|
|
|
kind = payload[0]
|
|
|
|
|
if kind == 0x03:
|
|
|
|
|
(code,) = struct.unpack("<I", payload[4:8])
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"Der Server LEHNT die gewuenschte Sicherheitsstufe ab: "
|
|
|
|
|
+ _FAILURE_CODES.get(code, f"unbekannter failureCode 0x{code:02x}")
|
|
|
|
|
)
|
|
|
|
|
raise ValueError("Der Server LEHNT die gewuenschte Sicherheitsstufe ab: "
|
|
|
|
|
+ _FAILURE_CODES.get(code, f"unbekannter failureCode 0x{code:02x}"))
|
|
|
|
|
if kind != 0x02:
|
|
|
|
|
raise ValueError(f"Unerwartete Antwortart 0x{kind:02x} in der X.224-Antwort")
|
|
|
|
|
(selected,) = struct.unpack("<I", payload[4:8])
|
|
|
|
|
return selected
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _context(variant: str) -> ssl.SSLContext:
|
|
|
|
|
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
|
|
|
# Wie guacd mit ignore-cert: keinerlei Pruefung. Damit ist ausgeschlossen,
|
|
|
|
|
# dass ein Ergebnis hier an der Zertifikatspruefung haengt.
|
|
|
|
|
ctx.check_hostname = False
|
|
|
|
|
ctx.verify_mode = ssl.CERT_NONE
|
|
|
|
|
if variant == "system":
|
|
|
|
|
return ctx # erbt /etc/ssl/openssl.cnf -- genau wie guacd
|
|
|
|
|
if variant == "relaxed":
|
|
|
|
|
ctx.minimum_version = ssl.TLSVersion.TLSv1
|
|
|
|
|
ctx.set_ciphers("ALL:@SECLEVEL=0")
|
|
|
|
|
return ctx
|
|
|
|
|
if variant in ("tls1.0", "tls1.1", "tls1.2", "tls1.3"):
|
|
|
|
|
version = {
|
|
|
|
|
"tls1.0": ssl.TLSVersion.TLSv1,
|
|
|
|
|
"tls1.1": ssl.TLSVersion.TLSv1_1,
|
|
|
|
|
"tls1.2": ssl.TLSVersion.TLSv1_2,
|
|
|
|
|
"tls1.3": ssl.TLSVersion.TLSv1_3,
|
|
|
|
|
}[variant]
|
|
|
|
|
ctx.set_ciphers("ALL:@SECLEVEL=0")
|
|
|
|
|
ctx.minimum_version = version
|
|
|
|
|
ctx.maximum_version = version
|
|
|
|
|
return ctx
|
|
|
|
|
raise ValueError(variant)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def try_handshake(host: str, port: int, requested: int, variant: str, timeout: float):
|
|
|
|
|
"""Gibt (erfolg, text) zurueck."""
|
|
|
|
|
def try_handshake(host, port, requested, variante: Variante, timeout):
|
|
|
|
|
try:
|
|
|
|
|
with socket.create_connection((host, port), timeout=timeout) as sock:
|
|
|
|
|
selected = negotiate(sock, requested)
|
|
|
|
|
if selected == PROTOCOL_RDP and requested != PROTOCOL_RDP:
|
|
|
|
|
return (False, "Server waehlt 'Native RDP' -- er bietet auf dieser Stufe gar kein TLS an")
|
|
|
|
|
ctx = _context(variant)
|
|
|
|
|
with ctx.wrap_socket(sock, server_hostname=host) as tls:
|
|
|
|
|
return (False, "Server waehlt 'Native RDP' -- kein TLS auf dieser Stufe")
|
|
|
|
|
with variante.context().wrap_socket(sock, server_hostname=host) as tls:
|
|
|
|
|
cert = tls.getpeercert(binary_form=True)
|
|
|
|
|
info = f"{tls.version()}, {tls.cipher()[0]}, Zertifikat {len(cert)} Byte"
|
|
|
|
|
return (True, info)
|
|
|
|
|
return (True, f"{tls.version()}, {tls.cipher()[0]}, Zertifikat {len(cert)} Byte")
|
|
|
|
|
except ssl.SSLError as exc:
|
|
|
|
|
return (False, f"{type(exc).__name__}: {exc.reason if getattr(exc, 'reason', None) else exc}")
|
|
|
|
|
grund = getattr(exc, "reason", None) or exc
|
|
|
|
|
return (False, f"{type(exc).__name__}: {grund}")
|
|
|
|
|
except ConnectionResetError:
|
|
|
|
|
# Wichtig zu unterscheiden: KEIN lokaler OpenSSL-Abbruch, sondern die
|
|
|
|
|
# Gegenstelle legt auf. SCHANNEL (Windows) schickt ein TCP-RST statt
|
|
|
|
|
# einer sauberen handshake_failure-Warnung, wenn im ClientHello nichts
|
|
|
|
|
# Verwendbares steht -- also ein Aushandlungsproblem, kein Netzfehler.
|
|
|
|
|
return (False, "Connection reset by peer -- das Ziel hat aufgelegt "
|
|
|
|
|
"(nichts Passendes im ClientHello gefunden)")
|
|
|
|
|
except (OSError, ValueError) as exc:
|
|
|
|
|
return (False, f"{type(exc).__name__}: {exc}")
|
|
|
|
|
|
|
|
|
|
@ -174,20 +237,16 @@ def main() -> int:
|
|
|
|
|
parser.add_argument("--timeout", type=float, default=10.0)
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
import ssl as _ssl
|
|
|
|
|
print(f"Jumphost: {_ssl.OPENSSL_VERSION}")
|
|
|
|
|
print(f"Jumphost: {ssl.OPENSSL_VERSION}")
|
|
|
|
|
try:
|
|
|
|
|
with open("/etc/ssl/openssl.cnf", encoding="utf-8") as fh:
|
|
|
|
|
relevant = [
|
|
|
|
|
line.strip() for line in fh
|
|
|
|
|
if line.strip().startswith(("MinProtocol", "CipherString", "MaxProtocol", "Options"))
|
|
|
|
|
]
|
|
|
|
|
relevant = [line.strip() for line in fh
|
|
|
|
|
if line.strip().startswith(("MinProtocol", "MaxProtocol", "CipherString", "Options"))]
|
|
|
|
|
print(" /etc/ssl/openssl.cnf: " + ("; ".join(relevant) if relevant else "keine Protokoll-/Cipher-Vorgaben"))
|
|
|
|
|
except OSError:
|
|
|
|
|
print(" /etc/ssl/openssl.cnf nicht lesbar")
|
|
|
|
|
print(f"Ziel: {args.host}:{args.port}\n")
|
|
|
|
|
|
|
|
|
|
# 1) Welche Sicherheitsstufe waehlt der Server, wenn man ihm alles anbietet?
|
|
|
|
|
print("== Schritt 1: X.224-Aushandlung ==")
|
|
|
|
|
requested = PROTOCOL_SSL | PROTOCOL_HYBRID
|
|
|
|
|
try:
|
|
|
|
|
@ -196,8 +255,8 @@ def main() -> int:
|
|
|
|
|
print(f" Angeboten: TLS + CredSSP/NLA -> Server waehlt: "
|
|
|
|
|
f"{_PROTOCOL_NAMES.get(selected, hex(selected))}")
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
print(f" FEHLGESCHLAGEN: {exc}")
|
|
|
|
|
print("\n Damit erübrigt sich der TLS-Test -- die RDP-Vorstufe scheitert bereits.")
|
|
|
|
|
print(f" FEHLGESCHLAGEN: {exc}\n")
|
|
|
|
|
print(" Damit eruebrigt sich der TLS-Test -- die RDP-Vorstufe scheitert bereits.")
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
if selected == PROTOCOL_RDP:
|
|
|
|
|
@ -206,43 +265,52 @@ def main() -> int:
|
|
|
|
|
print(" (GPO: 'Bestimmte Sicherheitsebene fuer RDP-Verbindungen anfordern').")
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
# 2) TLS-Handshake unter verschiedenen OpenSSL-Vorgaben
|
|
|
|
|
print("\n== Schritt 2: TLS-Handshake (Zertifikatspruefung ueberall AUS, wie ignore-cert) ==")
|
|
|
|
|
varianten = [
|
|
|
|
|
("system", "System-Standard (das, was guacd/FreeRDP bekommt)"),
|
|
|
|
|
("relaxed", "Alles erlaubt (SECLEVEL=0, ab TLS 1.0)"),
|
|
|
|
|
("tls1.0", "nur TLS 1.0"),
|
|
|
|
|
("tls1.1", "nur TLS 1.1"),
|
|
|
|
|
("tls1.2", "nur TLS 1.2"),
|
|
|
|
|
("tls1.3", "nur TLS 1.3"),
|
|
|
|
|
]
|
|
|
|
|
print("\n== Schritt 2: TLS-Handshake, aufsteigend gelockert ==")
|
|
|
|
|
print(" (Zertifikatspruefung ueberall AUS -- wie bei ignore-cert)\n")
|
|
|
|
|
ergebnisse = {}
|
|
|
|
|
for variant, label in varianten:
|
|
|
|
|
ok, text = try_handshake(args.host, args.port, requested, variant, args.timeout)
|
|
|
|
|
ergebnisse[variant] = ok
|
|
|
|
|
print(f" [{'OK ' if ok else 'FEHL'}] {label}\n {text}")
|
|
|
|
|
for v in VARIANTEN:
|
|
|
|
|
ok, text = try_handshake(args.host, args.port, requested, v, args.timeout)
|
|
|
|
|
ergebnisse[v.key] = ok
|
|
|
|
|
print(f" [{'OK ' if ok else 'FEHL'}] {v.label}\n {text}")
|
|
|
|
|
|
|
|
|
|
print("\n== Schritt 3: welche TLS-Versionen spricht das Ziel? ==")
|
|
|
|
|
gesprochen = []
|
|
|
|
|
for v in VERSIONSTESTS:
|
|
|
|
|
ok, text = try_handshake(args.host, args.port, requested, v, args.timeout)
|
|
|
|
|
if ok:
|
|
|
|
|
gesprochen.append(v.label.replace("nur ", ""))
|
|
|
|
|
print(f" [{'OK ' if ok else 'FEHL'}] {v.label}: {text}")
|
|
|
|
|
|
|
|
|
|
# 3) Auswertung
|
|
|
|
|
print("\n== Befund ==")
|
|
|
|
|
if ergebnisse["system"]:
|
|
|
|
|
print(" Der TLS-Handshake gelingt mit den System-Vorgaben. Die Ursache liegt")
|
|
|
|
|
print(" dann NICHT in der TLS-Schicht, sondern eine Stufe weiter (CredSSP/NLA,")
|
|
|
|
|
print(" Anmeldedaten) oder in guacd/FreeRDP selbst.")
|
|
|
|
|
print(" Naechster Schritt: guacd_log_level auf 'debug' und erneut verbinden.")
|
|
|
|
|
print(" Der TLS-Handshake gelingt bereits mit den System-Vorgaben.")
|
|
|
|
|
print(" Die Ursache liegt dann NICHT in der TLS-Schicht, sondern eine Stufe")
|
|
|
|
|
print(" weiter (CredSSP/NLA, Anmeldedaten) oder in guacd/FreeRDP selbst.")
|
|
|
|
|
print(" Naechster Schritt: guacd_log_level auf 'debug', erneut verbinden,")
|
|
|
|
|
print(" 'journalctl -u guacd -n 200' auswerten.")
|
|
|
|
|
return 0
|
|
|
|
|
if ergebnisse["relaxed"]:
|
|
|
|
|
moeglich = [v for v in ("tls1.0", "tls1.1", "tls1.2", "tls1.3") if ergebnisse[v]]
|
|
|
|
|
print(" EINDEUTIG: Der Handshake scheitert NUR an den OpenSSL-Vorgaben DIESES")
|
|
|
|
|
print(" Jumphosts, nicht am Ziel und nicht am Zertifikat.")
|
|
|
|
|
print(f" Das Ziel spricht: {', '.join(moeglich) if moeglich else '(nur mit SECLEVEL=0)'}")
|
|
|
|
|
print(" Debian 12 setzt systemweit MinProtocol=TLSv1.2 und DEFAULT@SECLEVEL=2;")
|
|
|
|
|
print(" guacd erbt das. Abhilfe: eine eigene OPENSSL_CONF NUR fuer die")
|
|
|
|
|
print(" guacd-Unit (nicht systemweit lockern).")
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
empfehlung = next((v for v in VARIANTEN if v.kandidat and ergebnisse.get(v.key)), None)
|
|
|
|
|
if empfehlung is None:
|
|
|
|
|
print(" Der Handshake scheitert in JEDER Variante -- die Ursache liegt am Ziel")
|
|
|
|
|
print(" (10.0.0.12) oder dazwischen, nicht an den OpenSSL-Vorgaben des Jumphosts.")
|
|
|
|
|
print(" Am Windows-Ziel pruefen: SCHANNEL-Registry (aktivierte TLS-Versionen),")
|
|
|
|
|
print(" Cipher-Suite-Richtlinie, und ob ein RDP-Zertifikat hinterlegt ist.")
|
|
|
|
|
print(f" ({args.host}) oder dazwischen, nicht an den OpenSSL-Vorgaben des Jumphosts.")
|
|
|
|
|
print(" Am Windows-Ziel pruefen: SCHANNEL-Registry (aktivierte TLS-Versionen und")
|
|
|
|
|
print(" Cipher-Suites), Gruppenrichtlinie 'SSL-Verschluesselungsverfahren', und ob")
|
|
|
|
|
print(" ueberhaupt ein RDP-Zertifikat hinterlegt ist.")
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
print(" Der Handshake scheitert an den OpenSSL-Vorgaben DIESES Jumphosts,")
|
|
|
|
|
print(" nicht am Ziel und nicht am Zertifikat.")
|
|
|
|
|
print(f" Das Ziel spricht: {', '.join(gesprochen) if gesprochen else '(unklar)'}")
|
|
|
|
|
print(f"\n MILDESTE ausreichende Lockerung: {empfehlung.label}")
|
|
|
|
|
print("\n In ansible/inventory/group_vars eintragen:")
|
|
|
|
|
print(" guacd_tls_legacy_compat: true")
|
|
|
|
|
for key, value in empfehlung.ansible.items():
|
|
|
|
|
gezeigt = '""' if value == "" else (f'"{value}"' if isinstance(value, str) else value)
|
|
|
|
|
print(f" {key}: {gezeigt}")
|
|
|
|
|
print("\n Das wirkt NUR auf den guacd-Prozess (Environment=OPENSSL_CONF in dessen")
|
|
|
|
|
print(" Unit), nicht systemweit. Die saubere Loesung bleibt, das Windows-Ziel")
|
|
|
|
|
print(" passend zu konfigurieren -- dies ist die Ueberbrueckung bis dahin.")
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|