connectiopn fix round 2 2

This commit is contained in:
2026-08-21 15:23:46 +02:00
parent 6b2c5ac8a2
commit 8c9af78672
4 changed files with 297 additions and 19 deletions

View File

@ -9,6 +9,7 @@ Pflicht: ohne gepinnten Fingerprint wird die Verbindung abgelehnt.
"""
from __future__ import annotations
import asyncio
import logging
import asyncssh
@ -380,6 +381,35 @@ def resolve_ssh_username(host: dict, credential_username: str | None) -> str:
return username
HOST_KEY_CONNECT_TIMEOUT = 10
async def _fetch_server_host_key(address: str, port: int) -> asyncssh.SSHKey | None:
"""Reiner SSH-Key-Exchange ohne Anmeldung, mit Zeitlimit.
ACHTUNG, das war der Bug hinter drei Fehlersuchen (Phase 12 + Nachtraege):
asyncssh.get_server_host_key() nimmt -- ANDERS als asyncssh.connect() --
KEIN **kwargs entgegen. Seine Parameterliste ist in asyncssh 2.18.0
abschliessend:
get_server_host_key(host='', port=(), *, tunnel=(), proxy_command=(),
family=(), flags=0, local_addr=(), sock=None,
client_version=(), kex_algs=(),
server_host_key_algs=(), config=(), options=None)
'connect_timeout' ist NICHT dabei (nur asyncssh.connect() reicht unbekannte
Argumente ueber **kwargs an SSHClientConnectionOptions weiter). Ein Aufruf
mit connect_timeout=... endete deshalb SOFORT -- vor jedem Socket, daher
"in unter einer Sekunde" -- in einem TypeError. Das Zeitlimit wird hier
darum von aussen per asyncio.wait_for() gesetzt; das ist zugleich
unabhaengig davon, wie einzelne asyncssh-Versionen ihre Optionen benennen.
"""
return await asyncio.wait_for(
asyncssh.get_server_host_key(address, port=port),
timeout=HOST_KEY_CONNECT_TIMEOUT,
)
async def _verified_host_key(
conn: aiosqlite.Connection, host: dict
) -> asyncssh.SSHKey:
@ -401,17 +431,25 @@ async def _verified_host_key(
raise HostKeyNotPinnedError(host["id"])
try:
observed_key = await asyncssh.get_server_host_key(
host["address"], port=host["port"], connect_timeout=10
)
observed_key = await _fetch_server_host_key(host["address"], host["port"])
except asyncio.TimeoutError as exc:
raise HostKeyDiscoveryError(
host["id"], f"Zeitlimit von {HOST_KEY_CONNECT_TIMEOUT}s ueberschritten"
) from exc
except TypeError:
# NICHT maskieren: ein TypeError aus diesem Aufruf ist kein
# Netzwerkproblem, sondern ein Aufruffehler im eigenen Code. Genau so
# wurde der connect_timeout-Bug oben mehrfach als "Ziel nicht
# erreichbar" fehlgedeutet, nachdem das except hier auf Exception
# verbreitert worden war.
raise
except Exception as exc:
# Bewusst dieselbe Verbreiterung wie in discover_and_store_host_key():
# (asyncssh.Error, OSError) allein fing nicht jede Art von Fehlschlag
# dieses externen Aufrufs ab (z.B. asyncio.TimeoutError vor Python
# 3.11, kein OSError). Ein hier unbehandelter Fehler wuerde ungefiltert
# bis in terminal_ws.py/sftp.py durchreichen -- SSH_SETUP_ERRORS
# erkennt HostKeyDiscoveryError dort explizit und zeigt eine saubere
# deutsche Meldung statt eines generischen Sitzungsabbruchs.
# Bewusst breit: (asyncssh.Error, OSError) allein fing nicht jede Art
# von Fehlschlag dieses externen Aufrufs ab. Ein hier unbehandelter
# Fehler wuerde ungefiltert bis in terminal_ws.py/sftp.py
# durchreichen -- SSH_SETUP_ERRORS erkennt HostKeyDiscoveryError dort
# explizit und zeigt eine saubere deutsche Meldung statt eines
# generischen Sitzungsabbruchs.
raise HostKeyDiscoveryError(host["id"], str(exc)) from exc
if observed_key is None:
raise HostKeyDiscoveryError(host["id"], "Das Ziel hat keinen Host-Key gesendet")
@ -554,9 +592,17 @@ async def discover_and_store_host_key(
raise HostNotConfiguredError("Host-Key-Ermittlung ist nur fuer SSH-Ziele moeglich")
try:
key = await asyncssh.get_server_host_key(
host["address"], port=host["port"], connect_timeout=10
)
key = await _fetch_server_host_key(host["address"], host["port"])
except asyncio.TimeoutError as exc:
logger.warning("Host-Key-Ermittlung fuer Host %s: Zeitlimit ueberschritten", host_id)
raise HostKeyDiscoveryError(
host_id, f"Zeitlimit von {HOST_KEY_CONNECT_TIMEOUT}s ueberschritten"
) from exc
except TypeError:
# Siehe _fetch_server_host_key(): Aufruffehler im eigenen Code duerfen
# NICHT als "Ziel nicht erreichbar" verkleidet werden.
logger.exception("Host-Key-Ermittlung fuer Host %s: fehlerhafter asyncssh-Aufruf", host_id)
raise
except Exception as exc:
logger.warning("Host-Key-Ermittlung fuer Host %s fehlgeschlagen: %s", host_id, exc)
raise HostKeyDiscoveryError(host_id, str(exc)) from exc