"""路线 B 的 Windows TUN 后端:tun2socks + 自有 SOCKS5/UoT 桥接。 这里不启动 sing-box。tun2socks 只负责 Wintun 和用户态 TCP/IP 栈, 所有上游协议由本项目的 ``socks_bridge`` 处理:TCP 使用 HTTP CONNECT, UDP 使用裸 UoT v2。 """ from __future__ import annotations import ctypes import ipaddress import json import os import shutil import socket import subprocess import sys import threading import time from pathlib import Path from typing import Any, Callable from .socks_bridge import Socks5Bridge class TunBackendError(RuntimeError): """TUN 后端无法启动或停止。""" class TunBackend: def __init__(self, user_dir: str | Path, engine_path: str | Path | None = None, log: Callable[[str], None] | None = None, detail_log: Callable[[str], None] | None = None): self.user_dir = Path(user_dir) self.engine_path = Path(engine_path) if engine_path else None self.log = log or (lambda _message: None) self.detail_log = detail_log or self.log self._process: subprocess.Popen[str] | None = None self._bridge: Socks5Bridge | None = None self._lock = threading.RLock() self._route_added = False self._ipv6_route_added = False self._lan_routes: list[str] = [] self._lan_ipv6_routes: list[str] = [] self._node_routes: list[str] = [] self._node_ipv6_routes: list[str] = [] self._physical_gateway = "" self._physical_ifindex = "" self._physical_ipv6_gateway = "" self._physical_ipv6_ifindex = "" self._device = "RRPP-TUN" self._address = "10.255.0.1" self._ipv6_address = "fd42:7272:5050::1" self._dns_configured = False self._tun_dns_servers = ("8.8.8.8", "1.1.1.1") self._bypass_lan = True self._cleanup_pending = False self._state_path = self.user_dir / "tun_state.json" # Wintun 是三层虚拟接口,默认路由的下一跳使用接口自身地址。 self._gateway = self._address self._ipv6_gateway = self._ipv6_address self._recover_stale_state() @staticmethod def is_admin() -> bool: if os.name != "nt": return True try: return bool(ctypes.windll.shell32.IsUserAnAdmin()) except Exception: return False def resolve_engine(self) -> Path: candidates: list[Path] = [] if self.engine_path: candidates.append(self.engine_path) # 目录式发布:主 EXE 与 tun2socks.exe、wintun.dll 放在同一目录。 # PyInstaller onefile:主程序会把内置文件解压到 _MEIPASS。 executable_dir = Path(sys.executable).resolve().parent meipass = getattr(sys, "_MEIPASS", None) package_dir = Path(__file__).resolve().parents[1] candidates.extend([ self.user_dir / "tun2socks.exe", self.user_dir.parent / "tun2socks.exe", executable_dir / "tun2socks.exe", Path(meipass) / "tun2socks.exe" if meipass else Path("__rrpp_no_meipass__"), package_dir / "tun2socks.exe", package_dir / "runtime" / "tun2socks.exe", ]) found = shutil.which("tun2socks") if found: candidates.append(Path(found)) for candidate in candidates: try: path = candidate.expanduser().resolve() except OSError: continue if path.is_file(): return path raise TunBackendError( "未找到 tun2socks.exe。请将 tun2socks.exe 和 wintun.dll 放在 TUN 程序目录," "或在 config.json 的 tun_engine_path 中填写完整路径。" ) @staticmethod def _physical_interface() -> str: command = ["powershell", "-NoProfile", "-Command", "(Get-NetRoute -DestinationPrefix '0.0.0.0/0' | " "Where-Object InterfaceAlias -ne 'RRPP-TUN' | " "Sort-Object RouteMetric | Select-Object -First 1 -ExpandProperty InterfaceAlias)"] try: result = subprocess.run(command, capture_output=True, text=True, timeout=5) value = (result.stdout or "").strip().splitlines() if value and value[0].strip(): return value[0].strip() except Exception: pass return "" @staticmethod def _physical_source_address(interface: str) -> str: if not interface: return "" escaped = interface.replace("'", "''") command = ["powershell", "-NoProfile", "-Command", f"(Get-NetIPAddress -InterfaceAlias '{escaped}' -AddressFamily IPv4 " "-AddressState Preferred | Select-Object -First 1 -ExpandProperty IPAddress)"] try: result = subprocess.run(command, capture_output=True, text=True, timeout=5) value = (result.stdout or "").strip().splitlines() if value and value[0].strip(): return value[0].strip() except Exception: pass return "" @staticmethod def _physical_route_info() -> tuple[str, str]: """返回当前物理默认路由的网关和接口索引。""" command = ["powershell", "-NoProfile", "-Command", "Get-NetRoute -DestinationPrefix '0.0.0.0/0' | " "Where-Object InterfaceAlias -ne 'RRPP-TUN' | " "Sort-Object RouteMetric | Select-Object -First 1 " "NextHop,InterfaceIndex | ConvertTo-Json -Compress"] try: result = subprocess.run(command, capture_output=True, text=True, timeout=5) data = json.loads((result.stdout or "").strip()) if isinstance(data, dict): return str(data.get("NextHop") or ""), str(data.get("InterfaceIndex") or "") except Exception: pass return "", "" @staticmethod def _physical_ipv6_route_info() -> tuple[str, str]: """返回当前物理 IPv6 默认路由的下一跳和接口索引。""" command = ["powershell", "-NoProfile", "-Command", "Get-NetRoute -AddressFamily IPv6 -DestinationPrefix '::/0' | " "Where-Object InterfaceAlias -ne 'RRPP-TUN' | " "Sort-Object RouteMetric | Select-Object -First 1 " "NextHop,InterfaceIndex | ConvertTo-Json -Compress"] try: result = subprocess.run(command, capture_output=True, text=True, timeout=5) data = json.loads((result.stdout or "").strip()) if isinstance(data, dict): return str(data.get("NextHop") or ""), str(data.get("InterfaceIndex") or "") except Exception: pass return "", "" @staticmethod def _active_global_ipv6() -> bool | None: """返回是否存在公网 IPv6;无法查询时返回 None,避免误报已接管。""" if os.name != "nt": return False command = [ "powershell", "-NoProfile", "-Command", "@(Get-NetIPAddress -AddressFamily IPv6 -AddressState Preferred | " "Where-Object { $_.IPAddress -notlike 'fe80:*' -and " "$_.IPAddress -ne '::1' -and $_.IPAddress -notlike 'fc*' -and " "$_.IPAddress -notlike 'fd*' }).IPAddress | ConvertTo-Json -Compress", ] try: result = subprocess.run(command, capture_output=True, text=True, timeout=5) if result.returncode: return None value = json.loads((result.stdout or "").strip() or "[]") if isinstance(value, str): return bool(value.strip()) return bool(value) except Exception: return None @staticmethod def _resolve_proxy_addresses(node: dict[str, Any]) -> list[tuple]: methods = (node.get("methods") or {}).get("http") or {} host = str(node.get("domain") or "").strip() port = int(methods.get("port") or node.get("port") or 443) if not host: return [] addresses: list[tuple] = [] for item in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM): if item[4] not in addresses: addresses.append(item[4]) return addresses @staticmethod def _node_chain(node: dict[str, Any]) -> list[dict[str, Any]]: chain = node.get("_jump_chain") if isinstance(node, dict) else None if isinstance(chain, list) and chain: return [item for item in chain if isinstance(item, dict)] return [node] if isinstance(node, dict) else [] @staticmethod def _node_key(node: dict[str, Any]) -> str: methods = (node.get("methods") or {}).get("http") or {} return "|".join((str(node.get("domain") or ""), str(methods.get("port") or node.get("port") or 443), str(methods.get("username") or node.get("username") or ""))) def _add_node_routes(self, proxy_addresses: list[tuple]) -> None: """把 HTTP CONNECT 节点固定走物理网卡,防止默认 TUN 路由形成回环。""" candidates_v4: list[str] = [] candidates_v6: list[str] = [] for sockaddr in proxy_addresses: address = str(sockaddr[0]) try: parsed = ipaddress.ip_address(address.split("%", 1)[0]) except ValueError: continue target = candidates_v4 if parsed.version == 4 else candidates_v6 normalized = parsed.compressed if normalized not in target: target.append(normalized) if candidates_v4 and not self._physical_ifindex: raise TunBackendError("找不到物理 IPv4 接口,无法固定代理节点路由") for address in candidates_v4: if address in self._node_routes: continue command = ["route", "ADD", address, "MASK", "255.255.255.255"] if self._physical_gateway: command.append(self._physical_gateway) command.extend(["METRIC", "1", "IF", self._physical_ifindex]) result = subprocess.run(command, capture_output=True, text=True, encoding="mbcs", errors="replace", timeout=15) if result.returncode: detail = (result.stderr or result.stdout or "").strip() # 已存在的等价路由不影响启动;其它错误继续暴露,避免静默回环。 if "already exists" not in detail.lower() and "已存在" not in detail: raise TunBackendError(f"节点物理路由添加失败 ({address}): {detail[:200]}") continue self._node_routes.append(address) self._write_state() if candidates_v6 and not self._physical_ipv6_ifindex: # 双栈节点仍可使用 IPv4 地址;只有 IPv6-only 节点需要物理 IPv6 出口。 if candidates_v4: candidates_v6 = [] else: raise TunBackendError("代理节点仅包含 IPv6 地址,但找不到物理 IPv6 默认路由") for address in candidates_v6: if address in self._node_ipv6_routes: continue self._add_ipv6_physical_route(address) def _add_ipv6_physical_route(self, address: str) -> None: if not self._physical_ipv6_ifindex: raise TunBackendError("找不到物理 IPv6 接口,无法固定 IPv6 路由") command = ["interface", "ipv6", "add", "route", f"prefix={address}/128", f"interface={self._physical_ipv6_ifindex}"] if self._physical_ipv6_gateway and self._physical_ipv6_gateway != "::": command.append(f"nexthop={self._physical_ipv6_gateway}") command.append("metric=1") result = subprocess.run(["netsh", *command], capture_output=True, text=True, encoding="mbcs", errors="replace", timeout=15) if result.returncode: detail = (result.stderr or result.stdout or "").strip() if "already exists" not in detail.lower() and "已存在" not in detail: raise TunBackendError(f"IPv6 节点物理路由添加失败 ({address}): {detail[:200]}") return self._node_ipv6_routes.append(address) self._write_state() def _ensure_physical_route(self, address: str) -> None: """为 TUN 内部的直连目标补一条物理出口 /32 路由。""" if os.name != "nt": return try: parsed = ipaddress.ip_address(str(address).split("%", 1)[0]) except ValueError: return if parsed.version == 6: normalized = parsed.compressed if normalized in self._node_ipv6_routes: return self._add_ipv6_physical_route(normalized) return if not self._physical_ifindex: raise TunBackendError("找不到物理 IPv4 接口,无法固定直连路由") normalized = parsed.compressed if normalized in self._node_routes: return command = ["route", "ADD", normalized, "MASK", "255.255.255.255"] if self._physical_gateway: command.append(self._physical_gateway) command.extend(["METRIC", "1", "IF", self._physical_ifindex]) result = subprocess.run(command, capture_output=True, text=True, encoding="mbcs", errors="replace", timeout=15) detail = (result.stderr or result.stdout or "").strip() if result.returncode: if "already exists" in detail.lower() or "已存在" in detail: return raise TunBackendError(f"直连目标物理路由添加失败 ({normalized}): {detail[:200]}") self._node_routes.append(normalized) self._write_state() @staticmethod def _cleanup_succeeded(result: subprocess.CompletedProcess[str]) -> bool: if result.returncode == 0: return True detail = (result.stderr or result.stdout or "").strip().lower() # 清理是幂等操作:路由或网卡已经消失时,目标状态已经达到。 return any(marker in detail for marker in ( "not found", "does not exist", "cannot find", "no such", "找不到", "不存在", "未找到", "没有找到", )) def _cleanup_netsh(self, args: list[str]) -> bool: try: result = subprocess.run(["netsh", *args], capture_output=True, text=True, encoding="mbcs", errors="replace", timeout=15) except Exception as exc: self.detail_log(f"[TUN] 清理 netsh 失败: {exc}") return False if self._cleanup_succeeded(result): return True detail = (result.stderr or result.stdout or "").strip() self.detail_log(f"[TUN] 清理 netsh 未完成: {detail[:300]}") return False def _remove_node_routes(self) -> bool: remaining: list[str] = [] for address in self._node_routes: try: result = subprocess.run(["route", "DELETE", address], capture_output=True, text=True, encoding="mbcs", errors="replace", timeout=15) except Exception as exc: self.detail_log(f"[TUN] 清理节点路由失败 ({address}): {exc}") remaining.append(address) continue if not self._cleanup_succeeded(result): detail = (result.stderr or result.stdout or "").strip() self.detail_log(f"[TUN] 清理节点路由未完成 ({address}): {detail[:200]}") remaining.append(address) self._node_routes = remaining remaining_v6: list[str] = [] for address in self._node_ipv6_routes: command = ["interface", "ipv6", "delete", "route", f"prefix={address}/128"] if self._physical_ipv6_ifindex: command.append(f"interface={self._physical_ipv6_ifindex}") try: result = subprocess.run(["netsh", *command], capture_output=True, text=True, encoding="mbcs", errors="replace", timeout=15) except Exception as exc: self.detail_log(f"[TUN] 清理 IPv6 节点路由失败 ({address}): {exc}") remaining_v6.append(address) continue if not self._cleanup_succeeded(result): detail = (result.stderr or result.stdout or "").strip() self.detail_log(f"[TUN] 清理 IPv6 节点路由未完成 ({address}): {detail[:200]}") remaining_v6.append(address) self._node_ipv6_routes = remaining_v6 return not remaining and not remaining_v6 def _netsh(self, args: list[str], *, ignore_error: bool = False) -> None: result = subprocess.run(["netsh", *args], capture_output=True, text=True, encoding="mbcs", errors="replace", timeout=15) if result.returncode and not ignore_error: detail = (result.stderr or result.stdout or "").strip() raise TunBackendError(f"netsh 操作失败: {detail[:300]}") def _configure_routes(self) -> None: for _ in range(20): try: probe = subprocess.run( ["powershell", "-NoProfile", "-Command", f"Get-NetAdapter -Name '{self._device}' -ErrorAction SilentlyContinue"], capture_output=True, text=True, timeout=3, ) if probe.stdout and self._device.lower() in probe.stdout.lower(): break except Exception: pass time.sleep(0.25) else: raise TunBackendError(f"tun2socks 未创建虚拟网卡: {self._device}") self._netsh(["interface", "ipv4", "set", "address", f"name={self._device}", "source=static", f"addr={self._address}", "mask=255.255.255.252"]) self._netsh(["interface", "ipv4", "add", "route", "0.0.0.0/0", f"{self._device}", self._gateway, "metric=1"]) self._route_added = True self._write_state() self._netsh(["interface", "ipv6", "set", "address", f"interface={self._device}", f"address={self._ipv6_address}", "store=active"]) self._netsh(["interface", "ipv6", "add", "route", "prefix=::/0", f"interface={self._device}", f"nexthop={self._ipv6_gateway}", "metric=1"]) self._ipv6_route_added = True self._write_state() if not self._bypass_lan: # Windows 物理网卡通常已有更具体的私网路由;补同前缀的 TUN # 路由,确保“内网也代理”不会在进入 tun2socks 前被物理网卡截走。 for network in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16"): self._netsh(["interface", "ipv4", "add", "route", network, f"{self._device}", self._gateway, "metric=1"]) self._lan_routes.append(network) self._write_state() for network in ("fc00::/7",): self._netsh(["interface", "ipv6", "add", "route", f"prefix={network}", f"interface={self._device}", f"nexthop={self._ipv6_gateway}", "metric=1"]) self._lan_ipv6_routes.append(network) self._write_state() self._configure_dns() def _configure_dns(self) -> None: """让 Windows DNS 请求也进入 TUN,避免沿用物理网卡 DNS。""" if os.name != "nt": return ipv4 = [item for item in self._tun_dns_servers if ":" not in item] ipv6 = [item for item in self._tun_dns_servers if ":" in item] if ipv4: self._netsh([ "interface", "ipv4", "set", "dnsservers", f"name={self._device}", "source=static", f"address={ipv4[0]}", "register=none", "validate=no", ]) for index, secondary in enumerate(ipv4[1:], start=2): self._netsh([ "interface", "ipv4", "add", "dnsservers", f"name={self._device}", f"address={secondary}", f"index={index}", "validate=no", ], ignore_error=True) if ipv6: self._netsh([ "interface", "ipv6", "set", "dnsservers", f"name={self._device}", "source=static", f"address={ipv6[0]}", "register=none", "validate=no", ], ignore_error=True) for index, secondary in enumerate(ipv6[1:], start=2): self._netsh([ "interface", "ipv6", "add", "dnsservers", f"name={self._device}", f"address={secondary}", f"index={index}", "validate=no", ], ignore_error=True) self._dns_configured = True self._write_state() def _remove_routes(self) -> None: cleanup_ok = True if self._dns_configured: ipv4_ok = self._cleanup_netsh([ "interface", "ipv4", "set", "dnsservers", f"name={self._device}", "source=dhcp", ]) ipv6_ok = self._cleanup_netsh([ "interface", "ipv6", "set", "dnsservers", f"name={self._device}", "source=dhcp", ]) cleanup_ok &= ipv4_ok and ipv6_ok if ipv4_ok and ipv6_ok: self._dns_configured = False if self._route_added: route_ok = self._cleanup_netsh(["interface", "ipv4", "delete", "route", "0.0.0.0/0", f"{self._device}", self._gateway]) cleanup_ok &= route_ok if route_ok: self._route_added = False if self._ipv6_route_added: route_ok = self._cleanup_netsh(["interface", "ipv6", "delete", "route", "prefix=::/0", f"interface={self._device}", f"nexthop={self._ipv6_gateway}"]) cleanup_ok &= route_ok if route_ok: self._ipv6_route_added = False remaining_lan: list[str] = [] for network in self._lan_routes: route_ok = self._cleanup_netsh(["interface", "ipv4", "delete", "route", network, f"{self._device}", self._gateway]) cleanup_ok &= route_ok if not route_ok: remaining_lan.append(network) self._lan_routes = remaining_lan remaining_lan_v6: list[str] = [] for network in self._lan_ipv6_routes: route_ok = self._cleanup_netsh(["interface", "ipv6", "delete", "route", f"prefix={network}", f"interface={self._device}", f"nexthop={self._ipv6_gateway}"]) cleanup_ok &= route_ok if not route_ok: remaining_lan_v6.append(network) self._lan_ipv6_routes = remaining_lan_v6 cleanup_ok &= self._remove_node_routes() self._cleanup_pending = not cleanup_ok if cleanup_ok: self._clear_state() else: self._write_state() self.detail_log("[TUN] 部分路由/DNS 清理失败,已保留状态并将在下次启动重试") def _write_state(self) -> None: """记录本进程改过的路由/DNS,强杀后下次启动可恢复。""" try: self.user_dir.mkdir(parents=True, exist_ok=True) payload = { "device": self._device, "address": self._address, "gateway": self._gateway, "ipv6_address": self._ipv6_address, "ipv6_gateway": self._ipv6_gateway, "lan_routes": list(self._lan_routes), "lan_ipv6_routes": list(self._lan_ipv6_routes), "bypass_lan": bool(self._bypass_lan), "physical_gateway": self._physical_gateway, "physical_ifindex": self._physical_ifindex, "physical_ipv6_gateway": self._physical_ipv6_gateway, "physical_ipv6_ifindex": self._physical_ipv6_ifindex, "node_routes": list(self._node_routes), "node_ipv6_routes": list(self._node_ipv6_routes), "route_added": bool(self._route_added), "ipv6_route_added": bool(self._ipv6_route_added), "dns_configured": bool(self._dns_configured), "dns_servers": list(self._tun_dns_servers), "cleanup_pending": bool(self._cleanup_pending), "pid": os.getpid(), } temp = self._state_path.with_suffix(".tmp") temp.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8") temp.replace(self._state_path) except Exception as exc: self.detail_log(f"[TUN] 状态记录失败: {exc}") def _clear_state(self) -> None: try: self._state_path.unlink(missing_ok=True) except Exception: pass @staticmethod def _pid_alive(pid: Any) -> bool: """避免合并版误清理仍在运行的旧 TUN 实例。""" if os.name != "nt": return False try: number = int(pid) if number <= 0: return False kernel = ctypes.windll.kernel32 kernel.OpenProcess.argtypes = [ctypes.c_ulong, ctypes.c_bool, ctypes.c_ulong] kernel.OpenProcess.restype = ctypes.c_void_p kernel.GetExitCodeProcess.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_ulong)] kernel.CloseHandle.argtypes = [ctypes.c_void_p] handle = kernel.OpenProcess(0x1000, False, number) if not handle: return False try: code = ctypes.c_ulong() return bool(kernel.GetExitCodeProcess(handle, ctypes.byref(code)) and code.value == 259) finally: kernel.CloseHandle(handle) except (TypeError, ValueError, OSError): return False def _recover_stale_state(self) -> None: """启动前清理上次崩溃/强杀留下的 RRPP-TUN 路由和 DNS。""" try: if not self._state_path.is_file(): return state = json.loads(self._state_path.read_text(encoding="utf-8")) if not state.get("cleanup_pending") and self._pid_alive(state.get("pid")): raise TunBackendError("检测到另一个 TUN 实例仍在运行,请先关闭后再启动") self._device = str(state.get("device") or self._device) self._address = str(state.get("address") or self._address) self._gateway = str(state.get("gateway") or self._gateway) self._ipv6_address = str(state.get("ipv6_address") or self._ipv6_address) self._ipv6_gateway = str(state.get("ipv6_gateway") or self._ipv6_gateway) self._physical_ipv6_gateway = str(state.get("physical_ipv6_gateway") or "") self._physical_ipv6_ifindex = str(state.get("physical_ipv6_ifindex") or "") self._route_added = bool(state.get("route_added")) self._ipv6_route_added = bool(state.get("ipv6_route_added")) self._lan_routes = [str(item).strip() for item in (state.get("lan_routes") or []) if str(item).strip()] self._lan_ipv6_routes = [str(item).strip() for item in (state.get("lan_ipv6_routes") or []) if str(item).strip()] self._node_routes = [str(item).strip() for item in (state.get("node_routes") or []) if str(item).strip()] self._node_ipv6_routes = [str(item).strip() for item in (state.get("node_ipv6_routes") or []) if str(item).strip()] self._dns_configured = bool(state.get("dns_configured")) self._cleanup_pending = True self._remove_routes() if self._cleanup_pending: self.detail_log("[TUN] 上次异常退出的部分路由/DNS 仍未清理,状态已保留") else: self.log("已清理上次异常退出遗留的 TUN 路由和 DNS") except TunBackendError: raise except Exception as exc: self.detail_log(f"[TUN] 异常退出残留清理失败: {exc}") def _read_output(self, process: subprocess.Popen[str]) -> None: if process.stdout is None: return try: for line in process.stdout: line = line.rstrip() if line: self.detail_log(f"[TUN] {line}") except OSError: pass def start(self, node: dict[str, Any], *, bypass_lan: bool = True, route_rules: list[dict[str, Any]] | None = None, dns_servers: tuple[str, ...] | list[str] | None = None, direct_target_resolver: Callable[[str, str, int], tuple[str, int] | None] | None = None, route_rule_resolver: Callable[[str], dict[str, Any] | None] | None = None, route_node_resolver: Callable[[dict[str, Any]], dict[str, Any] | None] | None = None, route_nodes: list[dict[str, Any]] | None = None) -> None: with self._lock: if self.status()["running"]: raise TunBackendError("TUN 已经在运行") if not self.is_admin(): raise TunBackendError("启动虚拟网卡 TUN 需要管理员权限") self._bypass_lan = bool(bypass_lan) if dns_servers is not None: cleaned = tuple(str(item).strip() for item in dns_servers if str(item).strip()) if not cleaned: raise TunBackendError("自定义 DNS 不能为空") try: for item in cleaned: address = ipaddress.ip_address(item) if address.is_unspecified or address.is_multicast: raise ValueError(item) except ValueError as exc: raise TunBackendError("自定义 DNS 必须是有效的 IPv4/IPv6 地址") from exc self._tun_dns_servers = cleaned[:3] engine = self.resolve_engine() interface = self._physical_interface() source_address = self._physical_source_address(interface) self._physical_gateway, self._physical_ifindex = self._physical_route_info() self._physical_ipv6_gateway, self._physical_ipv6_ifindex = self._physical_ipv6_route_info() if not self._physical_ifindex: raise TunBackendError("找不到物理默认路由,无法保证代理节点不进入 TUN 回环") if not self._physical_ipv6_ifindex: self.log("警告:未找到物理 IPv6 默认路由,IPv6 目标将仅尝试通过 IPv4 代理节点转发") try: proxy_addresses_by_node: dict[str, list[tuple]] = {} connection_nodes = [node] connection_nodes.extend( (item.get("node") if isinstance(item.get("node"), dict) else item) for item in (route_nodes or ()) if isinstance(item, dict) ) for connection_node in connection_nodes: for hop in self._node_chain(connection_node): try: addresses = self._resolve_proxy_addresses(hop) except OSError as exc: raise TunBackendError(f"无法解析 HTTP CONNECT 节点地址: {exc}") from exc if not addresses: raise TunBackendError("无法解析 HTTP CONNECT 节点地址") self._add_node_routes(addresses) proxy_addresses_by_node[self._node_key(hop)] = addresses first_hop = self._node_chain(node)[0] proxy_addresses = proxy_addresses_by_node.get(self._node_key(first_hop), []) if not proxy_addresses: raise TunBackendError("无法解析 HTTP CONNECT 节点地址") for route_node in route_nodes or (): if not isinstance(route_node, dict): continue route_value = route_node.get("node") if isinstance(route_node.get("node"), dict) else route_node for hop in self._node_chain(route_value): addresses = self._resolve_proxy_addresses(hop) proxy_addresses_by_node[self._node_key(hop)] = addresses if not source_address: self.log("警告:未找到物理网卡 IPv4,节点连接可能无法绕过 TUN") bridge = Socks5Bridge( node, bypass_lan=bypass_lan, route_rules=route_rules, log=self.detail_log, source_address=source_address or None, proxy_addresses=proxy_addresses, direct_target_resolver=direct_target_resolver, route_rule_resolver=route_rule_resolver, route_node_resolver=route_node_resolver, proxy_addresses_by_node=proxy_addresses_by_node, direct_route_ensurer=self._ensure_physical_route, ) socks_port = bridge.start() except Exception: self._remove_routes() raise self._bridge = bridge command = [str(engine), "--device", self._device, "--proxy", f"socks5://127.0.0.1:{socks_port}"] # tun2socks 的 Windows socket 选项会把 UDP/TCP socket 绑定到 # --interface 指定的网卡;本项目的 SOCKS relay 在本机回环地址, # 绑定 WLAN 会让 UDP 回环投递失效。Windows 官方用法也不传该参数。 # 节点连接已经通过物理 /32 路由和 source_address 绕过 TUN, # 因此 Windows 不需要 --interface;Linux/macOS 仍保留原行为。 if interface and os.name != "nt": command.extend(["--interface", interface]) self.log(f"本地 SOCKS5 桥接: 127.0.0.1:{socks_port}") try: self._process = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding="utf-8", errors="replace", creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), ) threading.Thread(target=self._read_output, args=(self._process,), daemon=True).start() time.sleep(0.5) if self._process.poll() is not None: raise TunBackendError(f"tun2socks 启动失败,退出码 {self._process.returncode}") self._configure_routes() self._write_state() self.log(f"虚拟网卡 TUN 已启动,进程 PID={self._process.pid}") except Exception: bridge.stop() self._remove_routes() self._bridge = None process = self._process self._process = None if process is not None and process.poll() is None: process.terminate() raise def stop(self) -> None: with self._lock: process = self._process bridge = self._bridge self._process = None self._bridge = None self._remove_routes() if process is not None and process.poll() is None: process.terminate() try: process.wait(timeout=10) except subprocess.TimeoutExpired: process.kill() process.wait(timeout=5) if bridge is not None: bridge.stop() if process is not None: self.log("虚拟网卡 TUN 已停止") def status(self) -> dict[str, Any]: with self._lock: process = self._process if process is None: return {"running": False, "pid": None, "exit_code": None} code = process.poll() if code is not None: self._process = None self._remove_routes() bridge = self._bridge self._bridge = None if bridge is not None: bridge.stop() return {"running": False, "pid": process.pid, "exit_code": code} return {"running": True, "pid": process.pid, "exit_code": None} def traffic(self) -> tuple[int, int, int, int]: if self._bridge is None: return 0, 0, 0, 0 return self._bridge.traffic()