feat(macos): KillSwitch L0→L3 — NE includeAllNetworks+enforceRoutes+on-demand
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user