Skip to content

第 11 章:系统代理、PAC、TUN 与特权边界

核心在运行不代表系统流量会经过它。应用还要决定全局代理或 PAC、选择实际端口、维护代理守卫,并在核心切换和退出时恢复操作系统。这里最危险的不是设置失败,而是旧异步任务把新会话的代理状态改回去。

一、三条流量接入路径

1.1 全局系统代理

把 HTTP/HTTPS/SOCKS 指向 127.0.0.1:mixed-port,并设置 bypass。适合大多数桌面应用,但不覆盖忽略系统代理的进程。

1.2 PAC

操作系统从嵌入服务器读取 FindProxyForURL,可按域名选择 DIRECT/PROXY。PAC endpoint 只在核心实际运行时可用。

1.3 TUN

通过虚拟网卡接管更广泛流量,需要管理员权限或特权 Service,并与 DNS/fake-ip 配置紧密耦合。

这三者不是互斥的技术实现,而是不同控制范围和权限成本。

二、端口有 desired 与 effective 两种答案

MixedPort 明确区分:

2.1 desired()

优先用户在 Verge 中选择的端口,否则取 Application Merge Config,最后落默认 7897。它在核心启动前就能回答,适合生成配置和启动决策。

2.2 effective()

先问当前 Mihomo base config 的实际端口;读取失败或核心报告 0 时回退 desired。适合用户触发的“把客户端指向正在服务的端口”。

启动 fallback 或手工核心配置可能使两者暂时不同。把它们合成一个 helper,会让配置写入和运行时访问至少有一方拿错答案。

三、系统代理的后端路由

proxy_backend_route(is_macos, running_mode)

  • macOS + Service → 通过 Service 修改系统代理;
  • 其他组合 → 应用本地 Sysopt 修改。

macOS Service 路径这样设计,是因为特权 helper 已持有系统网络控制能力,并能将核心启动与代理修改放在同一 owner session 中。其他平台沿用本地实现。

前端完全不知道这条差异,只调用 Verge 配置 patch。

四、Service 代理配置主动收窄输入

构造 MacosProxyConfig 时:

  • host 强制为 127.0.0.1,忽略可疑的自定义远端 host;
  • mixed port/PAC port 不得为 0;
  • bypass 拒绝 NUL;
  • bypass 最大 8192 字节并按 UTF-8 边界截断;
  • 默认 bypass 包含 loopback、私网和 .local

这是特权边界的典型原则:传给 Service 的数据不是把 UI 配置原样序列化,而是由非特权侧先规范化成窄、可验证的命令。

五、PAC availability 从 RunningMode 派生

RunStateEnv 接受 set_pac_available(bool)

  • core_started(Service/Sidecar) → true;
  • core_stopped() → false;
  • core_starting() → 临时 false;
  • core_start_settled() → 再按 mode 推导。

为什么交接时先关 PAC?操作系统可能在旧核心刚停、新核心未绑定之间请求脚本,拿到的 PAC 会把流量指向空端口。短暂返回不可用比发放错误路由更安全。

六、Proxy Guard:周期修复系统代理

用户或其他软件可能改写系统代理。开启 guard 后,后台按 proxy_guard_duration 周期重新应用期望配置。

普通循环并不安全:

text
旧 guard 发请求
  ↓ 正在 await Service
用户关闭系统代理 / 核心切换 owner

旧请求晚到并重新打开代理

项目使用 ServiceProxyOperations

  • guard_generation:最终操作先 invalidate 旧 guard;
  • operation_lock:周期请求与最终 apply/clear 串行;
  • captured OwnerSessionProof:旧 guard 不得采用新的 owner proof;
  • 每次 RPC 前检查 generation、mode 和 active proof。

七、为什么 invalidate 还不够,还要 drain

generation 能阻止“尚未发出”的旧请求,但一个请求可能已经进入 Service。最终 clear 若不等待它结束,顺序可能是:

text
clear 完成 → 旧 guard RPC 完成 → 代理又被打开

run_final_service_operation() 先 invalidate,再获取同一 operation lock,等待在途 guard 完成,最后执行 clear/apply。最终操作因此一定排在所有旧请求之后。

这是异步资源控制常见的 cancel-and-drain 协议:取消未来工作,并排空已经开始的工作。

八、Owner proof 防止跨会话控制

Guard 捕获的 proof 必须与 active_service_session() 完全相等。即使 RunningMode 仍是 Service、generation 也恰好未变,只要 Service 已被另一个应用会话接管,旧 guard 就退出。

这比只比较 PID 强:服务进程可以长期不变,内部托管的核心和 owner session 已经换代。

九、TUN 能力与配置派生

打开 TUN 会影响多个层面:

  1. Verge 保存用户意图;
  2. RunState 判断是否具备权限;
  3. enhance 写 tun.enable
  4. 必要时补 fake-ip DNS;
  5. Linux 可能必须重启核心;
  6. tray/icon/UI 同步;
  7. 失去能力时 session suppress 或持久关闭。

因此 TUN 开关不能被实现成 patchClashConfig({ tun: { enable } })。那会绕过权限、DNS、服务对话和一致性恢复。

十、Session suppress 与持久关闭

当用户选择“本次继续 Sidecar”时,系统不一定要永久改掉 TUN 设置。TUN_SESSION_SUPPRESSED 表示:

  • 配置文件仍记得用户希望 TUN;
  • 本次会话 enhance 时强制视为关闭;
  • 服务修复后可恢复意图。

另一路 disable_tun_and_persist() 则明确写入 enable_tun_mode=false

短期能力缺失和长期用户选择被分开建模,避免一次服务故障永久改变偏好。

十一、退出与异常退出恢复

正常退出会:

  • 停止 proxy guard;
  • 清系统代理;
  • 停止核心;
  • macOS 恢复公共 DNS;
  • 保存配置。

但 Windows session ending、SIGTERM 或 UI 崩溃可能绕过正常路径。项目同时注册系统 signal handler、Tauri Exit/ExitRequested,并使用 shutdown latch 避免重复清理。

重复清理也危险:两个 stop/clear 并发可能互相覆盖状态。latch 的 Canceled outcome 会释放,允许用户取消后再次退出;Committed 则永久挡住重复信号。

十二、系统集成的安全原则

  1. 指向核心时使用 effective,生成配置时使用 desired。
  2. 特权命令的数据面要主动收窄和规范化。
  3. 后台修复任务必须有代际、所有权和 drain。
  4. 短期降级不等同于永久改偏好。
  5. 恢复系统状态是退出协议的一部分,不是附加清理。

本章源码索引

  • src-tauri/src/config/mixed_port.rs::MixedPort:desired/effective
  • src-tauri/src/core/proxy_control.rs:系统代理路由、guard 与 cancel-and-drain
  • src-tauri/src/core/sysopt.rs:本地系统代理与 guard
  • src-tauri/src/core/service.rs::set_system_proxy_by_service:Service 路径
  • src-tauri/src/utils/server.rs:PAC endpoint 与 availability
  • src-tauri/src/enhance/tun.rs:TUN/DNS 配置派生
  • src-tauri/src/feat/tun.rs:TUN reconciliation
  • crates/clash-verge-signal/src/:跨平台退出信号与 ShutdownLatch

原创文档 CC BY-SA 4.0 · 站点代码 MIT