feat(lite): add opt-in tun ipv6 mode
This commit is contained in:
@@ -1312,6 +1312,15 @@ def _set_tun_dns_servers(value):
|
|||||||
data["tun_dns"] = list(parsed)
|
data["tun_dns"] = list(parsed)
|
||||||
_settings_save(data)
|
_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():
|
def _use_system_pac():
|
||||||
"""只决定 Windows 系统代理是否使用 PAC;旧设置按原行为读取。"""
|
"""只决定 Windows 系统代理是否使用 PAC;旧设置按原行为读取。"""
|
||||||
data = _settings_load()
|
data = _settings_load()
|
||||||
@@ -2632,6 +2641,7 @@ def _settings_services():
|
|||||||
get_port=lambda: _TLS_LOCAL_PORT,
|
get_port=lambda: _TLS_LOCAL_PORT,
|
||||||
get_system_pac=_use_system_pac,
|
get_system_pac=_use_system_pac,
|
||||||
get_dns=_tun_dns_servers,
|
get_dns=_tun_dns_servers,
|
||||||
|
get_ipv6_enabled=_tun_ipv6_enabled,
|
||||||
get_update_via_proxy=_update_via_proxy,
|
get_update_via_proxy=_update_via_proxy,
|
||||||
get_skip_update_check=_skip_update_check,
|
get_skip_update_check=_skip_update_check,
|
||||||
get_node_display_mode=_node_display_mode,
|
get_node_display_mode=_node_display_mode,
|
||||||
@@ -2640,6 +2650,7 @@ def _settings_services():
|
|||||||
set_port=_set_local_port,
|
set_port=_set_local_port,
|
||||||
set_system_pac=_set_use_system_pac,
|
set_system_pac=_set_use_system_pac,
|
||||||
set_dns=_set_tun_dns_servers,
|
set_dns=_set_tun_dns_servers,
|
||||||
|
set_ipv6_enabled=_set_tun_ipv6_enabled,
|
||||||
set_update_via_proxy=_set_update_via_proxy,
|
set_update_via_proxy=_set_update_via_proxy,
|
||||||
set_skip_update_check=_set_skip_update_check,
|
set_skip_update_check=_set_skip_update_check,
|
||||||
set_node_display_mode=_set_node_display_mode,
|
set_node_display_mode=_set_node_display_mode,
|
||||||
@@ -2722,6 +2733,7 @@ def _start_tun_backend(backend, payload):
|
|||||||
bypass_lan=bypass_lan,
|
bypass_lan=bypass_lan,
|
||||||
route_rules=rules,
|
route_rules=rules,
|
||||||
dns_servers=payload["dns_servers"],
|
dns_servers=payload["dns_servers"],
|
||||||
|
ipv6_enabled=bool(payload.get("ipv6_enabled", False)),
|
||||||
direct_target_resolver=direct_target,
|
direct_target_resolver=direct_target,
|
||||||
route_rule_resolver=user_rule,
|
route_rule_resolver=user_rule,
|
||||||
route_node_resolver=route_node,
|
route_node_resolver=route_node,
|
||||||
@@ -2748,6 +2760,7 @@ def _tun_start_payload(use_pac, bypass_lan):
|
|||||||
"route_rules": _route_rules(),
|
"route_rules": _route_rules(),
|
||||||
"route_nodes": route_nodes,
|
"route_nodes": route_nodes,
|
||||||
"dns_servers": _tun_dns_servers(),
|
"dns_servers": _tun_dns_servers(),
|
||||||
|
"ipv6_enabled": _tun_ipv6_enabled(),
|
||||||
"bypass_lan": bool(bypass_lan),
|
"bypass_lan": bool(bypass_lan),
|
||||||
"smart": bool(use_pac),
|
"smart": bool(use_pac),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -587,6 +587,20 @@ class Socks5Bridge:
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
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:
|
def _open_target(self, target: Address) -> socket.socket:
|
||||||
policy_host = self._policy_host(target.host)
|
policy_host = self._policy_host(target.host)
|
||||||
rule = self._route_rule(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 rule and str(rule.get("action") or "").lower() == "node":
|
||||||
if route_node is None:
|
if route_node is None:
|
||||||
raise ConnectionError("域名路由指定的节点不可用")
|
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)
|
direct = self._direct_target_for(target.host, target.port, policy_host)
|
||||||
if direct is not None:
|
if direct is not None:
|
||||||
self._ensure_direct_routes(direct[0], direct[1])
|
self._ensure_direct_routes(direct[0], direct[1])
|
||||||
return socket.create_connection(direct, 15)
|
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):
|
def _tcp(self, client: socket.socket, target: Address):
|
||||||
upstream = self._open_target(target)
|
upstream = self._open_target(target)
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class TunBackend:
|
|||||||
self._ipv6_address = "fd42:7272:5050::1"
|
self._ipv6_address = "fd42:7272:5050::1"
|
||||||
self._dns_configured = False
|
self._dns_configured = False
|
||||||
self._tun_dns_servers = ("8.8.8.8", "1.1.1.1")
|
self._tun_dns_servers = ("8.8.8.8", "1.1.1.1")
|
||||||
|
self._ipv6_enabled = False
|
||||||
self._bypass_lan = True
|
self._bypass_lan = True
|
||||||
self._cleanup_pending = False
|
self._cleanup_pending = False
|
||||||
self._state_path = self.user_dir / "tun_state.json"
|
self._state_path = self.user_dir / "tun_state.json"
|
||||||
@@ -251,6 +252,10 @@ class TunBackend:
|
|||||||
continue
|
continue
|
||||||
self._node_routes.append(address)
|
self._node_routes.append(address)
|
||||||
self._write_state()
|
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:
|
if candidates_v6 and not self._physical_ipv6_ifindex:
|
||||||
# 双栈节点仍可使用 IPv4 地址;只有 IPv6-only 节点需要物理 IPv6 出口。
|
# 双栈节点仍可使用 IPv4 地址;只有 IPv6-only 节点需要物理 IPv6 出口。
|
||||||
if candidates_v4:
|
if candidates_v4:
|
||||||
@@ -289,6 +294,8 @@ class TunBackend:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return
|
return
|
||||||
if parsed.version == 6:
|
if parsed.version == 6:
|
||||||
|
if not self._ipv6_enabled:
|
||||||
|
raise TunBackendError("IPv6 模式未启用,无法添加 IPv6 物理路由")
|
||||||
normalized = parsed.compressed
|
normalized = parsed.compressed
|
||||||
if normalized in self._node_ipv6_routes:
|
if normalized in self._node_ipv6_routes:
|
||||||
return
|
return
|
||||||
@@ -400,14 +407,15 @@ class TunBackend:
|
|||||||
f"{self._device}", self._gateway, "metric=1"])
|
f"{self._device}", self._gateway, "metric=1"])
|
||||||
self._route_added = True
|
self._route_added = True
|
||||||
self._write_state()
|
self._write_state()
|
||||||
self._netsh(["interface", "ipv6", "set", "address",
|
if self._ipv6_enabled:
|
||||||
f"interface={self._device}", f"address={self._ipv6_address}",
|
self._netsh(["interface", "ipv6", "set", "address",
|
||||||
"store=active"])
|
f"interface={self._device}", f"address={self._ipv6_address}",
|
||||||
self._netsh(["interface", "ipv6", "add", "route", "prefix=::/0",
|
"store=active"])
|
||||||
f"interface={self._device}", f"nexthop={self._ipv6_gateway}",
|
self._netsh(["interface", "ipv6", "add", "route", "prefix=::/0",
|
||||||
"metric=1"])
|
f"interface={self._device}", f"nexthop={self._ipv6_gateway}",
|
||||||
self._ipv6_route_added = True
|
"metric=1"])
|
||||||
self._write_state()
|
self._ipv6_route_added = True
|
||||||
|
self._write_state()
|
||||||
if not self._bypass_lan:
|
if not self._bypass_lan:
|
||||||
# Windows 物理网卡通常已有更具体的私网路由;补同前缀的 TUN
|
# Windows 物理网卡通常已有更具体的私网路由;补同前缀的 TUN
|
||||||
# 路由,确保“内网也代理”不会在进入 tun2socks 前被物理网卡截走。
|
# 路由,确保“内网也代理”不会在进入 tun2socks 前被物理网卡截走。
|
||||||
@@ -417,12 +425,13 @@ class TunBackend:
|
|||||||
f"{self._device}", self._gateway, "metric=1"])
|
f"{self._device}", self._gateway, "metric=1"])
|
||||||
self._lan_routes.append(network)
|
self._lan_routes.append(network)
|
||||||
self._write_state()
|
self._write_state()
|
||||||
for network in ("fc00::/7",):
|
if self._ipv6_enabled:
|
||||||
self._netsh(["interface", "ipv6", "add", "route", f"prefix={network}",
|
for network in ("fc00::/7",):
|
||||||
f"interface={self._device}", f"nexthop={self._ipv6_gateway}",
|
self._netsh(["interface", "ipv6", "add", "route", f"prefix={network}",
|
||||||
"metric=1"])
|
f"interface={self._device}", f"nexthop={self._ipv6_gateway}",
|
||||||
self._lan_ipv6_routes.append(network)
|
"metric=1"])
|
||||||
self._write_state()
|
self._lan_ipv6_routes.append(network)
|
||||||
|
self._write_state()
|
||||||
self._configure_dns()
|
self._configure_dns()
|
||||||
|
|
||||||
def _configure_dns(self) -> None:
|
def _configure_dns(self) -> None:
|
||||||
@@ -430,7 +439,7 @@ class TunBackend:
|
|||||||
if os.name != "nt":
|
if os.name != "nt":
|
||||||
return
|
return
|
||||||
ipv4 = [item for item in self._tun_dns_servers if ":" not in item]
|
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:
|
if ipv4:
|
||||||
self._netsh([
|
self._netsh([
|
||||||
"interface", "ipv4", "set", "dnsservers",
|
"interface", "ipv4", "set", "dnsservers",
|
||||||
@@ -531,6 +540,7 @@ class TunBackend:
|
|||||||
"node_ipv6_routes": list(self._node_ipv6_routes),
|
"node_ipv6_routes": list(self._node_ipv6_routes),
|
||||||
"route_added": bool(self._route_added),
|
"route_added": bool(self._route_added),
|
||||||
"ipv6_route_added": bool(self._ipv6_route_added),
|
"ipv6_route_added": bool(self._ipv6_route_added),
|
||||||
|
"ipv6_enabled": bool(self._ipv6_enabled),
|
||||||
"dns_configured": bool(self._dns_configured),
|
"dns_configured": bool(self._dns_configured),
|
||||||
"dns_servers": list(self._tun_dns_servers),
|
"dns_servers": list(self._tun_dns_servers),
|
||||||
"cleanup_pending": bool(self._cleanup_pending),
|
"cleanup_pending": bool(self._cleanup_pending),
|
||||||
@@ -587,6 +597,7 @@ class TunBackend:
|
|||||||
self._gateway = str(state.get("gateway") or self._gateway)
|
self._gateway = str(state.get("gateway") or self._gateway)
|
||||||
self._ipv6_address = str(state.get("ipv6_address") or self._ipv6_address)
|
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_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_gateway = str(state.get("physical_ipv6_gateway") or "")
|
||||||
self._physical_ipv6_ifindex = str(state.get("physical_ipv6_ifindex") or "")
|
self._physical_ipv6_ifindex = str(state.get("physical_ipv6_ifindex") or "")
|
||||||
self._route_added = bool(state.get("route_added"))
|
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,
|
def start(self, node: dict[str, Any], *, bypass_lan: bool = True,
|
||||||
route_rules: list[dict[str, Any]] | None = None,
|
route_rules: list[dict[str, Any]] | None = None,
|
||||||
dns_servers: tuple[str, ...] | list[str] | 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,
|
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_rule_resolver: Callable[[str], dict[str, Any] | None] | None = None,
|
||||||
route_node_resolver: Callable[[dict[str, Any]], 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():
|
if not self.is_admin():
|
||||||
raise TunBackendError("启动虚拟网卡 TUN 需要管理员权限")
|
raise TunBackendError("启动虚拟网卡 TUN 需要管理员权限")
|
||||||
self._bypass_lan = bool(bypass_lan)
|
self._bypass_lan = bool(bypass_lan)
|
||||||
|
self._ipv6_enabled = bool(ipv6_enabled)
|
||||||
if dns_servers is not None:
|
if dns_servers is not None:
|
||||||
cleaned = tuple(str(item).strip() for item in dns_servers if str(item).strip())
|
cleaned = tuple(str(item).strip() for item in dns_servers if str(item).strip())
|
||||||
if not cleaned:
|
if not cleaned:
|
||||||
@@ -651,10 +664,13 @@ class TunBackend:
|
|||||||
interface = self._physical_interface()
|
interface = self._physical_interface()
|
||||||
source_address = self._physical_source_address(interface)
|
source_address = self._physical_source_address(interface)
|
||||||
self._physical_gateway, self._physical_ifindex = self._physical_route_info()
|
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:
|
if not self._physical_ifindex:
|
||||||
raise TunBackendError("找不到物理默认路由,无法保证代理节点不进入 TUN 回环")
|
raise TunBackendError("找不到物理默认路由,无法保证代理节点不进入 TUN 回环")
|
||||||
if not self._physical_ipv6_ifindex:
|
if self._ipv6_enabled and not self._physical_ipv6_ifindex:
|
||||||
self.log("警告:未找到物理 IPv6 默认路由,IPv6 目标将仅尝试通过 IPv4 代理节点转发")
|
self.log("警告:未找到物理 IPv6 默认路由,IPv6 目标将仅尝试通过 IPv4 代理节点转发")
|
||||||
try:
|
try:
|
||||||
proxy_addresses_by_node: dict[str, list[tuple]] = {}
|
proxy_addresses_by_node: dict[str, list[tuple]] = {}
|
||||||
|
|||||||
@@ -933,6 +933,7 @@ class SettingsServices:
|
|||||||
get_port: object
|
get_port: object
|
||||||
get_system_pac: object
|
get_system_pac: object
|
||||||
get_dns: object
|
get_dns: object
|
||||||
|
get_ipv6_enabled: object
|
||||||
get_update_via_proxy: object
|
get_update_via_proxy: object
|
||||||
get_skip_update_check: object
|
get_skip_update_check: object
|
||||||
get_node_display_mode: object
|
get_node_display_mode: object
|
||||||
@@ -941,6 +942,7 @@ class SettingsServices:
|
|||||||
set_port: object
|
set_port: object
|
||||||
set_system_pac: object
|
set_system_pac: object
|
||||||
set_dns: object
|
set_dns: object
|
||||||
|
set_ipv6_enabled: object
|
||||||
set_update_via_proxy: object
|
set_update_via_proxy: object
|
||||||
set_skip_update_check: object
|
set_skip_update_check: object
|
||||||
set_node_display_mode: object
|
set_node_display_mode: object
|
||||||
@@ -1114,6 +1116,18 @@ class SettingsDialog(QDialog):
|
|||||||
dns_row.addWidget(self.ed_dns)
|
dns_row.addWidget(self.ed_dns)
|
||||||
left_col.addLayout(dns_row)
|
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_row = QHBoxLayout()
|
||||||
update_label = QLabel("使用代理更新")
|
update_label = QLabel("使用代理更新")
|
||||||
update_label.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 12px;")
|
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.ed_port.setText(str(self._services.get_port()))
|
||||||
self.cb_system_pac.setChecked(self._services.get_system_pac())
|
self.cb_system_pac.setChecked(self._services.get_system_pac())
|
||||||
self.ed_dns.setText(", ".join(self._services.get_dns()))
|
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_update_proxy.setChecked(self._services.get_update_via_proxy())
|
||||||
self.cb_skip_update.setChecked(self._services.get_skip_update_check())
|
self.cb_skip_update.setChecked(self._services.get_skip_update_check())
|
||||||
self.btn_node_domain.setChecked(True)
|
self.btn_node_domain.setChecked(True)
|
||||||
@@ -1287,6 +1303,8 @@ class SettingsDialog(QDialog):
|
|||||||
return
|
return
|
||||||
self._services.set_port(port)
|
self._services.set_port(port)
|
||||||
self._services.set_system_pac(bool(self.cb_system_pac.isChecked()))
|
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_update_via_proxy(bool(self.cb_update_proxy.isChecked()))
|
||||||
self._services.set_skip_update_check(bool(self.cb_skip_update.isChecked()))
|
self._services.set_skip_update_check(bool(self.cb_skip_update.isChecked()))
|
||||||
self._services.set_node_display_mode(
|
self._services.set_node_display_mode(
|
||||||
|
|||||||
Reference in New Issue
Block a user