feat(lite): merge TUN mode with IPv6 routing

This commit is contained in:
RoyirinRosmontis
2026-09-19 03:41:41 +08:00
parent 3e29012d9c
commit 50364dc17a
23 changed files with 2996 additions and 237 deletions
+272 -195
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import sys, os, subprocess, time, ctypes, threading, functools, base64, bisect, re, json, uuid
import sys, os, subprocess, time, ctypes, threading, functools, base64, bisect, re, json, uuid, ipaddress
from datetime import datetime
from pathlib import Path
@@ -13,7 +13,7 @@ from PyQt5.QtCore import Qt, QTimer, pyqtSignal
from PyQt5.QtGui import QFont, QIcon, QPixmap, QImage, QColor
from rrpp_ui.theme import C_TITLE, C_GREEN, C_YELLOW, C_RED, C_BG, C_CARD, C_BORDER, C_TEXT, C_SUBTEXT
from rrpp_ui.widgets import NodeLabel, StatusDot, ToggleSwitch
from rrpp_ui.widgets import NodeLabel, StatusDot, ToggleSwitch, ProxyModeCard
from rrpp_ui.dialogs import (
LoginDialog, LoginServices, SettingsDialog, SettingsServices, LogDialog, LogServices,
ThirdPartyDialog, ThirdPartyServices,
@@ -58,6 +58,9 @@ from rrpp_core.network_primitives import (
resolve_host_ips as _resolve_host_ips,
ttl_cache as _ttl_cache,
)
from rrpp_core.tun_backend import TunBackend
from rrpp_core.tun_mode import TunModeController, WORKER_FLAG, run_tun_worker
from rrpp_core.system_proxy_mode import SystemProxyMode, is_our_pac_url as _is_our_pac_url
# GUI 窗口在 96 DPI / 100% 缩放下的逻辑尺寸;启用 Qt 高分屏缩放后,
# Qt 会自动按 devicePixelRatio 放大/缩小,因此代码里保持逻辑尺寸不变即可。
@@ -1004,114 +1007,15 @@ def _run(cmd, warn=True):
log(f"[警告] {' '.join(str(c) for c in cmd)} -> {r.returncode} {err}")
return r
def _notify_proxy_change():
"""通知 WinINET 和顶层窗口代理设置已变更,让已运行程序立即重读。"""
try:
import ctypes
ctypes.windll.Wininet.InternetSetOptionW(None, 39, None, 0)
ctypes.windll.Wininet.InternetSetOptionW(None, 37, None, 0)
result = ctypes.c_ulong()
ctypes.windll.user32.SendMessageTimeoutW(
0xFFFF, 0x001A, 0,
ctypes.c_wchar_p("InternetSettings"),
0x0002, 5000, ctypes.byref(result))
except Exception:
pass
def _reg_query_value(name):
"""查询 HKCU Internet Settings 下指定值,返回 (类型, 数据);值不存在返回 None。"""
r = _run(["reg", "query", PROXY_REG, "/v", name], warn=False)
if r.returncode != 0:
return None
for line in (r.stdout or "").splitlines():
if name in line:
parts = line.strip().split(None, 2) # 形如 "ProxyServer REG_SZ 127.0.0.1:7897"
if len(parts) >= 3:
return parts[1], parts[2]
return None
def _is_our_pac_url(value: str) -> bool:
"""value 是否指向本程序的本地 PAC 服务(http://127.0.0.1:<端口>/proxy.pac)。"""
import re
return bool(re.fullmatch(r"http://127\.0\.0\.1:\d+/proxy\.pac", value.strip()))
_PROXY_BACKUP_KEY = "prev_proxy_state" # settings.json 里的系统代理原值备份
_proxy_prev = None # 运行期备份:None = 尚未备份
def _backup_proxy_state():
"""覆盖系统代理前把原值备份下来(内存 + settings.json,防崩溃丢失)。
只在尚未备份时执行:本次运行里第一次开启代理时的注册表状态才是用户
原有的(可能是其他代理软件的值),之后反复开/关不应覆盖首次备份。"""
global _proxy_prev
if _proxy_prev is not None:
return
state = {}
for name in ("ProxyEnable", "ProxyServer", "ProxyOverride", "AutoConfigURL"):
v = _reg_query_value(name)
state[name] = list(v) if v else None
_proxy_prev = state
try:
_settings_save({**_settings_load(), _PROXY_BACKUP_KEY: state})
except Exception:
pass # 持久化失败只影响崩溃后的恢复,不影响本次运行
def _restore_proxy_state():
"""恢复开启代理前的系统代理原值;没有备份时退化为清空(历史行为)。"""
global _proxy_prev
state = _proxy_prev
if state is None:
state = _settings_load().get(_PROXY_BACKUP_KEY)
if isinstance(state, dict):
for name in ("ProxyEnable", "ProxyServer", "ProxyOverride", "AutoConfigURL"):
v = state.get(name)
if v:
_run(["reg", "add", PROXY_REG, "/v", name, "/t", v[0], "/d", v[1], "/f"], warn=False)
else:
_run(["reg", "delete", PROXY_REG, "/v", name, "/f"], warn=False)
log("已恢复开启代理前的系统代理设置")
else:
# 无备份(旧版本升级后首次运行):清空,与历史行为一致
for name in ("ProxyServer", "ProxyOverride", "AutoConfigURL"):
_run(["reg", "delete", PROXY_REG, "/v", name, "/f"], warn=False)
_run(["reg", "add", PROXY_REG, "/v", "ProxyEnable", "/t", "REG_DWORD", "/d", "0", "/f"])
_proxy_prev = None
# 备份已消费:清除持久化副本,避免下次启动误恢复
try:
data = _settings_load()
if isinstance(data, dict) and _PROXY_BACKUP_KEY in data:
del data[_PROXY_BACKUP_KEY]
_settings_save(data)
except Exception:
pass
def proxy_on(pac_url=None, bypass_lan=False):
"""设置 Windows 系统代理。pac_url 不为空时启用 PAC 模式,否则使用固定代理。"""
_backup_proxy_state() # 覆盖前保存用户原有代理设置,关闭时恢复而不是清空
if pac_url:
# PAC 模式:仅设置 AutoConfigURL,ProxyEnable 置 0,
# 避免程序读到"固定代理已启用但服务器为空"的混乱状态
_run(["reg", "add", PROXY_REG, "/v", "ProxyEnable", "/t", "REG_DWORD", "/d", "0", "/f"])
# PAC 模式:清理固定代理遗留项,避免两种设置冲突
_run(["reg", "delete", PROXY_REG, "/v", "ProxyServer", "/f"], warn=False)
_run(["reg", "delete", PROXY_REG, "/v", "ProxyOverride", "/f"], warn=False)
_run(["reg", "add", PROXY_REG, "/v", "AutoConfigURL", "/t", "REG_SZ", "/d", pac_url, "/f"])
log("系统代理已设置 (PAC)")
else:
hp = f"{PROXY_HOST}:{PROXY_PORT}"
_run(["reg", "add", PROXY_REG, "/v", "ProxyEnable", "/t", "REG_DWORD", "/d", "1", "/f"])
_run(["reg", "add", PROXY_REG, "/v", "ProxyServer", "/t", "REG_SZ", "/d", hp, "/f"])
_run(["reg", "add", PROXY_REG, "/v", "ProxyOverride", "/t", "REG_SZ", "/d", _proxy_override(bypass_lan), "/f"])
log(f"系统代理已设置: {hp}")
_notify_proxy_change()
"""开启系统代理模式,复用主程序的本地转发器。"""
_SYSTEM_PROXY_MODE.enable(pac_url=pac_url, bypass_lan=bypass_lan)
def proxy_off():
"""关闭系统代理并停止 TLS 转发器 / PAC 本地服务。"""
log("正在关闭系统代理...")
_restore_proxy_state() # 恢复用户原有代理设置,而不是简单清空
_SYSTEM_PROXY_MODE.disable()
log("系统代理已关闭")
_notify_proxy_change()
_tls_stop()
_stop_pac_server()
# 打印本次转发流量统计
@@ -1344,6 +1248,13 @@ def _settings_save(data):
"""本地设置原子写回。"""
_SETTINGS_STORE.save(data)
_SYSTEM_PROXY_MODE = SystemProxyMode(
run=_run, settings_load=_settings_load, settings_save=_settings_save,
log=log, endpoint=lambda: (PROXY_HOST, PROXY_PORT),
proxy_override=lambda bypass_lan: _proxy_override(bypass_lan),
)
def _parse_port(text):
"""解析端口号;非法(非数字或超出 1-65535)返回 None。"""
try:
@@ -1370,23 +1281,65 @@ def _set_local_port(port):
_settings_save(data)
log(f"代理转发端口已保存: {port}(下次开启代理生效)")
def _use_builtin_pac():
"""是否使用固定本地代理承载系统流量、由转发器内部执行 PAC 判断。"""
return bool(_settings_load().get("builtin_pac", True))
_DEFAULT_TUN_DNS = ("8.8.8.8", "1.1.1.1")
def _set_builtin_pac(enabled):
"""保存内置 PAC 偏好,下一次开启代理时生效。"""
def _parse_dns_servers(value):
parts = value if isinstance(value, (list, tuple)) else re.split(r"[,;\s]+", str(value or "").strip())
result = []
for item in parts:
value = str(item).strip()
if not value:
continue
try:
address = ipaddress.ip_address(value)
except ValueError:
return None
if address.is_unspecified or address.is_multicast:
return None
normalized = address.compressed
if normalized not in result:
result.append(normalized)
return tuple(result) if 1 <= len(result) <= 3 else None
def _tun_dns_servers():
return _parse_dns_servers(_settings_load().get("tun_dns")) or _DEFAULT_TUN_DNS
def _set_tun_dns_servers(value):
parsed = _parse_dns_servers(value)
if parsed is None:
raise ValueError("DNS 应为 1-3 个有效 IPv4/IPv6 地址")
data = _settings_load()
data["builtin_pac"] = bool(enabled)
data["tun_dns"] = list(parsed)
_settings_save(data)
def _should_use_system_pac(use_pac, use_builtin_pac):
"""决定系统侧是否下发 PAC;内置 PAC 只保留转发器内部智能分流。"""
return bool(use_pac and not use_builtin_pac)
def _use_system_pac():
"""只决定 Windows 系统代理是否使用 PAC;旧设置按原行为读取。"""
data = _settings_load()
if "use_system_pac" in data:
return bool(data["use_system_pac"])
return bool(data.get("pac_mode", True) and not data.get("builtin_pac", True))
def _should_use_internal_pac(use_pac, use_builtin_pac):
"""决定转发器内部是否启用 PAC 智能直连/代理判断。"""
return bool(use_pac or use_builtin_pac)
def _set_use_system_pac(enabled):
data = _settings_load()
data["use_system_pac"] = bool(enabled)
_settings_save(data)
def _should_use_system_pac(use_pac, use_system_pac):
return bool(use_pac and use_system_pac)
def _should_use_internal_pac(use_pac, _legacy_builtin_pac=None):
"""智能分流由主界面 PAC 规则模式独立控制。"""
return bool(use_pac)
def _proxy_mode():
return "tun" if _settings_load().get("proxy_mode") == "tun" else "system"
def _set_proxy_mode(mode):
if mode not in ("system", "tun"):
raise ValueError("未知代理模式")
data = _settings_load()
data["proxy_mode"] = mode
_settings_save(data)
def _update_via_proxy():
"""更新检查/下载是否走节点隧道(CONNECT);设置项,默认关=直连仓库。"""
@@ -1488,10 +1441,16 @@ _ROUTE_RULES = None # 全量域名路由规则(含 direct/node)
def _route_domain_normalize(entry: str) -> str:
"""规范化域名规则,保留 ``*.`` 通配符,兼容旧白名单输入。"""
"""规范化域名或 IP/CIDR 规则,保留旧白名单的网段语义。"""
s = str(entry or "").strip().lower()
if "://" in s:
s = s.split("://", 1)[1]
if "/" in s:
try:
import ipaddress
return str(ipaddress.ip_network(s, strict=False))
except ValueError:
pass
s = s.split("/", 1)[0].rstrip(".")
wildcard = s.startswith("*.")
if wildcard:
@@ -1549,10 +1508,10 @@ def _route_rules():
return _ROUTE_RULES
def _route_rule_for_host(host: str):
def _route_rule_for_host(host: str, rules=None):
"""返回目标域名命中的最高优先级规则;规则变更对新连接立即生效。"""
matches = []
for rule in _route_rules():
for rule in (_route_rules() if rules is None else rules):
if not rule.get("enabled") or not _route_rule_match(host, rule.get("domain", "")):
continue
domain = rule["domain"]
@@ -1671,8 +1630,14 @@ def _whitelist_validate_entry(entry: str):
def _fixed_route_validate_domain(entry: str):
"""校验域名路由规则;支持 ``example.com`` 与 ``*.example.com``。"""
"""校验域名路由规则;兼容旧白名单的 IP/CIDR。"""
value = _route_domain_normalize(entry)
try:
import ipaddress
ipaddress.ip_network(value, strict=False)
return value, ""
except ValueError:
pass
base = value[2:] if value.startswith("*.") else value
if not base:
return "", "域名不能为空"
@@ -2542,12 +2507,8 @@ def _release_forwarder(started_here):
_tls_stop()
def is_proxy_on():
"""系统代理是否生效:固定代理已开启,或 AutoConfigURL 指向本地 PAC 服务。"""
v = _reg_query_value("ProxyEnable")
if v and "0x1" in (v[1] or ""):
return True
v = _reg_query_value("AutoConfigURL")
return bool(v) and _is_our_pac_url(v[1] or "")
"""仅报告本程序的系统代理,不把其它软件的代理当成 RRPP 状态。"""
return _SYSTEM_PROXY_MODE.is_on()
def _cleanup_stale_proxy():
"""清理上次异常退出残留的系统代理设置,并恢复用户原有设置。
@@ -2558,11 +2519,7 @@ def _cleanup_stale_proxy():
不能因为地址里带 127.0.0.1 就误删。
"""
try:
server = _reg_query_value("ProxyServer")
pac = _reg_query_value("AutoConfigURL")
ours = (bool(server) and (server[1] or "").strip() == f"{PROXY_HOST}:{PROXY_PORT}") or \
(bool(pac) and _is_our_pac_url(pac[1] or ""))
if not ours:
if not _SYSTEM_PROXY_MODE.is_stale():
return
proxy_off() # 内部从备份恢复用户原有设置(无备份则清空)
except Exception:
@@ -2673,14 +2630,16 @@ def _settings_services():
load_icon=load_icon,
version=VERSION,
get_port=lambda: _TLS_LOCAL_PORT,
get_builtin_pac=_use_builtin_pac,
get_system_pac=_use_system_pac,
get_dns=_tun_dns_servers,
get_update_via_proxy=_update_via_proxy,
get_skip_update_check=_skip_update_check,
get_node_display_mode=_node_display_mode,
get_close_behavior=_close_behavior,
parse_port=_parse_port,
set_port=_set_local_port,
set_builtin_pac=_set_builtin_pac,
set_system_pac=_set_use_system_pac,
set_dns=_set_tun_dns_servers,
set_update_via_proxy=_set_update_via_proxy,
set_skip_update_check=_set_skip_update_check,
set_node_display_mode=_set_node_display_mode,
@@ -2705,6 +2664,94 @@ def _jump_services():
remove_profile=_remove_jump_profile,
)
def _tun_worker_command():
"""同一个源码入口或单文件 EXE,以工作进程参数重新进入。"""
if "__compiled__" in globals():
return [str((_frozen_executable_dir() / Path(sys.argv[0]).name).resolve())]
if getattr(sys, "frozen", False):
return [sys.executable]
return [sys.executable, str(Path(__file__).resolve())]
def _make_tun_backend(emit):
engine = Path(BASE_DIR) / "tun2socks.exe"
if not engine.is_file():
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)
def _start_tun_backend(backend, payload):
"""管理员工作进程使用与系统代理相同的规则函数。"""
node = payload["node"]
rules = payload["route_rules"]
bypass_lan = bool(payload["bypass_lan"])
smart = bool(payload["smart"])
route_nodes = {item["ref"]: item["node"] for item in payload["route_nodes"]}
def user_rule(host):
return _route_rule_for_host(host, rules)
def route_node(rule):
return route_nodes.get(str(rule.get("node_ref") or ""))
def direct_target(policy_host, original_host, port):
rule = user_rule(policy_host) or user_rule(original_host)
action = str((rule or {}).get("action") or "").lower()
if action == "node":
return None
if action == "direct":
return original_host, port
# TUN 实际连接保持原目标 IP,避免在规则判断后再次 DNS 解析。
if _is_whitelisted(policy_host, _WHITELIST_VERSION) or _is_whitelisted(original_host, _WHITELIST_VERSION):
return original_host, port
if smart and _is_force_proxy(policy_host):
return None
if smart and _is_direct_domain(policy_host):
return original_host, port
if bypass_lan and _is_private_ip(original_host):
return original_host, port
if smart and _is_china_ip(original_host):
return original_host, port
return None
backend.start(
node,
bypass_lan=bypass_lan,
route_rules=rules,
dns_servers=payload["dns_servers"],
direct_target_resolver=direct_target,
route_rule_resolver=user_rule,
route_node_resolver=route_node,
route_nodes=list(route_nodes.values()),
)
def _tun_start_payload(use_pac, bypass_lan):
node = _current_node()
route_nodes = []
seen = set()
for rule in _route_rules():
if not rule.get("enabled") or rule.get("action") != "node":
continue
selected = _fixed_route_node(rule)
if selected is None:
raise RuntimeError(f"域名路由节点不可用: {rule.get('domain')}")
ref = str(rule.get("node_ref") or "")
if ref not in seen:
seen.add(ref)
route_nodes.append({"ref": ref, "node": selected})
return {
"node": node,
"route_rules": _route_rules(),
"route_nodes": route_nodes,
"dns_servers": _tun_dns_servers(),
"bypass_lan": bool(bypass_lan),
"smart": bool(use_pac),
}
# ── 代理白名单弹窗 ─────────────────────────────────────
# ── 主窗口 ────────────────────────────────────────────
@@ -2722,6 +2769,8 @@ class MainWindow(QWidget):
def __init__(self):
super().__init__()
self._proxy_set_by_us = False # 系统代理是否由本程序开启(退出时只清理自己设置的)
self._mode = _proxy_mode()
self._tun_controller = TunModeController(_tun_worker_command(), log)
self._quitting = False
self._tray = None
self._tray_menu = None
@@ -2766,6 +2815,23 @@ class MainWindow(QWidget):
# 节点未配置时延迟弹出登录框(等窗口显示后)
QTimer.singleShot(600, self._maybe_show_login)
def _update_mode_cards(self, system_running: bool, tun_running: bool):
self.system_card.set_state(self._mode == "system", system_running)
self.tun_card.set_state(self._mode == "tun", tun_running)
def _choose_mode(self, mode: str):
if mode == self._mode:
return
if self._start_in_progress or self._proxy_set_by_us or is_proxy_on() or self._tun_controller.status()["running"]:
QMessageBox.information(self, "请先关闭代理", "关闭当前代理后再切换模式。")
return
_set_proxy_mode(mode)
self._mode = mode
self._update_mode_cards(False, False)
self._set_toggle_button_style(is_start=True)
self._update_tray_menu()
log(f"已选择{'系统代理' if mode == 'system' else '虚拟网卡'}模式")
def _setup_tray(self):
"""创建托盘图标和动态状态菜单;系统不支持托盘时保留窗口模式。"""
if not QSystemTrayIcon.isSystemTrayAvailable():
@@ -2803,11 +2869,11 @@ class MainWindow(QWidget):
self._tray_menu.addAction(show_action)
self._tray_menu.addSeparator()
proxy_on = is_proxy_on()
pac_on = _should_use_internal_pac(_use_pac_mode(), _use_builtin_pac())
proxy_on = is_proxy_on() if self._mode == "system" else self._tun_controller.status()["running"]
pac_on = _should_use_internal_pac(_use_pac_mode())
bypass_lan = _bypass_lan()
for text in (
f"代理:{'开' if proxy_on else '关'}",
f"{'系统代理' if self._mode == 'system' else '虚拟网卡'}:{'开' if proxy_on else '关'}",
f"PAC:{'启用' if pac_on else '关闭'}",
f"内网IP:{'不代理' if bypass_lan else '代理'}",
):
@@ -2906,27 +2972,16 @@ class MainWindow(QWidget):
layout.addWidget(line)
# 系统代理状态卡片
px_frame = QFrame()
px_frame.setStyleSheet(f"""
QFrame {{
background-color: {C_CARD};
border: 1px solid {C_BORDER};
border-radius: 8px;
}}
""")
px_layout = QHBoxLayout(px_frame)
px_layout.setContentsMargins(16, 12, 16, 12)
px_label = QLabel("IE 代理")
px_label.setStyleSheet(f"color: {C_TEXT}; font-weight: bold; background: transparent;")
self.px_dot = StatusDot()
self.px_status = QLabel("检测中...")
self.px_status.setStyleSheet(f"color: {C_SUBTEXT}; background: transparent;")
px_layout.addWidget(px_label)
px_layout.addStretch()
px_layout.addWidget(self.px_status)
px_layout.addWidget(self.px_dot)
layout.addWidget(px_frame)
mode_row = QHBoxLayout()
mode_row.setSpacing(8)
self.system_card = ProxyModeCard("系统代理")
self.tun_card = ProxyModeCard("虚拟网卡")
self.system_card.clicked.connect(lambda: self._choose_mode("system"))
self.tun_card.clicked.connect(lambda: self._choose_mode("tun"))
mode_row.addWidget(self.system_card, 1)
mode_row.addWidget(self.tun_card, 1)
layout.addLayout(mode_row)
self._update_mode_cards(False, False)
traffic_cards = QHBoxLayout()
traffic_cards.setSpacing(8)
@@ -3089,7 +3144,7 @@ class MainWindow(QWidget):
# 在 GUI 线程读取控件状态传参,避免子线程直接访问 Qt 控件(非线程安全)
use_pac = self.cb_pac.isChecked()
use_bypass_lan = self.cb_bypass_lan.isChecked()
use_builtin_pac = _use_builtin_pac()
use_system_pac = _use_system_pac()
self._start_cancel_event = threading.Event()
self._start_in_progress = True
# 网络探测可能需要数秒;此时保留按钮给用户取消,而不是把界面锁死。
@@ -3097,12 +3152,12 @@ class MainWindow(QWidget):
self.btn_toggle.setEnabled(True)
threading.Thread(
target=self._start_thread,
args=(use_pac, use_bypass_lan, use_builtin_pac, self._start_cancel_event),
args=(use_pac, use_bypass_lan, use_system_pac, self._start_cancel_event),
daemon=True,
).start()
def _start_thread(self, use_pac, use_bypass_lan, use_builtin_pac, cancel_event):
def _start_thread(self, use_pac, use_bypass_lan, use_system_pac, cancel_event):
try:
self._start_proxy(use_pac, use_bypass_lan, use_builtin_pac, cancel_event)
self._start_proxy(use_pac, use_bypass_lan, use_system_pac, cancel_event)
_raise_if_start_cancelled(cancel_event)
self._op_done.emit("start", True)
except _StartCancelled:
@@ -3117,6 +3172,12 @@ class MainWindow(QWidget):
def _cleanup_start_resources(self):
"""回收本次启动已建立的资源;仅恢复本程序实际写入过的系统代理。"""
if self._mode == "tun":
try:
self._tun_controller.stop()
finally:
self._proxy_set_by_us = False
return
with self._proxy_state_lock:
proxy_owned = self._proxy_set_by_us
self._proxy_set_by_us = False
@@ -3127,14 +3188,26 @@ class MainWindow(QWidget):
_tls_stop()
_stop_pac_server()
def _start_proxy(self, use_pac, use_bypass_lan, use_builtin_pac=False, cancel_event=None):
"""启动本地转发器并设置系统代理(普通 PAC、内置 PAC 或固定代理)。"""
def _start_proxy(self, use_pac, use_bypass_lan, use_system_pac=False, cancel_event=None):
"""按当前选择启动系统代理或按需提权的虚拟网卡。"""
if _endpoint_domain() == "???":
# 未配置节点时禁止开启:否则系统代理会指向连不通任何目标的转发器,导致断网
raise RuntimeError("未配置节点:请先通过 nodes_url 在线更新或 nodes_tool 导入节点配置")
if not _node_reachable(_current_node()):
# 当前选中节点是 IPv6 专用而本机没有 IPv6 出口:转发的流量全部连不上
raise RuntimeError("当前节点仅提供 IPv6 地址,本机没有 IPv6 出口,请在节点菜单切换其他节点")
if self._mode == "tun":
payload = _tun_start_payload(use_pac, use_bypass_lan)
_raise_if_start_cancelled(cancel_event)
try:
self._tun_controller.start(payload, cancel_event)
_raise_if_start_cancelled(cancel_event)
self._proxy_set_by_us = True
log("虚拟网卡已开启")
except Exception:
self._cleanup_start_resources()
raise
return
_traffic_reset()
current_node = _current_node()
jump_chain = _jump_chain(current_node)
@@ -3144,7 +3217,7 @@ class MainWindow(QWidget):
if jump_chain:
log(f" 跳板路径: {_jump_route_text(current_node)}")
# PAC 模式:全部流量进转发器,按域名表/解析 IP 智能分流
use_internal_pac = _should_use_internal_pac(use_pac, use_builtin_pac)
use_internal_pac = _should_use_internal_pac(use_pac)
if use_internal_pac:
_load_cn_ranges()
_load_force_proxy_suffixes()
@@ -3155,7 +3228,7 @@ class MainWindow(QWidget):
jump_chain=jump_chain)
try:
_raise_if_start_cancelled(cancel_event)
if _should_use_system_pac(use_pac, use_builtin_pac):
if _should_use_system_pac(use_pac, use_system_pac):
# 本地生成全量转发 PAC(不再从服务器拉取),全部流量交给转发器做智能分流
pac_content = _build_pac(f"{PROXY_HOST}:{PROXY_PORT}")
if use_bypass_lan:
@@ -3173,7 +3246,7 @@ class MainWindow(QWidget):
_raise_if_start_cancelled(cancel_event)
proxy_on(bypass_lan=use_bypass_lan)
self._proxy_set_by_us = True
if use_builtin_pac:
if use_internal_pac:
log(f"使用内置 PAC:系统固定代理 127.0.0.1:{PROXY_PORT},转发器内部智能分流")
_raise_if_start_cancelled(cancel_event)
if use_bypass_lan:
@@ -3213,10 +3286,13 @@ class MainWindow(QWidget):
threading.Thread(target=self._stop_thread, daemon=True).start()
def _stop_thread(self):
"""后台关闭代理:清理系统代理、停止 TLS/PAC 服务。"""
"""后台关闭当前模式并回收其资源。"""
try:
log("正在关闭代理...")
proxy_off()
if self._mode == "tun":
self._tun_controller.stop()
else:
proxy_off()
self._proxy_set_by_us = False
log("已关闭")
self._op_done.emit("stop", True)
@@ -3236,6 +3312,8 @@ class MainWindow(QWidget):
"""设置页关闭后同步主页面的本地偏好显示。"""
self.cb_pac.setChecked(_use_pac_mode())
self.cb_bypass_lan.setChecked(_bypass_lan())
self._mode = _proxy_mode()
self.refresh_status()
self._update_tray_menu()
# ── 节点登录 ──
@@ -3391,32 +3469,26 @@ class MainWindow(QWidget):
def _poll_status(self):
try:
px = is_proxy_on()
state = {"system": is_proxy_on(), "tun": bool(self._tun_controller.status()["running"])}
except Exception:
px = False
self._status_ready.emit(px)
def _apply_status(self, px_on_):
"""GUI 线程:根据轮询结果刷新状态指示灯、IE代理、流量卡片。"""
state = {"system": False, "tun": False}
self._status_ready.emit(state)
def _apply_status(self, state):
"""GUI 线程:按模式选择和真实运行状态刷新两张状态卡。"""
self._status_busy = False
# IE 代理状态
if px_on_:
self.px_dot.set_on()
self.px_status.setText("已开启")
self.px_status.setStyleSheet(f"color: {C_GREEN}; background: transparent;")
if not self._start_in_progress:
self._set_toggle_button_style(is_start=False)
else:
self.px_dot.set_off()
self.px_status.setText("已关闭")
self.px_status.setStyleSheet(f"color: {C_RED}; background: transparent;")
if not self._start_in_progress:
self._set_toggle_button_style(is_start=True)
system_on = bool(state.get("system"))
tun_on = bool(state.get("tun"))
self._update_mode_cards(system_on, tun_on)
px_on = system_on if self._mode == "system" else tun_on
if not self._start_in_progress:
self._set_toggle_button_style(is_start=not px_on)
# 流量卡片(始终更新,未连接时显示 0)
px_on = is_proxy_on()
if px_on:
up_rate, down_rate, up_total, down_total = _traffic_rate()
if self._mode == "tun":
up_rate, down_rate, up_total, down_total = self._tun_controller.traffic()
else:
up_rate, down_rate, up_total, down_total = _traffic_rate()
else:
up_rate, down_rate, up_total, down_total = 0, 0, 0, 0
self.traffic_up.setText(f"↑{_fmt_bytes(up_rate)}/s")
@@ -3635,10 +3707,7 @@ class MainWindow(QWidget):
QMessageBox.warning(self, "下载失败", f"无法下载新版本:\n{message}")
def quit_app(self):
"""只清理自己设置的系统代理:用户或其他软件设置的代理不受影响。
本程序异常退出残留的代理设置由下次启动时 _cleanup_stale_proxy() 处理。
"""
"""退出时按当前模式回收本程序建立的资源。"""
self._quitting = True
if self._tray is not None:
self._tray.hide()
@@ -3650,7 +3719,10 @@ class MainWindow(QWidget):
pass # 清理失败也不能阻止退出,残留交给下次启动清理
elif self._proxy_set_by_us:
try:
proxy_off()
if self._mode == "tun":
self._tun_controller.stop()
else:
proxy_off()
except Exception:
pass # 清理失败也不能阻止退出,残留交给下次启动清理
QApplication.quit()
@@ -3698,7 +3770,12 @@ def _acquire_single_instance():
return True # 互斥体创建失败时放行,避免把用户锁在门外
def main():
"""主入口:检查管理员权限、依赖文件,然后启动 GUI。"""
"""普通 GUI 与按需提权 TUN 工作进程的唯一入口。"""
if len(sys.argv) == 4 and sys.argv[1] == WORKER_FLAG:
if not TunBackend.is_admin():
raise RuntimeError("TUN 工作进程需要管理员权限")
run_tun_worker(int(sys.argv[2]), sys.argv[3], _make_tun_backend, _start_tun_backend)
return
_set_per_monitor_dpi_aware()
# 单实例:第二个实例直接退出(且不能执行下面的残留代理清理,