feat(lite): merge TUN mode with IPv6 routing
This commit is contained in:
+272
-195
@@ -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()
|
||||
|
||||
# 单实例:第二个实例直接退出(且不能执行下面的残留代理清理,
|
||||
|
||||
Reference in New Issue
Block a user