121 lines
4.9 KiB
Python
121 lines
4.9 KiB
Python
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)
|