From 9d5bc527d334cbe29908a61cd39315952ea94ac7 Mon Sep 17 00:00:00 2001 From: RoyirinRosmontis Date: Sat, 19 Sep 2026 04:20:34 +0800 Subject: [PATCH] feat(lite): add opt-in tun ipv6 mode --- lite/main.py | 13 +++++++++ lite/rrpp_core/socks_bridge.py | 18 ++++++++++-- lite/rrpp_core/tun_backend.py | 50 ++++++++++++++++++++++------------ lite/rrpp_ui/dialogs.py | 18 ++++++++++++ 4 files changed, 80 insertions(+), 19 deletions(-) diff --git a/lite/main.py b/lite/main.py index 0e68efb..fcb823a 100644 --- a/lite/main.py +++ b/lite/main.py @@ -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() @@ -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, @@ -2722,6 +2733,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 +2760,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), } diff --git a/lite/rrpp_core/socks_bridge.py b/lite/rrpp_core/socks_bridge.py index 9ecbf99..7d6c73a 100644 --- a/lite/rrpp_core/socks_bridge.py +++ b/lite/rrpp_core/socks_bridge.py @@ -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) diff --git a/lite/rrpp_core/tun_backend.py b/lite/rrpp_core/tun_backend.py index 739ae78..cc2791b 100644 --- a/lite/rrpp_core/tun_backend.py +++ b/lite/rrpp_core/tun_backend.py @@ -53,6 +53,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" @@ -251,6 +252,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: @@ -289,6 +294,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 @@ -400,14 +407,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 +425,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 +439,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 +540,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 +597,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 +636,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,6 +647,7 @@ 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: @@ -651,10 +664,13 @@ class TunBackend: 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]] = {} diff --git a/lite/rrpp_ui/dialogs.py b/lite/rrpp_ui/dialogs.py index 8a8c810..5bd3f0b 100644 --- a/lite/rrpp_ui/dialogs.py +++ b/lite/rrpp_ui/dialogs.py @@ -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(