Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41204df78a | ||
|
|
d0e739eff1 | ||
|
|
e406d614b7 | ||
|
|
357fd26a49 | ||
|
|
9d5bc527d3 | ||
|
|
2315735979 | ||
|
|
889dcb88e5 | ||
|
|
9224ad33fe |
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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":
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user