feat: implement comprehensive error diagnostics and structured error codes for VPN connectivity, API fetching, and proxy initialization.

This commit is contained in:
baoweise-bot
2026-05-30 14:02:39 +08:00
parent 531beca616
commit 7b9248c4df
4 changed files with 638 additions and 440 deletions
+2 -1
View File
@@ -436,7 +436,8 @@ def print_status():
print_line(format_line("出口 IP (出站)", proxy_ip))
print_line(format_line("本地代理延迟", f"{proxy_latency} ms" if proxy_latency else "检测中..."))
else:
print_line(format_line("出口 IP (出站)", f"{red}[检测中/未就绪]{reset}"))
proxy_err = state.get("proxy_error") or "检测中/未就绪"
print_line(format_line("出口 IP (出站)", f"{red}[不可用 - {proxy_err}]{reset}"))
else:
print_line(format_line("节点状态", "无活动连接"))
print_line()
+13 -2
View File
@@ -51,7 +51,11 @@ def resolve_dns_over_tun0(host: str, dns_server: str = "8.8.8.8", timeout: float
sock.settimeout(timeout)
try:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, b"tun0")
except OSError:
except OSError as e:
if "operation not permitted" in str(e).lower() or e.errno == 1:
print("[DNS 绑定失败] [错误代码 3006] DNS 解析绑定 tun0 权限不足,请确保程序以 root 权限运行!", flush=True)
elif "no such device" in str(e).lower() or e.errno == 19:
print("[DNS 绑定失败] [错误代码 3004] DNS 解析绑定 tun0 失败,网卡设备不存在,请检查 VPN 连接!", flush=True)
return None
sock.sendto(packet, (dns_server, 53))
resp, _ = sock.recvfrom(2048)
@@ -131,6 +135,10 @@ def create_connection(address: tuple[str, int], timeout: float = 20) -> socket.s
return sock
except OSError as e:
err = e
if "operation not permitted" in str(e).lower() or e.errno == 1:
err = OSError(f"[错误代码 3006] [ERR_PROXY_BIND_TUN_PERM_DENIED] 绑定虚拟网卡 tun0 失败,权限不足!必须以 root 权限运行,或者进程缺少 CAP_NET_RAW 权限。")
elif "no such device" in str(e).lower() or e.errno == 19:
err = OSError(f"[错误代码 3004] [ERR_ROUTE_DEV_NOT_FOUND] 绑定虚拟网卡 tun0 失败,找不到设备!这通常是因为 OpenVPN 核心未能成功连接或已被异常终止。")
if sock is not None:
sock.close()
if err is not None:
@@ -255,7 +263,10 @@ def start_proxy_server(host: str, port: int) -> None:
server.listen(256)
print(f"HTTP/SOCKS5 proxy listening on {host}:{port}", flush=True)
except Exception as e:
print(f"[ERROR] Failed to start HTTP/SOCKS5 proxy on {host}:{port}: {e}", flush=True)
import vpn_utils
diag = vpn_utils.diagnose_local_obstructions(port)
diag_msg = diag[1] if diag else str(e)
print(f"[ERROR] Failed to start HTTP/SOCKS5 proxy on {host}:{port}: {diag_msg}", flush=True)
return
while True:
+592 -422
View File
File diff suppressed because it is too large Load Diff
+31 -15
View File
@@ -333,8 +333,16 @@ def fetch_candidates() -> list[dict[str, Any]]:
print(f"[fetch_candidates] Fetch {i+1} failed: {e}", flush=True)
log_to_json("WARNING", "Main", f"{i+1} 次拉取 API 节点失败: {e}")
if i == max_attempts - 1 and not candidates:
log_to_json("ERROR", "Main", f"获取官方 API 节点失败: {e}")
raise
err_code, diag_msg = vpn_utils.diagnose_api_failure(API_URL)
full_err_msg = f"获取官方 API 节点失败: {e} | 诊断结果: {diag_msg}"
print(f"[错误代码 {err_code}] {full_err_msg}", flush=True)
log_to_json("ERROR", "Main", f"[错误代码 {err_code}] {full_err_msg}")
set_state(
last_fetch_status="error",
last_fetch_error_code=err_code,
last_fetch_message=diag_msg
)
raise RuntimeError(diag_msg) from e
set_state(
last_fetch_at=time.time(),
@@ -468,9 +476,9 @@ def run_openvpn_until_ready(config_file: str, keep_alive: bool, route_nopull: bo
cwd=str(ROOT_DIR),
)
except FileNotFoundError:
return False, "openvpn command not found", None
return False, "[错误代码 2001] [ERR_OVPN_CMD_NOT_FOUND] 未找到 openvpn 命令。原因: 系统未安装 openvpn,或 PATH 环境变量不正确。", None
except OSError as exc:
return False, f"openvpn start failed: {exc}", None
return False, f"[错误代码 2002] [ERR_OVPN_START_FAILED] openvpn 启动失败: {exc}。原因: 系统权限不足或配置冲突。", None
lines: queue.Queue[str | None] = queue.Queue()
startup_done = [False]
@@ -521,8 +529,9 @@ def run_openvpn_until_ready(config_file: str, keep_alive: bool, route_nopull: bo
else:
message = f"OpenVPN timeout after {limit}s."
if not ok and tail:
message = tail[-1][-220:]
if not ok:
err_code, diag_msg = vpn_utils.diagnose_openvpn_failure(tail)
message = f"[错误代码 {err_code}] {diag_msg} (原始日志尾部: {tail[-1][-100:] if tail else ''})"
startup_done[0] = True
if not keep_alive or not ok:
stop_process(process)
@@ -975,7 +984,11 @@ def maintain_valid_nodes(force: bool = False) -> str:
candidates = fetch_candidates()
except Exception as exc:
vpn_utils.check_and_fix_dns()
set_state(last_fetch_at=time.time(), last_fetch_status="error", last_fetch_message=str(exc))
diag_msg = str(exc)
if not any(token in diag_msg for token in ["[ERR_", "错误代码"]):
err_code, raw_diag = vpn_utils.diagnose_api_failure(API_URL)
diag_msg = f"[错误代码 {err_code}] 获取节点失败: {exc} | 诊断结果: {raw_diag}"
set_state(last_fetch_at=time.time(), last_fetch_status="error", last_fetch_message=diag_msg)
candidates = []
if not candidates:
@@ -2709,11 +2722,7 @@ function render(){
pBadge.className = "badge unavailable";
pBadge.textContent = "不可用";
pIpVal.textContent = "-";
if (state.last_check_message) {
pLatVal.innerHTML = `<span style="color: var(--text-secondary); font-size: 12px;">${esc(state.last_check_message)}</span>`;
} else {
pLatVal.innerHTML = `<span class="latency-val latency-poor" style="margin-left:8px; font-size:11px;" title="${esc(state.proxy_error)}">${esc(state.proxy_error || "连接失败")}</span>`;
}
pLatVal.innerHTML = `<span class="latency-val latency-poor" style="margin-left:8px; font-size:11px; max-width: 450px; display: inline-block; white-space: normal; line-height: 1.4; text-align: left;" title="${esc(state.proxy_error)}">${esc(state.proxy_error || "连接失败")}</span>`;
}
} else {
pBadge.className = "badge not_checked";
@@ -3303,9 +3312,11 @@ def check_proxy_health() -> dict[str, Any]:
try:
s.connect(("127.0.0.1", LOCAL_PROXY_PORT))
except Exception as e:
diag = vpn_utils.diagnose_local_obstructions(LOCAL_PROXY_PORT)
diag_msg = diag[1] if diag else f"端口 {LOCAL_PROXY_PORT} 连接失败,原因: {e}"
return {
"ok": False,
"error": f"代理服务未运行 (端口 {LOCAL_PROXY_PORT} 连接失败,原因: {e})"
"error": f"代理服务未运行 ({diag_msg})"
}
finally:
try:
@@ -3318,7 +3329,7 @@ def check_proxy_health() -> dict[str, Any]:
if sys.platform.startswith("linux") and not tun_path.exists():
return {
"ok": False,
"error": "VPN 虚拟网卡 (tun0) 未启用,请确保当前已成功连接 VPN 节点"
"error": "[错误代码 3004] [ERR_ROUTE_DEV_NOT_FOUND] VPN 虚拟网卡 (tun0) 未启用,请确保当前已成功连接 VPN 节点"
}
# 3. 使用 curl 通过本地 SOCKS5 代理接口测试 IP 与实际延迟
@@ -3353,7 +3364,12 @@ def check_proxy_health() -> dict[str, Any]:
result = _curl_check_ip("http://api.ipify.org")
if result:
return result
return {"ok": False, "error": "出口连接测试失败 (ip.sb 和 api.ipify.org 均无法连通)"}
diag = vpn_utils.diagnose_local_obstructions(LOCAL_PROXY_PORT)
if diag:
return {"ok": False, "error": f"出口连接测试失败 | 本机诊断结果: {diag[1]}"}
return {"ok": False, "error": "出口连接测试失败 (ip.sb 和 api.ipify.org 均无法连通,可能是节点已失效或 VPS 防火墙限制了 UDP/TCP 出站端口)"}
except Exception as e:
return {"ok": False, "error": f"出口连接测试异常: {e}"}