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
+31 -34
View File
@@ -592,53 +592,50 @@ class ProcurveDriver(ConfigLifecycleMixin, SwitchDriver):
Used as fallback when the REST API does not expose the config endpoint
(e.g. HP 2530 / YA firmware with Mocana SSH).
Uses paramiko directly (not netmiko) to:
- Explicitly enable diffie-hellman-group1-sha1 (required by Mocana SSH 6.3)
- Keep total blocking time ≤ 10 seconds
Uses a raw socket with settimeout() so the timeout covers ALL phases of
the SSH handshake (TCP connect, banner, KEX, auth) — not just TCP connect.
This is the only reliable way to prevent a hanging KEX from blocking
indefinitely inside the poll task.
"""
import socket as _socket
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 = [
"diffie-hellman-group1-sha1",
"diffie-hellman-group14-sha1",
"diffie-hellman-group14-sha256",
"diffie-hellman-group-exchange-sha256",
]
_LEGACY_DISABLED = {
"pubkeys": ["rsa-sha2-256", "rsa-sha2-512"],
}
try:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
sock = _socket.create_connection(
(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
# paramiko will negotiate group1-sha1 with the old Mocana SSH server.
orig_kex = paramiko.Transport._preferred_kex
try:
paramiko.Transport._preferred_kex = tuple(_LEGACY_KEX)
client.connect(
hostname=self.hostname,
port=self.port or 22,
username=self.username,
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
transport = paramiko.Transport(sock)
transport.get_security_options().kex = _LEGACY_KEX
transport.connect(
username=self.username,
password=self.password,
)
_, stdout, _ = client.exec_command("show running-config", timeout=_FALLBACK_TIMEOUT)
config = stdout.read().decode("utf-8", errors="replace")
client.close()
return config
channel = transport.open_session()
channel.settimeout(_FALLBACK_TIMEOUT)
channel.exec_command("show running-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:
logger.warning("SSH config fallback failed for %s: %s", self.hostname, exc)
return ""