From a6f8722ad3381a4f599aba17b029ef7ba55de889 Mon Sep 17 00:00:00 2001 From: dv Date: Mon, 13 Apr 2026 16:38:19 +0300 Subject: [PATCH] =?UTF-8?q?+=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BC=D0=B8=D0=BA=D1=80=D0=BE=D1=82=D0=B8=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/services/discovery/orchestrator.py | 11 +++- backend/app/services/discovery/ssh_client.py | 56 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/backend/app/services/discovery/orchestrator.py b/backend/app/services/discovery/orchestrator.py index 5ede2e3..145ed12 100644 --- a/backend/app/services/discovery/orchestrator.py +++ b/backend/app/services/discovery/orchestrator.py @@ -32,6 +32,7 @@ from .ssh_client import ( fetch_slave_config, get_ip_neighbors_multi, ssh_run_multi, + verify_is_mikrotik, ) from .types import DeviceAction, DiscoveredDevice, DiscoveryResult, ProgressCallback @@ -811,7 +812,15 @@ async def _discover_mikrotik( logger.info("[discovery] MikroTik: no IP found via SSH_CLIENT") return - logger.info("[discovery] MikroTik found at %s", mikrotik_ip) + logger.info("[discovery] MikroTik candidate: %s — verifying...", mikrotik_ip) + await _emit("disc_action", {"message": f"Проверка MikroTik {mikrotik_ip} (WinBox/SSH)..."}) + is_mikrotik = await verify_is_mikrotik(mikrotik_ip, ssh_port, ssh_options) + if not is_mikrotik: + logger.info("[discovery] %s — NOT a MikroTik (WinBox and SSH banner check failed), skipping", mikrotik_ip) + await _emit("disc_action", {"message": f"{mikrotik_ip} — не MikroTik, пропускаем"}) + return + + logger.info("[discovery] MikroTik confirmed at %s", mikrotik_ip) await _emit("disc_infra_found", {"type": "mikrotik", "ip": mikrotik_ip}) try: existing = (await db.execute( diff --git a/backend/app/services/discovery/ssh_client.py b/backend/app/services/discovery/ssh_client.py index 06d726c..57b6bfb 100644 --- a/backend/app/services/discovery/ssh_client.py +++ b/backend/app/services/discovery/ssh_client.py @@ -177,6 +177,62 @@ async def open_ssh_tunnel( raise last_exc +# ── MikroTik verification ───────────────────────────────────────────────────── + +async def verify_is_mikrotik( + host: str, + ssh_port: int = 22, + ssh_options: list | None = None, + timeout: float = 3.0, +) -> bool: + """Check whether host is actually a MikroTik RouterOS device. + + Two independent probes are tried (first positive result wins): + 1. TCP connect to port 8291 (WinBox) — exclusively used by MikroTik. + 2. SSH banner check — RouterOS advertises "ROSSSH" as its SSH version. + + Returns True if either probe confirms MikroTik, False otherwise. + """ + # Probe 1: WinBox port 8291 (MikroTik-exclusive) + try: + reader, writer = await asyncio.wait_for( + asyncio.open_connection(host, 8291), timeout=timeout + ) + writer.close() + try: + await writer.wait_closed() + except Exception: + pass + logger.info("[discovery] %s — MikroTik confirmed via WinBox port 8291", host) + return True + except Exception: + pass + + # Probe 2: SSH banner + if ssh_options: + for opt in ssh_options: + try: + kwargs = _build_connect_kwargs(host, ssh_port, opt) + kwargs["connect_timeout"] = int(timeout) + async with asyncssh.connect(**kwargs) as conn: + banner = (conn.get_extra_info("server_version") or "").lower() + if "rosssh" in banner or "mikrotik" in banner: + logger.info( + "[discovery] %s — MikroTik confirmed via SSH banner: %s", host, banner + ) + return True + logger.info( + "[discovery] %s — SSH connected but banner does not match MikroTik: %s", + host, banner, + ) + return False # Connected successfully — definitely not MikroTik + except Exception: + continue + + logger.info("[discovery] %s — could not verify MikroTik (no probe succeeded)", host) + return False + + # ── Internal helpers ────────────────────────────────────────────────────────── def _parse_neigh_output(stdout: str) -> list[str]: