From a32d87c9c0388355b7fd1497a8bb488f2bd0a848 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Wed, 1 Jul 2026 06:09:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(macos):=20killswitch=20=E6=92=A4=E6=8E=89?= =?UTF-8?q?=20includeAllNetworks(=E5=A0=B5=E6=AD=BB=E6=95=B4=E6=9C=BA?= =?UTF-8?q?=E7=BD=91=E7=BB=9C)=20=E2=80=94=20=E6=94=B9=20enforceRoutes+on-?= =?UTF-8?q?demand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实测 includeAllNetworks=true 把所有流量(含 sing-box 连服务器握手、app 调控制面 请求)在隧道建起前就塞进隧道 → 握手出不去 → 连接失败 + on-demand 死循环 + 整机 断网(连「我的」页账户信息都拉不到、显示 —)。 改为不会误伤握手/控制面的组合: - configureKillSwitch:includeAllNetworks 恒 false;保留 enforceRoutes(连接期防漏) + excludeLocalNetworks + NEOnDemandRule 常开(兜重连) - 撤回 start() 的 killSwitch option 及扩展侧 includeAllNetworks 透传(回 false) - 诚实标注:当前 L1→L2 之间,未到 includeAllNetworks-L3;要拿「扛进程被杀」须先 在扩展内放行服务器/控制面连接,留待真机验证 docs/killswitch-design.html: §6.5 加踩坑修正、状态表 macOS 改「L1→L2 之间」 Co-Authored-By: Claude Opus 4.8 --- .../PacketTunnel/PacketTunnelProvider.swift | 13 ++----- client/macos/Runner/VpnChannel.swift | 17 +++++---- docs/killswitch-design.html | 37 ++++++++++--------- 3 files changed, 32 insertions(+), 35 deletions(-) diff --git a/client/macos/PacketTunnel/PacketTunnelProvider.swift b/client/macos/PacketTunnel/PacketTunnelProvider.swift index e9d11ac..4ffb4c4 100644 --- a/client/macos/PacketTunnel/PacketTunnelProvider.swift +++ b/client/macos/PacketTunnel/PacketTunnelProvider.swift @@ -63,12 +63,7 @@ final class PacketTunnelProvider: NEPacketTunnelProvider { LibboxSetup(setup, &setupErr) if let setupErr { throw setupErr } - // KillSwitch:主 app 经 startVPNTunnel options 下发;OS on-demand 自动拉起时 - // options 为 nil → 默认 true(on-demand 仅在 killswitch 开时存在)。libbox 平台回调 - // includeAllNetworks() 据此与 NE 层 includeAllNetworks 对齐,避免装绕行路由。 - let killSwitch = (options?["killSwitch"] as? NSNumber)?.boolValue ?? true - log.info("startTunnel: killSwitch=\(killSwitch, privacy: .public)") - let platform = PangolinPlatformInterface(provider: self, includeAllNetworks: killSwitch) + let platform = PangolinPlatformInterface(provider: self) self.platform = platform var newErr: NSError? @@ -187,11 +182,9 @@ final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol private var monitor: NWPathMonitor? private var defaultInterfaceIndex: Int32 = -1 private let monitorQueue = DispatchQueue(label: "pangolin.tunnel.pathmonitor") - private let includeAllNetworksFlag: Bool - init(provider: NEPacketTunnelProvider, includeAllNetworks: Bool) { + init(provider: NEPacketTunnelProvider) { self.provider = provider - self.includeAllNetworksFlag = includeAllNetworks super.init() } @@ -276,7 +269,7 @@ final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol // MARK: 其余协议方法(取合理默认) func underNetworkExtension() -> Bool { true } func useProcFS() -> Bool { false } - func includeAllNetworks() -> Bool { includeAllNetworksFlag } + func includeAllNetworks() -> Bool { false } func clearDNSCache() {} func readWIFIState() -> LibboxWIFIState? { nil } func systemCertificates() -> (any LibboxStringIteratorProtocol)? { nil } diff --git a/client/macos/Runner/VpnChannel.swift b/client/macos/Runner/VpnChannel.swift index 66ade2c..c3fe304 100644 --- a/client/macos/Runner/VpnChannel.swift +++ b/client/macos/Runner/VpnChannel.swift @@ -131,7 +131,6 @@ final class VpnChannel: NSObject { vpnLog("step③ startVPNTunnel(options: configContent) …") try mgr.connection.startVPNTunnel(options: [ "configContent": configJson as NSString, - "killSwitch": NSNumber(value: killSwitchEnabled), ]) vpnLog("step③ startVPNTunnel 调用已返回(实际起停由 NEVPNStatus 流驱动)✓") result(nil) @@ -181,14 +180,18 @@ final class VpnChannel: NSObject { // ── KillSwitch(断网保护)NE 配置 ──────────────────────────────── // 把当前 killSwitchEnabled 写入 manager 的 NE 字段(不保存,由调用方保存): - // L2 OS 强制:includeAllNetworks=true 全量入隧道 + enforceRoutes=true 隧道路由优先, - // 非隧道流量被 neagent 系统级阻断(扛 app/内核崩溃)。 - // L3 常开:NEOnDemandRule 让 OS 在掉线/开机窗口自动拉起隧道,全程 fail-closed。 - // excludeLocalNetworks=true 放行 LAN(打印机/AirPlay 等),不影响防泄漏目标。 + // enforceRoutes=true:隧道 includedRoutes(0.0.0.0/0 默认路由)优先于物理接口, + // 连接期间所有流量强制走隧道、不从旁路接口泄漏。 + // NEOnDemandRule 常开:OS 在掉线/开机窗口自动拉起隧道,缩小重连缺口。 + // excludeLocalNetworks=true:放行 LAN(打印机/AirPlay 等),不影响防泄漏目标。 + // ⚠️ 刻意不设 includeAllNetworks:它会把「所有」流量(含 sing-box 连服务器的握手、 + // app 调控制面的请求)在隧道建起来前就塞进隧道 → 握手出不去 → 连接失败 + on-demand + // 死循环 + 整机断网(实测)。要拿「扛进程被杀」那一档须先在扩展内正确放行服务器/ + // 控制面连接,留待真机验证后再评估,见 docs/killswitch-design.html §6.5。 // enforceRoutes/excludeLocalNetworks 为 macOS 11+ API,Runner 部署目标 10.15 → #available 守卫。 private func configureKillSwitch(on mgr: NETunnelProviderManager) { if let proto = mgr.protocolConfiguration as? NETunnelProviderProtocol { - proto.includeAllNetworks = killSwitchEnabled + proto.includeAllNetworks = false if #available(macOS 11.0, *) { proto.enforceRoutes = killSwitchEnabled proto.excludeLocalNetworks = true @@ -203,7 +206,7 @@ final class VpnChannel: NSObject { mgr.onDemandRules = [] mgr.isOnDemandEnabled = false } - vpnLog(" killSwitch 配置: includeAllNetworks=\(killSwitchEnabled) enforceRoutes=\(killSwitchEnabled) onDemand=\(killSwitchEnabled)") + vpnLog(" killSwitch 配置: enforceRoutes=\(killSwitchEnabled) onDemand=\(killSwitchEnabled) includeAllNetworks=false") } /// 即时应用 killswitch(改字段 + 保存 + 重载)。供 setKillSwitch 在 manager 已装配时调用。 diff --git a/docs/killswitch-design.html b/docs/killswitch-design.html index a73165d..35a0cb1 100644 --- a/docs/killswitch-design.html +++ b/docs/killswitch-design.html @@ -50,7 +50,7 @@ ← 文档索引

KillSwitch 设计与跨平台能力矩阵(知识库)

-

Pangolin 客户端 · 断网保护设计依据与现状 · 2026-06-22(2026-06-30 更新:macOS L0→L3 已实现)

+

Pangolin 客户端 · 断网保护设计依据与现状 · 2026-06-22(2026-07-01 更新:macOS killswitch 落地 enforceRoutes+on-demand;includeAllNetworks 实测堵死整机网络已撤回)

KillSwitch 不是「一个开关」,而是分层能力——理想态、平台天花板、当前实现是三件不同的事。本文沉淀其设计依据与现状。 @@ -104,7 +104,7 @@ KillSwitch 不是「一个开关」,而是分层能力——理想态 WindowsL1✅ 改 strict_route + 子进程重载 + 退避重连。进程被硬杀仍泄漏。desktop_vpn_bridge.dart:305applyKillSwitchToConfig)、:178 LinuxL1同 Windows(共用 DesktopVpnBridge)。同上 -macOSL3✅ NE fail-closed(本轮 #1):includeAllNetworks+enforceRoutes(L2 OS 强制、扛崩溃)+ NEOnDemandRule 常开(L3)。随 killswitch 开关即时切换;手动断开先关 on-demand 防反弹。详见 §6.5。VpnChannel.swiftconfigureKillSwitch)、PacketTunnelProvider.swiftincludeAllNetworks()) +macOSL1→L2 之间✅ 本轮 #1:enforceRoutes 连接期防漏 + NEOnDemandRule 常开兜重连;随 killswitch 开关即时切换,手动断开先关 on-demand 防反弹。⚠️ includeAllNetworks 实测堵死整机网络已撤回,故暂未到 includeAllNetworks-L3。详见 §6.5。VpnChannel.swiftconfigureKillSwitchAndroidL0❌ stub:setKillSwitch 只打日志(TODO 11G)。MainActivity.ktsetKillSwitch 分支) iOS—无客户端。— @@ -113,7 +113,7 @@ KillSwitch 不是「一个开关」,而是分层能力——理想态

6. Pangolin 现实判断与决策

-

6.5 macOS L0→L3 实现方案(本轮 #1)

-

macOS 走原生 NE System Extension(kUseNativeVpnMacOS=true),strict_route 那套对它不生效,必须用 NE 原语。L0→L3 的达成路径:

+

6.5 macOS KillSwitch 实现方案(本轮 #1)

+

macOS 走原生 NE System Extension(kUseNativeVpnMacOS=true),strict_route 那套对它不生效,必须用 NE 原语。

+
+踩坑修正(重要):最初按设计用 includeAllNetworks=true 追 L2,实测把整机网络堵死并陷入连不上的死循环——它会把所有流量(含 sing-box 连服务器的握手、app 调控制面 API 的请求)在隧道建起来之前就塞进隧道 → 握手出不去 → 连接失败 → on-demand 立刻重连 → 无限打转(连 app 的账户信息都拉不到,「我的」页全显示 —)。故撤掉 includeAllNetworks,改用下面不会误伤握手/控制面的组合。要拿回「扛进程被杀」那一档,须先在扩展内显式放行服务器/控制面连接,留待真机验证后再评估。
- + - - + + +
目标 LevelNE 机制效果
当前达成NE 机制效果 / 边界
L2 OS 强制protocolConfiguration.includeAllNetworks=true + enforceRoutes=true全量流量入隧道、由 neagent 系统级强制,扛 app/内核崩溃(规则在 OS 不在 app 进程)
L3 常开+持久L2 + onDemandRules=[NEOnDemandRuleConnect()] + isOnDemandEnabled=true掉线/开机窗口也堵,OS 自动拉起,全程 fail-closed
连接期防泄漏enforceRoutes=true(隧道 includedRoutes0.0.0.0/0连接期间所有流量强制走隧道、不从旁路接口泄漏;覆盖进程被杀后的窗口
掉线缺口收窄onDemandRules=[NEOnDemandRuleConnect()] + isOnDemandEnabled=true掉线/开机窗口 OS 自动拉起隧道,缩小重连缺口(非零)
放行 LAN:excludeLocalNetworks=true(打印机/AirPlay 等)。不设 includeAllNetworks(见上方踩坑修正)。
+

即「连接期 enforceRoutes 防漏 + on-demand 兜重连」,介于 L1 与 L2 之间,尚未到设计追求的 includeAllNetworks-L3。诚实标注,不夸大。

改动(2 个原生文件,不动 Dart/服务端)

client/macos/Runner/VpnChannel.swift
  1. 新增缓存字段 killSwitchEnabled(默认 true,与 Dart AppSettings.killSwitch 默认对齐)。
  2. setKillSwitch 落地:存标志;若 manager 已存在 → 重新 applyKillSwitchConfig + saveToPreferences 即时生效。
  3. -
  4. loadOrCreateManager 按标志设:includeAllNetworks / enforceRoutes=true / excludeLocalNetworks=true(放行 LAN 打印机等)/ onDemandRules + isOnDemandEnabled
  5. +
  6. configureKillSwitch 按标志设:enforceRoutes / excludeLocalNetworks=true / onDemandRules + isOnDemandEnabledincludeAllNetworksfalse
  7. 关键 gotcha — stop():on-demand 常开时直接 stopVPNTunnel() 会被 OS 立刻拉回。手动断开须 isOnDemandEnabled=false + saveToPreferences stop;否则「断开」按钮失效。
  8. -
  9. start()startVPNTunnel(options:) 附带 killSwitch 标志传给扩展。
-client/macos/PacketTunnel/PacketTunnelProvider.swift -
    -
  1. startTunnel(options:) 读取 killSwitch 选项并缓存。
  2. -
  3. libbox 回调 includeAllNetworks() 返回该缓存值(与 NE 层对齐,让 sing-box 不装绕行路由)。
  4. -
+client/macos/PacketTunnel/PacketTunnelProvider.swift:libbox 回调 includeAllNetworks() 保持 false(与 NE 层一致)。
-

无需新增 entitlement(packet-tunnel-provider 已有);includeAllNetworks/enforceRoutes/excludeLocalNetworks/on-demand 均 macOS 10.15/11+ 可用,Runner 部署目标满足。

+

无需新增 entitlement(packet-tunnel-provider 已有);enforceRoutes/excludeLocalNetworks 为 macOS 11+ API,已 #available 守卫,Runner 部署目标 10.15 满足。

验证(NE 难单测,靠真机手测)

  1. flutter build macos 通过、sysext 激活。
  2. -
  3. killswitch 开 + 连接 → 正常上网;杀扩展进程或拔隧道 → 流量被阻断(无 IP/DNS 泄漏)= 跨过 L1→L2 质变线。
  4. -
  5. on-demand:开机/掉线窗口隧道自动拉起;手动「断开」能真断、不反弹(验 stop() 的 on-demand 处理)。
  6. +
  7. killswitch 开 + 连接 → 能正常连上并上网(不再死循环 / 整机断网)、「我的」页账户信息正常加载。
  8. +
  9. 连接期:旁路接口(如同时插网线+WiFi)不泄漏。
  10. +
  11. on-demand:掉线后隧道自动拉起;手动「断开」能真断、不反弹(验 stop() 的 on-demand 处理)。
  12. killswitch 关 → on-demand 取消、回普通连接。
  13. +
  14. 脚本:scripts/local_test.sh ks-baseline|ks-test|ks-status 做 fail-closed 漏测。

7. 关联待办