feat(lite): add opt-in tun ipv6 mode

This commit is contained in:
RoyirinRosmontis
2026-09-19 04:20:34 +08:00
parent 2315735979
commit 9d5bc527d3
4 changed files with 80 additions and 19 deletions
+13
View File
@@ -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),
}
+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)
+33 -17
View File
@@ -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]] = {}
+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(