diff --git a/napalm_procurve/procurve.py b/napalm_procurve/procurve.py index 234955d..f63e78a 100644 --- a/napalm_procurve/procurve.py +++ b/napalm_procurve/procurve.py @@ -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 ""