diff --git a/backend/app/services/discovery/ssh_client.py b/backend/app/services/discovery/ssh_client.py index 57b6bfb..0513884 100644 --- a/backend/app/services/discovery/ssh_client.py +++ b/backend/app/services/discovery/ssh_client.py @@ -22,6 +22,8 @@ async def read_remote_file( remote_path: str, ) -> str: """SSH into host and return the contents of remote_path.""" + if not await is_port_open(host, port, timeout=3.0): + raise ConnectionRefusedError(f"{host}:{port} — порт недоступен") async with asyncssh.connect( host, port=port, username=username, password=password, known_hosts=None, connect_timeout=10, @@ -35,6 +37,9 @@ async def get_ip_neighbors( ) -> list[str]: """Run `ip neigh show` and return REACHABLE/DELAY neighbour IPs.""" try: + if not await is_port_open(host, port, timeout=3.0): + logger.info("[discovery] %s:%d — port closed, skipping ip neigh", host, port) + return [] async with asyncssh.connect( host, port=port, username=username, password=password, known_hosts=None, connect_timeout=10, @@ -46,12 +51,34 @@ async def get_ip_neighbors( return [] +# ── TCP port probe ──────────────────────────────────────────────────────────── + +async def is_port_open(host: str, port: int, timeout: float = 3.0) -> bool: + """Quick TCP connect check. Returns True if port is reachable, False otherwise.""" + try: + _, writer = await asyncio.wait_for( + asyncio.open_connection(host, port), timeout=timeout + ) + writer.close() + try: + await writer.wait_closed() + except Exception: + pass + return True + except Exception: + return False + + # ── Multi-auth helpers ──────────────────────────────────────────────────────── async def ssh_run_multi( host: str, port: int, options: list, command: str, timeout: int = 15, ) -> str: """Try each SSH auth option in order; return stdout on first success or raise.""" + # Fast pre-check: skip SSH entirely if port is not open + if not await is_port_open(host, port, timeout=3.0): + raise ConnectionRefusedError(f"{host}:{port} — порт недоступен (TCP check failed)") + last_exc: Exception = Exception("No auth options provided") for i, opt in enumerate(options): _log_attempt(i, len(options), host, opt)