From 680f1af205f85ab066f1015ab4baadf0004e0d20 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Tue, 30 Jun 2026 23:54:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(macos):=20KillSwitch=20L0=E2=86=92L3=20?= =?UTF-8?q?=E2=80=94=20NE=20includeAllNetworks+enforceRoutes+on-demand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS 走原生 NE,strict_route 不生效。补齐断网保护到天花板 L3: VpnChannel.swift - 缓存 killSwitchEnabled(默认 true,与 Dart AppSettings.killSwitch 对齐) - setKillSwitch 落地:存标志,manager 已装配则即时重写 NE 配置+保存 - configureKillSwitch:includeAllNetworks+enforceRoutes(L2 OS 强制、扛崩溃) + NEOnDemandRule 常开(L3)+ excludeLocalNetworks 放行 LAN (enforceRoutes/excludeLocalNetworks 为 macOS 11+ API,#available 守卫) - stop() gotcha:on-demand 常开时先关 isOnDemandEnabled+save 再 stop, 否则手动断开被 OS 立刻拉回 - start() options 带 killSwitch 传扩展 PacketTunnelProvider.swift - startTunnel 读 killSwitch 选项(OS on-demand 自启时 nil→默认 true) - libbox 回调 includeAllNetworks() 返回该值,与 NE 层对齐 docs/killswitch-design.html: 新增 §6.5 实现方案,状态表 macOS L0→L3 验证:xcodebuild CODE_SIGNING_ALLOWED=NO 两 target SwiftCompile 通过; 端到端 fail-closed/on-demand 需真机手测(NE 难单测)。 Co-Authored-By: Claude Opus 4.8 --- .../PacketTunnel/PacketTunnelProvider.swift | 13 +++- client/macos/Runner/VpnChannel.swift | 75 ++++++++++++++++++- docs/killswitch-design.html | 42 ++++++++++- 3 files changed, 122 insertions(+), 8 deletions(-) diff --git a/client/macos/PacketTunnel/PacketTunnelProvider.swift b/client/macos/PacketTunnel/PacketTunnelProvider.swift index 4ffb4c4..e9d11ac 100644 --- a/client/macos/PacketTunnel/PacketTunnelProvider.swift +++ b/client/macos/PacketTunnel/PacketTunnelProvider.swift @@ -63,7 +63,12 @@ final class PacketTunnelProvider: NEPacketTunnelProvider { LibboxSetup(setup, &setupErr) if let setupErr { throw setupErr } - let platform = PangolinPlatformInterface(provider: self) + // 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) self.platform = platform var newErr: NSError? @@ -182,9 +187,11 @@ 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) { + init(provider: NEPacketTunnelProvider, includeAllNetworks: Bool) { self.provider = provider + self.includeAllNetworksFlag = includeAllNetworks super.init() } @@ -269,7 +276,7 @@ final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol // MARK: 其余协议方法(取合理默认) func underNetworkExtension() -> Bool { true } func useProcFS() -> Bool { false } - func includeAllNetworks() -> Bool { false } + func includeAllNetworks() -> Bool { includeAllNetworksFlag } 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 e2105b9..66ade2c 100644 --- a/client/macos/Runner/VpnChannel.swift +++ b/client/macos/Runner/VpnChannel.swift @@ -33,6 +33,10 @@ final class VpnChannel: NSObject { private let statsClient = StatsClient() private var manager: NETunnelProviderManager? private var sysextDelegate: SysExtActivationDelegate? + // KillSwitch(断网保护)开关,默认 true 与 Dart AppSettings.killSwitch 默认对齐。 + // 决定 NE 层是否 fail-closed:includeAllNetworks + enforceRoutes(L2 OS 强制) + // + NEOnDemandRule 常开(L3 开机/掉线窗口也堵)。详见 docs/killswitch-design.html §6.5。 + private var killSwitchEnabled = true static func register(with registrar: FlutterPluginRegistrar) { let instance = VpnChannel() @@ -87,8 +91,26 @@ final class VpnChannel: NSObject { case "getActiveOutbound": result("auto") case "setKillSwitch": - // includeAllNetworks / on-demand 实现 kill switch(后续)。 - result(nil) + let on = (call.arguments as? Bool) ?? true + killSwitchEnabled = on + vpnLog("setKillSwitch=\(on)") + // 已装配 manager 则即时重写 NE 配置(includeAllNetworks/enforceRoutes/on-demand); + // 尚无 manager 时仅缓存,下次 loadOrCreateManager 装配时应用。 + if let mgr = manager { + Task { + do { + try await self.applyKillSwitchConfig(to: mgr) + vpnLog("setKillSwitch: NE 配置已更新并保存 ✓") + result(nil) + } catch { + let ns = error as NSError + vpnLog("setKillSwitch: 保存 NE 配置失败 ✗ code=\(ns.code) desc=\(ns.localizedDescription)") + result(FlutterError(code: "killswitch_failed", message: error.localizedDescription, details: nil)) + } + } + } else { + result(nil) + } default: result(FlutterMethodNotImplemented) } @@ -109,6 +131,7 @@ 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) @@ -120,6 +143,19 @@ final class VpnChannel: NSObject { } private func stop(_ result: @escaping FlutterResult) async { + // on-demand 常开时直接 stopVPNTunnel 会被 OS 立刻拉回 → 手动断开须先关 on-demand 再断。 + // 下次手动 start() 时 loadOrCreateManager 会按 killSwitchEnabled 重新启用 on-demand。 + if let mgr = manager, mgr.isOnDemandEnabled { + mgr.isOnDemandEnabled = false + do { + try await mgr.saveToPreferences() + try await mgr.loadFromPreferences() + vpnLog("stop: 已临时关闭 on-demand(避免手动断开被自动拉回)") + } catch { + let ns = error as NSError + vpnLog("stop: 关闭 on-demand 失败(仍继续断开) code=\(ns.code) desc=\(ns.localizedDescription)") + } + } manager?.connection.stopVPNTunnel() result(nil) } @@ -135,6 +171,7 @@ final class VpnChannel: NSObject { mgr.protocolConfiguration = proto mgr.localizedDescription = "Pangolin" mgr.isEnabled = true + configureKillSwitch(on: mgr) // 按 killSwitchEnabled 写入 NE fail-closed 字段 vpnLog(" saveToPreferences(providerBundleId=\(Self.tunnelBundleId)) …") try await mgr.saveToPreferences() try await mgr.loadFromPreferences() // 保存后重载,拿到有效 connection @@ -142,6 +179,40 @@ final class VpnChannel: NSObject { return mgr } + // ── KillSwitch(断网保护)NE 配置 ──────────────────────────────── + // 把当前 killSwitchEnabled 写入 manager 的 NE 字段(不保存,由调用方保存): + // L2 OS 强制:includeAllNetworks=true 全量入隧道 + enforceRoutes=true 隧道路由优先, + // 非隧道流量被 neagent 系统级阻断(扛 app/内核崩溃)。 + // L3 常开:NEOnDemandRule 让 OS 在掉线/开机窗口自动拉起隧道,全程 fail-closed。 + // excludeLocalNetworks=true 放行 LAN(打印机/AirPlay 等),不影响防泄漏目标。 + // 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 + if #available(macOS 11.0, *) { + proto.enforceRoutes = killSwitchEnabled + proto.excludeLocalNetworks = true + } + } + if killSwitchEnabled { + let rule = NEOnDemandRuleConnect() + rule.interfaceTypeMatch = .any + mgr.onDemandRules = [rule] + mgr.isOnDemandEnabled = true + } else { + mgr.onDemandRules = [] + mgr.isOnDemandEnabled = false + } + vpnLog(" killSwitch 配置: includeAllNetworks=\(killSwitchEnabled) enforceRoutes=\(killSwitchEnabled) onDemand=\(killSwitchEnabled)") + } + + /// 即时应用 killswitch(改字段 + 保存 + 重载)。供 setKillSwitch 在 manager 已装配时调用。 + private func applyKillSwitchConfig(to mgr: NETunnelProviderManager) async throws { + configureKillSwitch(on: mgr) + try await mgr.saveToPreferences() + try await mgr.loadFromPreferences() + } + // 请求系统加载/更新 PacketTunnel System Extension。首启系统会弹「隐私与安全性」 // 让用户允许;允许后 didFinishWithResult 回来。已是最新则快速完成。 private func activateSystemExtensionIfNeeded() async throws { diff --git a/docs/killswitch-design.html b/docs/killswitch-design.html index 79e8c3c..a73165d 100644 --- a/docs/killswitch-design.html +++ b/docs/killswitch-design.html @@ -50,7 +50,7 @@ ← 文档索引

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

-

Pangolin 客户端 · 断网保护设计依据与现状 · 2026-06-22

+

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

KillSwitch 不是「一个开关」,而是分层能力——理想态、平台天花板、当前实现是三件不同的事。本文沉淀其设计依据与现状。 @@ -104,7 +104,7 @@ KillSwitch 不是「一个开关」,而是分层能力——理想态 WindowsL1✅ 改 strict_route + 子进程重载 + 退避重连。进程被硬杀仍泄漏。desktop_vpn_bridge.dart:305applyKillSwitchToConfig)、:178 LinuxL1同 Windows(共用 DesktopVpnBridge)。同上 -macOSL0❌ stub:setKillSwitch 直接 result(nil)includeAllNetworks()→false。天花板 L3,实际啥都没做。VpnChannel.swift:69PacketTunnelProvider.swift:224 +macOSL3✅ NE fail-closed(本轮 #1):includeAllNetworks+enforceRoutes(L2 OS 强制、扛崩溃)+ NEOnDemandRule 常开(L3)。随 killswitch 开关即时切换;手动断开先关 on-demand 防反弹。详见 §6.5。VpnChannel.swiftconfigureKillSwitch)、PacketTunnelProvider.swiftincludeAllNetworks()AndroidL0❌ 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 的达成路径:

+ + + + + + +
目标 LevelNE 机制效果
L2 OS 强制protocolConfiguration.includeAllNetworks=true + enforceRoutes=true全量流量入隧道、由 neagent 系统级强制,扛 app/内核崩溃(规则在 OS 不在 app 进程)
L3 常开+持久L2 + onDemandRules=[NEOnDemandRuleConnect()] + isOnDemandEnabled=true掉线/开机窗口也堵,OS 自动拉起,全程 fail-closed
+

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

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

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

+

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

+
    +
  1. flutter build macos 通过、sysext 激活。
  2. +
  3. killswitch 开 + 连接 → 正常上网;杀扩展进程或拔隧道 → 流量被阻断(无 IP/DNS 泄漏)= 跨过 L1→L2 质变线。
  4. +
  5. on-demand:开机/掉线窗口隧道自动拉起;手动「断开」能真断、不反弹(验 stop() 的 on-demand 处理)。
  6. +
  7. killswitch 关 → on-demand 取消、回普通连接。
  8. +
+

7. 关联待办

KillSwitch 相关待办见项目 todo//todo list),主要三条: