Files
pangolin/client/macos/Runner/VpnChannel.swift
T
wangjia a32d87c9c0 fix(macos): killswitch 撤掉 includeAllNetworks(堵死整机网络) — 改 enforceRoutes+on-demand
实测 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 <noreply@anthropic.com>
2026-07-01 06:09:04 +08:00

362 lines
19 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// VpnChannel.swift app VPN (B / P1 )
//
// Flutter pangolin/vpn(MethodChannel)+ pangolin/vpn/statuspangolin/vpn/stats
// (EventChannel) NETunnelProviderManager:/ PacketTunnel System Extension
// client/lib/bridge/vpn_bridge.dart VpnNativeBridge
//
// : MainFlutterWindow.awakeFromNib `VpnChannel.register(with: flutterViewController)`
//
// ** System Extension**(OSSystemExtensionRequest,
// activateSystemExtensionIfNeeded + 4),
// ; NETunnelProviderManager provider
import FlutterMacOS
import NetworkExtension
import SystemExtensions
import os.log
// os_log + %{public} Console.app / `log show` (NSLog %@
// redact <private>,)os_log(C API) macOS 10.12 ,
// Runner 10.15 :`log show --predicate 'subsystem == "com.pangolin.pangolin"'`
private let vpnLogObj = OSLog(subsystem: "com.pangolin.pangolin", category: "vpn")
private func vpnLog(_ message: String) {
os_log("%{public}@", log: vpnLogObj, type: .default, message)
NSLog("[pangolin/vpn] %@", message) // stderr,flutter run
}
final class VpnChannel: NSObject {
private static let tunnelBundleId = "com.pangolin.pangolin.PacketTunnel"
private var statusSink: FlutterEventSink?
private var statsSink: FlutterEventSink?
private var statusObserver: NSObjectProtocol?
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()
let method = FlutterMethodChannel(name: "pangolin/vpn",
binaryMessenger: registrar.messenger)
method.setMethodCallHandler(instance.handle)
FlutterEventChannel(name: "pangolin/vpn/status", binaryMessenger: registrar.messenger)
.setStreamHandler(StatusStreamHandler(owner: instance))
FlutterEventChannel(name: "pangolin/vpn/stats", binaryMessenger: registrar.messenger)
.setStreamHandler(StatsStreamHandler(owner: instance))
instance.observeStatus()
// System Extension: sysext(on-demand/)
// , app primeExistingConnection start()
// sysext (,command.sock ,
// ) register : replace,
// bundle
instance.activateOnLaunch()
}
/// / sysext(): bundle
private func activateOnLaunch() {
Task {
do {
vpnLog("launch: 主动激活 System Extension(确保运行的是 bundle 内最新版)…")
try await activateSystemExtensionIfNeeded()
vpnLog("launch: System Extension 激活/更新完成 ✓")
} catch {
let ns = error as NSError
vpnLog("launch: System Extension 激活失败 ✗ code=\(ns.code) desc=\(ns.localizedDescription)")
}
}
}
// MethodChannel
private func handle(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
switch call.method {
case "start":
guard let configJson = call.arguments as? String else {
result(FlutterError(code: "bad_args", message: "expected config json", details: nil))
return
}
Task { await self.start(configJson, result) }
case "stop":
Task { await self.stop(result) }
case "getStatus":
result(Self.statusString(manager?.connection.status ?? .invalid))
case "selectOutbound":
// TODO: sendProviderMessage / libbox CommandClient outbound
result(nil)
case "getActiveOutbound":
result("auto")
case "setKillSwitch":
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)
}
}
private func start(_ configJson: String, _ result: @escaping FlutterResult) async {
vpnLog("start() 收到调用, config 长度=\(configJson.count) bytes")
do {
vpnLog("step① 激活 System Extension …")
try await activateSystemExtensionIfNeeded()
vpnLog("step① System Extension 激活完成 ✓")
vpnLog("step② 装配 NETunnelProviderManager …")
let mgr = try await loadOrCreateManager()
self.manager = mgr
vpnLog("step② manager 就绪, 当前隧道状态=\(Self.statusString(mgr.connection.status))")
vpnLog("step③ startVPNTunnel(options: configContent) …")
try mgr.connection.startVPNTunnel(options: [
"configContent": configJson as NSString,
])
vpnLog("step③ startVPNTunnel 调用已返回(实际起停由 NEVPNStatus 流驱动)✓")
result(nil)
} catch {
let ns = error as NSError
vpnLog("start FAILED ✗ domain=\(ns.domain) code=\(ns.code) desc=\(ns.localizedDescription) userInfo=\(ns.userInfo)")
result(FlutterError(code: "start_failed", message: error.localizedDescription, details: nil))
}
}
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)
}
// NETunnelProviderManager
private func loadOrCreateManager() async throws -> NETunnelProviderManager {
let all = try await NETunnelProviderManager.loadAllFromPreferences()
vpnLog(" loadAllFromPreferences: 已有 \(all.count) 个 VPN 配置")
let mgr = all.first ?? NETunnelProviderManager()
let proto = (mgr.protocolConfiguration as? NETunnelProviderProtocol) ?? NETunnelProviderProtocol()
proto.providerBundleIdentifier = Self.tunnelBundleId
proto.serverAddress = "Pangolin" //
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
vpnLog(" manager 保存+重载完成 ✓")
return mgr
}
// KillSwitch()NE
// killSwitchEnabled manager NE (,):
// 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 = false
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 配置: enforceRoutes=\(killSwitchEnabled) onDemand=\(killSwitchEnabled) includeAllNetworks=false")
}
/// 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 {
vpnLog(" 提交 OSSystemExtensionRequest.activationRequest(id=\(Self.tunnelBundleId)) …")
vpnLog(" 主 bundle=\(Bundle.main.bundlePath)")
let sysextDir = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/SystemExtensions").path
vpnLog(" SystemExtensions 目录=\(sysextDir) 内容=\((try? FileManager.default.contentsOfDirectory(atPath: sysextDir)) ?? ["<读取失败>"])")
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
let req = OSSystemExtensionRequest.activationRequest(
forExtensionWithIdentifier: Self.tunnelBundleId, queue: .main)
let delegate = SysExtActivationDelegate(continuation: cont)
self.sysextDelegate = delegate //
req.delegate = delegate
OSSystemExtensionManager.shared.submitRequest(req)
vpnLog(" submitRequest 已提交, 等待 sysextd 回调(didFinish / didFail / needsApproval)…")
}
}
// /
private func observeStatus() {
// StatsClient statsSink
statsClient.onStats = { [weak self] stats in self?.statsSink?(stats) }
statusObserver = NotificationCenter.default.addObserver(
forName: .NEVPNStatusDidChange, object: nil, queue: .main
) { [weak self] note in
guard let self, let conn = note.object as? NEVPNConnection else { return }
let s = Self.statusString(conn.status)
vpnLog("NEVPNStatus 变化 → \(s) (raw=\(conn.status.rawValue))")
self.statusSink?(s)
// sendProviderMessage
switch conn.status {
case .connected:
if let s = conn as? NETunnelProviderSession {
self.statsClient.start(session: s)
}
case .disconnected, .invalid:
self.statsClient.stop()
default:
break
}
}
// (sysext)app connected .connected
// observer statsClient start
// manager( onStatsListen )
Task { await self.primeExistingConnection() }
}
/// manager (StatsClient.start )
private func primeExistingConnection() async {
guard let all = try? await NETunnelProviderManager.loadAllFromPreferences(),
let mgr = all.first else { return }
await MainActor.run {
self.manager = mgr
if mgr.connection.status == .connected, let s = mgr.connection as? NETunnelProviderSession {
vpnLog("启动时检测到隧道已连接(raw=\(mgr.connection.status.rawValue)),主动启动实时统计")
self.statusSink?(Self.statusString(mgr.connection.status))
self.statsClient.start(session: s)
}
}
}
fileprivate func onStatusListen(_ sink: @escaping FlutterEventSink) {
statusSink = sink
sink(Self.statusString(manager?.connection.status ?? .invalid))
}
fileprivate func onStatusCancel() { statusSink = nil }
fileprivate func onStatsListen(_ sink: @escaping FlutterEventSink) {
statsSink = sink
// StatsClient( sendProviderMessage ) connected
// onStats ; sink
if manager?.connection.status == .connected,
let s = manager?.connection as? NETunnelProviderSession {
statsClient.start(session: s)
}
}
fileprivate func onStatsCancel() { statsSink = nil }
private static func statusString(_ s: NEVPNStatus) -> String {
switch s {
case .connected: return "on"
case .connecting, .reasserting: return "connecting"
case .disconnecting: return "disconnecting"
case .disconnected, .invalid: return "off"
@unknown default: return "error"
}
}
}
// EventChannel
private final class StatusStreamHandler: NSObject, FlutterStreamHandler {
weak var owner: VpnChannel?
init(owner: VpnChannel) { self.owner = owner }
func onListen(withArguments _: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
owner?.onStatusListen(events); return nil
}
func onCancel(withArguments _: Any?) -> FlutterError? { owner?.onStatusCancel(); return nil }
}
private final class StatsStreamHandler: NSObject, FlutterStreamHandler {
weak var owner: VpnChannel?
init(owner: VpnChannel) { self.owner = owner }
func onListen(withArguments _: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
owner?.onStatsListen(events); return nil
}
func onCancel(withArguments _: Any?) -> FlutterError? { owner?.onStatsCancel(); return nil }
}
// System Extension ,
// didFinishWithResult( await ,
// )
private final class SysExtActivationDelegate: NSObject, OSSystemExtensionRequestDelegate {
private let continuation: CheckedContinuation<Void, Error>
private var resumed = false
init(continuation: CheckedContinuation<Void, Error>) { self.continuation = continuation }
func request(_ request: OSSystemExtensionRequest,
didFinishWithResult result: OSSystemExtensionRequest.Result) {
vpnLog("sysext didFinishWithResult ✓ result=\(result.rawValue) (0=completed, 1=willCompleteAfterReboot)")
guard !resumed else { return }
resumed = true
continuation.resume()
}
func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) {
let ns = error as NSError
// OSSystemExtensionErrorDomain code :1 unknown,2 missingEntitlement,
// 3 unsupportedParentBundleLocation,4 extensionNotFound,8 codeSignatureInvalid,
// 9 validationFailed,10 forbiddenBySystemPolicy,13 authorizationRequired
vpnLog("sysext didFailWithError ✗ domain=\(ns.domain) code=\(ns.code) desc=\(ns.localizedDescription)")
guard !resumed else { return }
resumed = true
continuation.resume(throwing: error)
}
func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) {
vpnLog("sysext requestNeedsUserApproval —— 需在 系统设置 → 隐私与安全性 点「允许」(等待中…)")
}
func request(_ request: OSSystemExtensionRequest,
actionForReplacingExtension existing: OSSystemExtensionProperties,
withExtension ext: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction {
vpnLog("sysext 替换扩展: 已装 v\(existing.bundleVersion)/\(existing.bundleShortVersion) → 新 v\(ext.bundleVersion)/\(ext.bundleShortVersion), 选择 replace")
return .replace
}
}