fix(get_config): socket-level timeout for SSH fallback — covers KEX phase

paramiko banner_timeout/auth_timeout do not cover kex negotiation.
Using raw socket with settimeout(8) ensures the entire SSH handshake
is bounded, preventing the poll from hanging and timing out.
This commit is contained in:
Christian Manivong
2026-06-29 14:28:21 +02:00
parent 831727f2af
commit f7eff4b3ca
+28 -31
View File
@@ -592,53 +592,50 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
Used as fallback when the REST API does not expose the config endpoint Used as fallback when the REST API does not expose the config endpoint
(e.g. HP 2530 / YA firmware with Mocana SSH). (e.g. HP 2530 / YA firmware with Mocana SSH).
Uses paramiko directly (not netmiko) to: Uses a raw socket with settimeout() so the timeout covers ALL phases of
- Explicitly enable diffie-hellman-group1-sha1 (required by Mocana SSH 6.3) the SSH handshake (TCP connect, banner, KEX, auth) — not just TCP connect.
- Keep total blocking time ≤ 10 seconds This is the only reliable way to prevent a hanging KEX from blocking
indefinitely inside the poll task.
""" """
import socket as _socket
import paramiko import paramiko
_FALLBACK_TIMEOUT = 8 _FALLBACK_TIMEOUT = 8 # seconds — covers the entire SSH session
# KEX list that includes the old group1-sha1 required by Mocana SSH 6.3
_LEGACY_KEX = [ _LEGACY_KEX = [
"diffie-hellman-group1-sha1", "diffie-hellman-group1-sha1",
"diffie-hellman-group14-sha1", "diffie-hellman-group14-sha1",
"diffie-hellman-group14-sha256", "diffie-hellman-group14-sha256",
"diffie-hellman-group-exchange-sha256",
] ]
_LEGACY_DISABLED = {
"pubkeys": ["rsa-sha2-256", "rsa-sha2-512"],
}
try: try:
client = paramiko.SSHClient() sock = _socket.create_connection(
client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) (self.hostname, self.port or 22),
timeout=_FALLBACK_TIMEOUT,
)
sock.settimeout(_FALLBACK_TIMEOUT) # applies to all recv/send including KEX
# Patch the preferred kex list on the class before connecting so transport = paramiko.Transport(sock)
# paramiko will negotiate group1-sha1 with the old Mocana SSH server. transport.get_security_options().kex = _LEGACY_KEX
orig_kex = paramiko.Transport._preferred_kex transport.connect(
try:
paramiko.Transport._preferred_kex = tuple(_LEGACY_KEX)
client.connect(
hostname=self.hostname,
port=self.port or 22,
username=self.username, username=self.username,
password=self.password, password=self.password,
timeout=_FALLBACK_TIMEOUT,
banner_timeout=_FALLBACK_TIMEOUT,
auth_timeout=_FALLBACK_TIMEOUT,
look_for_keys=False,
allow_agent=False,
disabled_algorithms=_LEGACY_DISABLED,
) )
finally:
paramiko.Transport._preferred_kex = orig_kex
_, stdout, _ = client.exec_command("show running-config", timeout=_FALLBACK_TIMEOUT) channel = transport.open_session()
config = stdout.read().decode("utf-8", errors="replace") channel.settimeout(_FALLBACK_TIMEOUT)
client.close() channel.exec_command("show running-config")
return config
chunks: list[bytes] = []
while True:
data = channel.recv(4096)
if not data:
break
chunks.append(data)
transport.close()
return b"".join(chunks).decode("utf-8", errors="replace")
except Exception as exc: except Exception as exc:
logger.warning("SSH config fallback failed for %s: %s", self.hostname, exc) logger.warning("SSH config fallback failed for %s: %s", self.hostname, exc)
return "" return ""