Compare commits

..
7 changed files with 164 additions and 67 deletions
+2
View File
@@ -26,6 +26,8 @@ call ..\.venv\Scripts\python.exe -m nuitka ^
--include-data-files=runtime/LICENSE.tun2socks.txt=LICENSE.tun2socks.txt ^
--include-data-files=runtime/LICENSE.wintun.txt=LICENSE.wintun.txt ^
--include-data-files=runtime/README.txt=README.txt ^
--include-data-files=runtime/tun2socks.exe=tun2socks.exe ^
--include-data-files=runtime/wintun.dll=wintun.dll ^
--windows-icon-from-ico=ico.ico ^
--windows-console-mode=disable ^
--company-name="Global Rosmanka Cloud Nexus Inc" ^
+1 -1
View File
@@ -1,5 +1,5 @@
{
"version": "999909190333",
"version": "999909190441",
"pac_base_url": "https://cdn.rrrosmontis.icu/RRPP/PAC/",
"nodes_url": "https://api.rrrosmontis.icu/v1/rrpp/subscription?method=rrpp_client",
"update_url": "https://git.rrrosmontis.icu/api/v1/repos/RoyirinRosmontis/Private-Proxy/releases/latest"
+29 -15
View File
@@ -1056,7 +1056,7 @@ def _verify_proxy_connectivity(timeout=10, opener=None, cancel_event=None):
response.read(1)
_raise_if_start_cancelled(cancel_event)
results[name] = True
log(f" {name} 可达")
log_detail(f" {name} 可达")
except _StartCancelled:
raise
except Exception as e:
@@ -1064,7 +1064,7 @@ def _verify_proxy_connectivity(timeout=10, opener=None, cancel_event=None):
# 这不是“开启失败”,也不能在稍后把界面重新切成已开启。
_raise_if_start_cancelled(cancel_event)
results[name] = False
log(f" {name} 不可达: {e}")
log_detail(f" {name} 不可达: {e}")
if required:
raise RuntimeError(f"境外隧道验证失败({name}):{e}") from e
return results
@@ -1116,7 +1116,7 @@ def _tls_start(remote_host, remote_port=443, use_tls=True, bypass_lan=False, sma
svr.close()
svr = None
if attempt < 2:
log(f"端口 {_TLS_LOCAL_PORT} 绑定失败: {e},1 秒后重试 ({attempt + 1}/3)")
log_detail(f"端口 {_TLS_LOCAL_PORT} 绑定失败: {e},1 秒后重试 ({attempt + 1}/3)")
time.sleep(1)
if svr is None:
raise last_err
@@ -1145,7 +1145,7 @@ def _tls_start(remote_host, remote_port=443, use_tls=True, bypass_lan=False, sma
if not quiet:
route = "→".join(str(node.get("country") or "?") for node in (jump_chain or []))
suffix = f" [{route}]" if route else ""
log(f"本地转发器已启动 ({proto} 127.0.0.1:{_TLS_LOCAL_PORT} -> {remote_host}:{remote_port}{suffix})")
log_detail(f"本地转发器已启动 ({proto} 127.0.0.1:{_TLS_LOCAL_PORT} -> {remote_host}:{remote_port}{suffix})")
def _inject_proxy_auth(data: bytes, proxy_node=None) -> bytes:
"""在 HTTP 代理请求头中注入 Authorization(如果尚未存在)。
@@ -1312,6 +1312,15 @@ def _set_tun_dns_servers(value):
data["tun_dns"] = list(parsed)
_settings_save(data)
def _tun_ipv6_enabled():
"""TUN IPv6 默认关闭,避免未验证的 IPv6 路径接管系统流量。"""
return bool(_settings_load().get("tun_ipv6_enabled", False))
def _set_tun_ipv6_enabled(value):
data = _settings_load()
data["tun_ipv6_enabled"] = bool(value)
_settings_save(data)
def _use_system_pac():
"""只决定 Windows 系统代理是否使用 PAC;旧设置按原行为读取。"""
data = _settings_load()
@@ -2250,7 +2259,7 @@ def _connect_route(direct_target, allow_direct_fallback, remote_host, remote_por
except OSError as e:
if not allow_direct_fallback:
raise
log(f"[转发器] 国内直连 {direct_target[0]} 失败,回退上游隧道: {e}")
log_detail(f"[转发器] 国内直连 {direct_target[0]} 失败,回退上游隧道: {e}")
if jump_chain:
if not use_tls:
raise RuntimeError("跳板模式仅支持 TLS 上游")
@@ -2372,7 +2381,7 @@ def _tls_relay(client, remote_host, remote_port, ctx, use_tls, bypass_lan=False,
else:
log_detail(f"[连接 #{connection_id}] 无法解析目标: {first_line or '空请求'},客户端={peer_text}")
except Exception as e:
log(f"[转发器] 读取首包失败: {e}")
log_detail(f"[转发器] 读取首包失败: {e}")
log_detail(f"[连接 #{connection_id}] 失败: 读取首包,目标={target_text},规则匹配={rule_match},错误={e}")
client.close()
return
@@ -2405,7 +2414,7 @@ def _tls_relay(client, remote_host, remote_port, ctx, use_tls, bypass_lan=False,
if via_tunnel:
_traffic_add(up=len(buf))
except Exception as e:
log(f"[转发器] 连接目标失败: {e}")
log_detail(f"[转发器] 连接目标失败: {e}")
log_detail(f"[连接 #{connection_id}] 连接失败: 目标={target_text},规则匹配={rule_match},路径={route_summary},错误={e}")
try:
client.close()
@@ -2632,6 +2641,7 @@ def _settings_services():
get_port=lambda: _TLS_LOCAL_PORT,
get_system_pac=_use_system_pac,
get_dns=_tun_dns_servers,
get_ipv6_enabled=_tun_ipv6_enabled,
get_update_via_proxy=_update_via_proxy,
get_skip_update_check=_skip_update_check,
get_node_display_mode=_node_display_mode,
@@ -2640,6 +2650,7 @@ def _settings_services():
set_port=_set_local_port,
set_system_pac=_set_use_system_pac,
set_dns=_set_tun_dns_servers,
set_ipv6_enabled=_set_tun_ipv6_enabled,
set_update_via_proxy=_set_update_via_proxy,
set_skip_update_check=_set_skip_update_check,
set_node_display_mode=_set_node_display_mode,
@@ -2680,7 +2691,8 @@ def _make_tun_backend(emit):
engine = Path(BASE_DIR) / "runtime" / "tun2socks.exe"
if not engine.is_file() or not (engine.parent / "wintun.dll").is_file():
raise RuntimeError("单文件包中的 tun2socks.exe 或 wintun.dll 缺失")
return TunBackend(_USER_DIR, engine_path=engine, log=emit, detail_log=emit)
detail_emit = getattr(emit, "detail", emit)
return TunBackend(_USER_DIR, engine_path=engine, log=emit, detail_log=detail_emit)
def _start_tun_backend(backend, payload):
@@ -2722,6 +2734,7 @@ def _start_tun_backend(backend, payload):
bypass_lan=bypass_lan,
route_rules=rules,
dns_servers=payload["dns_servers"],
ipv6_enabled=bool(payload.get("ipv6_enabled", False)),
direct_target_resolver=direct_target,
route_rule_resolver=user_rule,
route_node_resolver=route_node,
@@ -2748,6 +2761,7 @@ def _tun_start_payload(use_pac, bypass_lan):
"route_rules": _route_rules(),
"route_nodes": route_nodes,
"dns_servers": _tun_dns_servers(),
"ipv6_enabled": _tun_ipv6_enabled(),
"bypass_lan": bool(bypass_lan),
"smart": bool(use_pac),
}
@@ -2770,7 +2784,7 @@ class MainWindow(QWidget):
super().__init__()
self._proxy_set_by_us = False # 系统代理是否由本程序开启(退出时只清理自己设置的)
self._mode = _proxy_mode()
self._tun_controller = TunModeController(_tun_worker_command(), log)
self._tun_controller = TunModeController(_tun_worker_command(), log, log_detail)
self._quitting = False
self._tray = None
self._tray_menu = None
@@ -3211,11 +3225,11 @@ class MainWindow(QWidget):
_traffic_reset()
current_node = _current_node()
jump_chain = _jump_chain(current_node)
log("TLS 多跳模式" if jump_chain else "TLS 直连模式")
log_detail("TLS 多跳模式" if jump_chain else "TLS 直连模式")
ep = _endpoint_domain()
log(f" 目标域名: {ep}")
log_detail(f" 目标域名: {ep}")
if jump_chain:
log(f" 跳板路径: {_jump_route_text(current_node)}")
log_detail(f" 跳板路径: {_jump_route_text(current_node)}")
# PAC 模式:全部流量进转发器,按域名表/解析 IP 智能分流
use_internal_pac = _should_use_internal_pac(use_pac)
if use_internal_pac:
@@ -3236,7 +3250,7 @@ class MainWindow(QWidget):
# 在内存中启动本地 PAC HTTP 服务,不往 EXE 目录写任何文件
pac_port = _start_pac_server(pac_content)
pac_url = f"http://127.0.0.1:{pac_port}/proxy.pac"
log(f"使用 PAC: {pac_url}")
log_detail(f"使用 PAC: {pac_url}")
with self._proxy_state_lock:
_raise_if_start_cancelled(cancel_event)
proxy_on(pac_url=pac_url)
@@ -3247,10 +3261,10 @@ class MainWindow(QWidget):
proxy_on(bypass_lan=use_bypass_lan)
self._proxy_set_by_us = True
if use_internal_pac:
log(f"使用内置 PAC:系统固定代理 127.0.0.1:{PROXY_PORT},转发器内部智能分流")
log_detail(f"使用内置 PAC:系统固定代理 127.0.0.1:{PROXY_PORT},转发器内部智能分流")
_raise_if_start_cancelled(cancel_event)
if use_bypass_lan:
log("不代理内网 IP:内网地址将直连")
log_detail("不代理内网 IP:内网地址将直连")
log("验证代理连通性...")
_verify_proxy_connectivity(cancel_event=cancel_event)
except Exception:
+16 -2
View File
@@ -587,6 +587,20 @@ class Socks5Bridge:
except OSError:
pass
def _proxy_target(self, target: Address) -> Address:
"""为上游 CONNECT 恢复唯一 DNS 域名,给 IPv6 目标提供 IPv4 回退。"""
try:
ipaddress.ip_address(target.host)
except ValueError:
return target
domain = self._dns.lookup_ip(target.host)
if not domain:
return target
self.log(
f"[桥接] IPv6 目标回退域名解析: {target.host} -> {domain}:{target.port}"
)
return Address(domain, target.port)
def _open_target(self, target: Address) -> socket.socket:
policy_host = self._policy_host(target.host)
rule = self._route_rule(policy_host, target.host)
@@ -594,12 +608,12 @@ class Socks5Bridge:
if rule and str(rule.get("action") or "").lower() == "node":
if route_node is None:
raise ConnectionError("域名路由指定的节点不可用")
return self._dialer(route_node).open(target)
return self._dialer(route_node).open(self._proxy_target(target))
direct = self._direct_target_for(target.host, target.port, policy_host)
if direct is not None:
self._ensure_direct_routes(direct[0], direct[1])
return socket.create_connection(direct, 15)
return self._dialer(self.node).open(target)
return self._dialer(self.node).open(self._proxy_target(target))
def _tcp(self, client: socket.socket, target: Address):
upstream = self._open_target(target)
+62 -34
View File
@@ -27,6 +27,16 @@ class TunBackendError(RuntimeError):
"""TUN 后端无法启动或停止。"""
def _run_hidden(command, **kwargs):
"""运行 Windows 网络命令时隐藏控制台窗口。
源码通过 test.bat 启动时这些子进程复用已有控制台;Nuitka GUI 没有父控制台,
未设置 CREATE_NO_WINDOW 就会为每次 PowerShell 查询弹出一个窗口。
"""
kwargs.setdefault("creationflags", getattr(subprocess, "CREATE_NO_WINDOW", 0))
return subprocess.run(command, **kwargs)
class TunBackend:
def __init__(self, user_dir: str | Path, engine_path: str | Path | None = None,
log: Callable[[str], None] | None = None,
@@ -53,6 +63,7 @@ class TunBackend:
self._ipv6_address = "fd42:7272:5050::1"
self._dns_configured = False
self._tun_dns_servers = ("8.8.8.8", "1.1.1.1")
self._ipv6_enabled = False
self._bypass_lan = True
self._cleanup_pending = False
self._state_path = self.user_dir / "tun_state.json"
@@ -109,7 +120,7 @@ class TunBackend:
"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)
result = _run_hidden(command, capture_output=True, text=True, timeout=5)
value = (result.stdout or "").strip().splitlines()
if value and value[0].strip():
return value[0].strip()
@@ -126,7 +137,7 @@ class TunBackend:
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)
result = _run_hidden(command, capture_output=True, text=True, timeout=5)
value = (result.stdout or "").strip().splitlines()
if value and value[0].strip():
return value[0].strip()
@@ -143,7 +154,7 @@ class TunBackend:
"Sort-Object RouteMetric | Select-Object -First 1 "
"NextHop,InterfaceIndex | ConvertTo-Json -Compress"]
try:
result = subprocess.run(command, capture_output=True, text=True, timeout=5)
result = _run_hidden(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 "")
@@ -160,7 +171,7 @@ class TunBackend:
"Sort-Object RouteMetric | Select-Object -First 1 "
"NextHop,InterfaceIndex | ConvertTo-Json -Compress"]
try:
result = subprocess.run(command, capture_output=True, text=True, timeout=5)
result = _run_hidden(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 "")
@@ -181,7 +192,7 @@ class TunBackend:
"$_.IPAddress -notlike 'fd*' }).IPAddress | ConvertTo-Json -Compress",
]
try:
result = subprocess.run(command, capture_output=True, text=True, timeout=5)
result = _run_hidden(command, capture_output=True, text=True, timeout=5)
if result.returncode:
return None
value = json.loads((result.stdout or "").strip() or "[]")
@@ -241,7 +252,7 @@ class TunBackend:
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,
result = _run_hidden(command, capture_output=True, text=True,
encoding="mbcs", errors="replace", timeout=15)
if result.returncode:
detail = (result.stderr or result.stdout or "").strip()
@@ -251,6 +262,10 @@ class TunBackend:
continue
self._node_routes.append(address)
self._write_state()
if candidates_v6 and not self._ipv6_enabled:
if not candidates_v4:
raise TunBackendError("IPv6 模式已关闭,代理节点仅包含 IPv6 地址")
candidates_v6 = []
if candidates_v6 and not self._physical_ipv6_ifindex:
# 双栈节点仍可使用 IPv4 地址;只有 IPv6-only 节点需要物理 IPv6 出口。
if candidates_v4:
@@ -270,7 +285,7 @@ class TunBackend:
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,
result = _run_hidden(["netsh", *command], capture_output=True, text=True,
encoding="mbcs", errors="replace", timeout=15)
if result.returncode:
detail = (result.stderr or result.stdout or "").strip()
@@ -289,6 +304,8 @@ class TunBackend:
except ValueError:
return
if parsed.version == 6:
if not self._ipv6_enabled:
raise TunBackendError("IPv6 模式未启用,无法添加 IPv6 物理路由")
normalized = parsed.compressed
if normalized in self._node_ipv6_routes:
return
@@ -303,7 +320,7 @@ class TunBackend:
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,
result = _run_hidden(command, capture_output=True, text=True,
encoding="mbcs", errors="replace", timeout=15)
detail = (result.stderr or result.stdout or "").strip()
if result.returncode:
@@ -326,7 +343,7 @@ class TunBackend:
def _cleanup_netsh(self, args: list[str]) -> bool:
try:
result = subprocess.run(["netsh", *args], capture_output=True, text=True,
result = _run_hidden(["netsh", *args], capture_output=True, text=True,
encoding="mbcs", errors="replace", timeout=15)
except Exception as exc:
self.detail_log(f"[TUN] 清理 netsh 失败: {exc}")
@@ -341,7 +358,7 @@ class TunBackend:
remaining: list[str] = []
for address in self._node_routes:
try:
result = subprocess.run(["route", "DELETE", address], capture_output=True,
result = _run_hidden(["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}")
@@ -358,7 +375,7 @@ class TunBackend:
if self._physical_ipv6_ifindex:
command.append(f"interface={self._physical_ipv6_ifindex}")
try:
result = subprocess.run(["netsh", *command], capture_output=True, text=True,
result = _run_hidden(["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}")
@@ -372,7 +389,7 @@ class TunBackend:
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,
result = _run_hidden(["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()
@@ -381,7 +398,7 @@ class TunBackend:
def _configure_routes(self) -> None:
for _ in range(20):
try:
probe = subprocess.run(
probe = _run_hidden(
["powershell", "-NoProfile", "-Command",
f"Get-NetAdapter -Name '{self._device}' -ErrorAction SilentlyContinue"],
capture_output=True, text=True, timeout=3,
@@ -400,14 +417,15 @@ class TunBackend:
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 self._ipv6_enabled:
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 前被物理网卡截走。
@@ -417,12 +435,13 @@ class TunBackend:
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()
if self._ipv6_enabled:
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:
@@ -430,7 +449,7 @@ class TunBackend:
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]
ipv6 = [item for item in self._tun_dns_servers if ":" in item] if self._ipv6_enabled else []
if ipv4:
self._netsh([
"interface", "ipv4", "set", "dnsservers",
@@ -531,6 +550,7 @@ class TunBackend:
"node_ipv6_routes": list(self._node_ipv6_routes),
"route_added": bool(self._route_added),
"ipv6_route_added": bool(self._ipv6_route_added),
"ipv6_enabled": bool(self._ipv6_enabled),
"dns_configured": bool(self._dns_configured),
"dns_servers": list(self._tun_dns_servers),
"cleanup_pending": bool(self._cleanup_pending),
@@ -587,6 +607,7 @@ class TunBackend:
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._ipv6_enabled = bool(state.get("ipv6_enabled", bool(state.get("ipv6_route_added"))))
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"))
@@ -625,6 +646,7 @@ class TunBackend:
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,
ipv6_enabled: bool = False,
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,
@@ -635,24 +657,30 @@ class TunBackend:
if not self.is_admin():
raise TunBackendError("启动虚拟网卡 TUN 需要管理员权限")
self._bypass_lan = bool(bypass_lan)
self._ipv6_enabled = bool(ipv6_enabled)
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:
socket.inet_pton(socket.AF_INET, item)
except OSError as exc:
raise TunBackendError("当前 TUN 仅支持 IPv4 DNS 服务器") from exc
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 self._ipv6_enabled:
self._physical_ipv6_gateway, self._physical_ipv6_ifindex = self._physical_ipv6_route_info()
else:
self._physical_ipv6_gateway = self._physical_ipv6_ifindex = ""
if not self._physical_ifindex:
raise TunBackendError("找不到物理默认路由,无法保证代理节点不进入 TUN 回环")
if not self._physical_ipv6_ifindex:
if self._ipv6_enabled and not self._physical_ipv6_ifindex:
self.log("警告:未找到物理 IPv6 默认路由,IPv6 目标将仅尝试通过 IPv4 代理节点转发")
try:
proxy_addresses_by_node: dict[str, list[tuple]] = {}
@@ -709,7 +737,7 @@ class TunBackend:
# 因此 Windows 不需要 --interface;Linux/macOS 仍保留原行为。
if interface and os.name != "nt":
command.extend(["--interface", interface])
self.log(f"本地 SOCKS5 桥接: 127.0.0.1:{socks_port}")
self.detail_log(f"本地 SOCKS5 桥接: 127.0.0.1:{socks_port}")
try:
self._process = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
+36 -15
View File
@@ -46,9 +46,11 @@ def _receive(reader) -> dict[str, Any]:
class TunModeController:
"""普通权限 GUI 仅在首次开启 TUN 时启动管理员工作进程。"""
def __init__(self, command: list[str], log: Callable[[str], None]):
def __init__(self, command: list[str], log: Callable[[str], None],
detail_log: Callable[[str], None] | None = None):
self.command = command
self.log = log
self.detail_log = detail_log or log
self._lock = threading.RLock()
self._socket: socket.socket | None = None
self._reader = None
@@ -124,17 +126,23 @@ class TunModeController:
def _request(self, command: str, payload: dict[str, Any] | None = None) -> Any:
if self._socket is None or self._reader is None:
raise TunModeError("TUN 工作进程未连接")
_send(self._socket, {"command": command, "payload": payload or {}})
while True:
message = _receive(self._reader)
if message.get("type") == "log":
self.log(str(message.get("message") or ""))
continue
if message.get("type") != "result":
raise TunModeError("TUN 工作进程响应无效")
if not message.get("ok"):
raise TunModeError(str(message.get("error") or "TUN 操作失败"))
return message.get("value")
try:
_send(self._socket, {"command": command, "payload": payload or {}})
while True:
message = _receive(self._reader)
if message.get("type") == "log":
callback = self.detail_log if message.get("detail") else self.log
callback(str(message.get("message") or ""))
continue
if message.get("type") != "result":
raise TunModeError("TUN 工作进程响应无效")
if not message.get("ok"):
raise TunModeError(str(message.get("error") or "TUN 操作失败"))
return message.get("value")
except TunModeError:
raise
except OSError as exc:
raise TunModeError(f"TUN 工作进程连接中止: {exc}") from exc
def status(self) -> dict[str, Any]:
with self._lock:
@@ -187,15 +195,24 @@ def run_tun_worker(port: int, token: str, backend_factory,
send_lock = threading.Lock()
_send(connection, {"type": "hello", "token": token}, send_lock)
def emit(message: str) -> None:
def emit(message: str, detail: bool = False) -> None:
try:
_send(connection, {"type": "log", "message": str(message)}, send_lock)
_send(connection, {"type": "log", "message": str(message),
"detail": bool(detail)}, send_lock)
except OSError:
pass
class _WorkerLogger:
def __call__(self, message: str) -> None:
emit(message)
def detail(self, message: str) -> None:
emit(message, detail=True)
logger = _WorkerLogger()
backend = None
try:
backend = backend_factory(emit)
while True:
try:
request = _receive(reader)
@@ -206,6 +223,10 @@ def run_tun_worker(port: int, token: str, backend_factory,
if command == "start":
if not isinstance(request.get("payload"), dict):
raise TunModeError("TUN 启动参数无效")
# 延迟创建后端,使初始化失败也能通过控制协议返回具体原因,
# 避免 GUI 只看到 Windows 的 10053 连接中止错误。
if backend is None:
backend = backend_factory(logger)
start_backend(backend, request["payload"])
value = None
elif command == "stop":
+18
View File
@@ -933,6 +933,7 @@ class SettingsServices:
get_port: object
get_system_pac: object
get_dns: object
get_ipv6_enabled: object
get_update_via_proxy: object
get_skip_update_check: object
get_node_display_mode: object
@@ -941,6 +942,7 @@ class SettingsServices:
set_port: object
set_system_pac: object
set_dns: object
set_ipv6_enabled: object
set_update_via_proxy: object
set_skip_update_check: object
set_node_display_mode: object
@@ -1114,6 +1116,18 @@ class SettingsDialog(QDialog):
dns_row.addWidget(self.ed_dns)
left_col.addLayout(dns_row)
ipv6_row = QHBoxLayout()
ipv6_label = QLabel("启用 IPv6 模式 (虚拟网卡)")
ipv6_label.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 12px;")
ipv6_label.setToolTip("TUN 模式安装 IPv6 地址和默认路由;默认关闭")
ipv6_row.addWidget(ipv6_label)
ipv6_row.addStretch()
get_ipv6_enabled = getattr(services, "get_ipv6_enabled", lambda: False)
self.cb_ipv6 = ToggleSwitch(checked=bool(get_ipv6_enabled()))
self.cb_ipv6.setToolTip("仅虚拟网卡模式使用;系统代理模式忽略此项")
ipv6_row.addWidget(self.cb_ipv6)
left_col.addLayout(ipv6_row)
update_row = QHBoxLayout()
update_label = QLabel("使用代理更新")
update_label.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 12px;")
@@ -1266,6 +1280,8 @@ class SettingsDialog(QDialog):
self.ed_port.setText(str(self._services.get_port()))
self.cb_system_pac.setChecked(self._services.get_system_pac())
self.ed_dns.setText(", ".join(self._services.get_dns()))
get_ipv6_enabled = getattr(self._services, "get_ipv6_enabled", lambda: False)
self.cb_ipv6.setChecked(bool(get_ipv6_enabled()))
self.cb_update_proxy.setChecked(self._services.get_update_via_proxy())
self.cb_skip_update.setChecked(self._services.get_skip_update_check())
self.btn_node_domain.setChecked(True)
@@ -1287,6 +1303,8 @@ class SettingsDialog(QDialog):
return
self._services.set_port(port)
self._services.set_system_pac(bool(self.cb_system_pac.isChecked()))
set_ipv6_enabled = getattr(self._services, "set_ipv6_enabled", lambda _value: None)
set_ipv6_enabled(bool(self.cb_ipv6.isChecked()))
self._services.set_update_via_proxy(bool(self.cb_update_proxy.isChecked()))
self._services.set_skip_update_check(bool(self.cb_skip_update.isChecked()))
self._services.set_node_display_mode(