from __future__ import annotations import socket import ssl import subprocess import time from copy import deepcopy from typing import Any, Callable class NetworkDiagnostics: def __init__( self, status_provider: Callable[[], dict[str, Any]], *, runner=subprocess.run, resolver=socket.getaddrinfo, connector=socket.create_connection, ) -> None: self.status_provider = status_provider self.runner = runner self.resolver = resolver self.connector = connector @staticmethod def _check(code: str, ok: bool, message: str, *, warning: bool = False) -> dict[str, Any]: return {"code": code, "ok": ok, "warning": warning, "message": message} def _command(self, command: list[str], timeout: int = 3) -> subprocess.CompletedProcess[str] | None: try: return self.runner(command, capture_output=True, text=True, timeout=timeout) except (OSError, subprocess.TimeoutExpired): return None def run(self) -> dict[str, Any]: started = time.monotonic() checks: list[dict[str, Any]] = [] active = (self.status_provider().get("active") or {}) connected = active.get("connected") is True and bool(active.get("ipv4_address")) checks.append(self._check("interface", connected, "网络接口已连接" if connected else "网卡或 WiFi 未连接")) if not connected: return self._result(checks, started, "failed", "网卡或 WiFi 未连接") route = self._command(["/usr/sbin/ip", "-4", "route", "show", "default"]) route_ok = bool(route and route.returncode == 0 and route.stdout.strip()) checks.append(self._check("default_route", route_ok, "默认路由正常" if route_ok else "默认网关缺失")) if not route_ok: return self._result(checks, started, "failed", "默认网关缺失") dns_ok = False for host in ("www.baidu.com", "www.qq.com"): try: if self.resolver(host, 443, type=socket.SOCK_STREAM): dns_ok = True break except OSError: continue checks.append(self._check("dns", dns_ok, "DNS 解析正常" if dns_ok else "DNS 解析失败")) if not dns_ok: return self._result(checks, started, "failed", "DNS 解析失败,请检查 DNS 或网关配置") reachable = False tls_ok = False for host in ("www.baidu.com", "www.qq.com"): try: raw = self.connector((host, 443), timeout=3) reachable = True try: context = ssl.create_default_context() with context.wrap_socket(raw, server_hostname=host): tls_ok = True break finally: try: raw.close() except OSError: pass except (OSError, ssl.SSLError): continue checks.append(self._check("internet", reachable, "外网 TCP 连接正常" if reachable else "网关或上游网络不可达")) checks.append(self._check("tls", tls_ok, "TLS 校验正常" if tls_ok else "TLS 或系统时间可能异常")) ping = self._command(["/usr/bin/ping", "-c", "1", "-W", "2", "www.baidu.com"]) ping_ok = bool(ping and ping.returncode == 0) checks.append(self._check( "icmp", ping_ok, "百度 ping 正常" if ping_ok else "目标未响应 ping;部分网络会屏蔽 ICMP", warning=not ping_ok and tls_ok, )) if tls_ok: return self._result(checks, started, "ok", "网络连接正常") if reachable: return self._result(checks, started, "failed", "TLS 或系统时间可能异常") return self._result(checks, started, "failed", "网关或上游网络不可达") @staticmethod def _result(checks: list[dict[str, Any]], started: float, overall: str, diagnosis: str) -> dict[str, Any]: return { "overall": overall, "diagnosis": diagnosis, "duration_ms": int((time.monotonic() - started) * 1000), "checks": checks, } class MockNetworkDiagnostics: def __init__(self, result: dict[str, Any] | None = None) -> None: self.result = result or { "overall": "ok", "diagnosis": "网络连接正常(mock)", "duration_ms": 1, "checks": [ {"code": "interface", "ok": True, "warning": False, "message": "网络接口已连接"}, {"code": "dns", "ok": True, "warning": False, "message": "DNS 解析正常"}, {"code": "internet", "ok": True, "warning": False, "message": "外网连接正常"}, ], } def run(self) -> dict[str, Any]: return deepcopy(self.result)