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 @@ ← 文档索引
Pangolin 客户端 · 断网保护设计依据与现状 · 2026-06-22(2026-06-30 更新:macOS L0→L3 已实现)
+Pangolin 客户端 · 断网保护设计依据与现状 · 2026-06-22(2026-07-01 更新:macOS killswitch 落地 enforceRoutes+on-demand;includeAllNetworks 实测堵死整机网络已撤回)
strict_route + 子进程重载 + 退避重连。进程被硬杀仍泄漏。desktop_vpn_bridge.dart:305(applyKillSwitchToConfig)、:178DesktopVpnBridge)。includeAllNetworks+enforceRoutes(L2 OS 强制、扛崩溃)+ NEOnDemandRule 常开(L3)。随 killswitch 开关即时切换;手动断开先关 on-demand 防反弹。详见 §6.5。VpnChannel.swift(configureKillSwitch)、PacketTunnelProvider.swift(includeAllNetworks())enforceRoutes 连接期防漏 + NEOnDemandRule 常开兜重连;随 killswitch 开关即时切换,手动断开先关 on-demand 防反弹。⚠️ includeAllNetworks 实测堵死整机网络已撤回,故暂未到 includeAllNetworks-L3。详见 §6.5。VpnChannel.swift(configureKillSwitch)setKillSwitch 只打日志(TODO 11G)。MainActivity.kt(setKillSwitch 分支)strict_route,与 Windows 一致),再引导用户开系统 Always-on 拿到 L3,UI 上诚实标注「彻底防泄漏需在系统设置开启」。这是「app 能力上限 + OS 兜底」的合理组合,不夸大。macOS 走原生 NE System Extension(kUseNativeVpnMacOS=true),strict_route 那套对它不生效,必须用 NE 原语。L0→L3 的达成路径:
macOS 走原生 NE System Extension(kUseNativeVpnMacOS=true),strict_route 那套对它不生效,必须用 NE 原语。
includeAllNetworks=true 追 L2,实测把整机网络堵死并陷入连不上的死循环——它会把所有流量(含 sing-box 连服务器的握手、app 调控制面 API 的请求)在隧道建起来之前就塞进隧道 → 握手出不去 → 连接失败 → on-demand 立刻重连 → 无限打转(连 app 的账户信息都拉不到,「我的」页全显示 —)。故撤掉 includeAllNetworks,改用下面不会误伤握手/控制面的组合。要拿回「扛进程被杀」那一档,须先在扩展内显式放行服务器/控制面连接,留待真机验证后再评估。| 目标 Level | NE 机制 | 效果 |
|---|---|---|
| 当前达成 | NE 机制 | 效果 / 边界 |
| L2 OS 强制 | protocolConfiguration.includeAllNetworks=true + enforceRoutes=true | 全量流量入隧道、由 neagent 系统级强制,扛 app/内核崩溃(规则在 OS 不在 app 进程) |
| L3 常开+持久 | L2 + onDemandRules=[NEOnDemandRuleConnect()] + isOnDemandEnabled=true | 掉线/开机窗口也堵,OS 自动拉起,全程 fail-closed |
| 连接期防泄漏 | enforceRoutes=true(隧道 includedRoutes 含 0.0.0.0/0) | 连接期间所有流量强制走隧道、不从旁路接口泄漏;不覆盖进程被杀后的窗口 |
| 掉线缺口收窄 | onDemandRules=[NEOnDemandRuleConnect()] + isOnDemandEnabled=true | 掉线/开机窗口 OS 自动拉起隧道,缩小重连缺口(非零) |
放行 LAN:excludeLocalNetworks=true(打印机/AirPlay 等)。不设 includeAllNetworks(见上方踩坑修正)。 | ||
即「连接期 enforceRoutes 防漏 + on-demand 兜重连」,介于 L1 与 L2 之间,尚未到设计追求的 includeAllNetworks-L3。诚实标注,不夸大。
client/macos/Runner/VpnChannel.swift
killSwitchEnabled(默认 true,与 Dart AppSettings.killSwitch 默认对齐)。setKillSwitch 落地:存标志;若 manager 已存在 → 重新 applyKillSwitchConfig + saveToPreferences 即时生效。loadOrCreateManager 按标志设:includeAllNetworks / enforceRoutes=true / excludeLocalNetworks=true(放行 LAN 打印机等)/ onDemandRules + isOnDemandEnabled。configureKillSwitch 按标志设:enforceRoutes / excludeLocalNetworks=true / onDemandRules + isOnDemandEnabled;includeAllNetworks 恒 false。stop():on-demand 常开时直接 stopVPNTunnel() 会被 OS 立刻拉回。手动断开须先 isOnDemandEnabled=false + saveToPreferences 再 stop;否则「断开」按钮失效。start() 的 startVPNTunnel(options:) 附带 killSwitch 标志传给扩展。client/macos/PacketTunnel/PacketTunnelProvider.swift
-startTunnel(options:) 读取 killSwitch 选项并缓存。includeAllNetworks() 返回该缓存值(与 NE 层对齐,让 sing-box 不装绕行路由)。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 满足。
flutter build macos 通过、sysext 激活。stop() 的 on-demand 处理)。stop() 的 on-demand 处理)。scripts/local_test.sh ks-baseline|ks-test|ks-status 做 fail-closed 漏测。