// VpnChannel.swift — 主 app 侧 VPN 控制(方案B / P1 骨架) // // 把 Flutter 的 pangolin/vpn(MethodChannel)+ pangolin/vpn/status、pangolin/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 成 ,排障时看不到内容)。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 var statsTimer: Timer? private var manager: NETunnelProviderManager? private var sysextDelegate: SysExtActivationDelegate? 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() } // ── 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": // includeAllNetworks / on-demand 实现 kill switch(后续)。 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 { 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 vpnLog(" saveToPreferences(providerBundleId=\(Self.tunnelBundleId)) …") try await mgr.saveToPreferences() try await mgr.loadFromPreferences() // 保存后重载,拿到有效 connection vpnLog(" manager 保存+重载完成 ✓") return mgr } // 请求系统加载/更新 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) 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() { statusObserver = NotificationCenter.default.addObserver( forName: .NEVPNStatusDidChange, object: nil, queue: .main ) { [weak self] note in guard let conn = note.object as? NEVPNConnection else { return } let s = Self.statusString(conn.status) vpnLog("NEVPNStatus 变化 → \(s) (raw=\(conn.status.rawValue))") self?.statusSink?(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 // TODO(P1): 接 libbox CommandClient 订阅实时速率;骨架先每秒推占位 0。 statsTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in self?.statsSink?(["up": 0, "down": 0, "uplinkTotal": 0, "downlinkTotal": 0]) } } fileprivate func onStatsCancel() { statsTimer?.invalidate(); statsTimer = nil; 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 private var resumed = false init(continuation: CheckedContinuation) { 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 } }