// VpnManager.swift — 主 App 侧 NETunnelProviderManager 封装 // // 职责: // 1. loadAllFromPreferences / saveToPreferences — 首次安装时建立 VPN 配置条目 // 2. start / stop — 通过 NETunnelProviderSession 驱动 PacketTunnel extension // 3. sendProviderMessage — 透传 IPC 消息到 PacketTunnelProvider.handleAppMessage // // AppDelegate 持有本类单例,在 MethodChannel handler 中调用其方法。 // // TODO(11F): 实现 start/stop 前的证书/权限检查;订阅 NEVPNStatus 通知并 // 通过 VpnStatusStreamHandler.shared.eventSink 推送到 Dart 侧。 import Foundation import NetworkExtension // ── 流处理器(Flutter EventChannel stream handlers)─────────────── /// status 流:推送 "off" / "connecting" / "on" / "error" 字符串 class VpnStatusStreamHandler: NSObject, FlutterStreamHandler { static let shared = VpnStatusStreamHandler() private(set) var eventSink: FlutterEventSink? func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { eventSink = events return nil } func onCancel(withArguments arguments: Any?) -> FlutterError? { eventSink = nil return nil } func push(_ status: String) { DispatchQueue.main.async { self.eventSink?(status) } } } /// stats 流:推送 [String: Any] 字典,字段同 vpn_bridge.dart VpnStatsEvent schema class VpnStatsStreamHandler: NSObject, FlutterStreamHandler { static let shared = VpnStatsStreamHandler() private(set) var eventSink: FlutterEventSink? func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { eventSink = events return nil } func onCancel(withArguments arguments: Any?) -> FlutterError? { eventSink = nil return nil } func push(_ stats: [String: Any]) { DispatchQueue.main.async { self.eventSink?(stats) } } } // ── VPN 管理器 ──────────────────────────────────────────────────── class VpnManager: NSObject { static let shared = VpnManager() private var tunnelManager: NETunnelProviderManager? // Bundle ID 与 PacketTunnel target PRODUCT_BUNDLE_IDENTIFIER 保持同步 private let extensionBundleId = "com.pangolin.pangolinVpn.PacketTunnel" private var statusObserver: NSObjectProtocol? private override init() { super.init() subscribeToVpnStatus() } // ── 偏好设置加载与保存 ───────────────────────────────────── /// 从系统偏好加载已存在的 VPN 配置(首次为空列表)。 /// 必须在调用 start() 前完成。 func loadFromPreferences(completion: @escaping (Error?) -> Void) { NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, error in guard let self = self else { return } if let error = error { completion(error) return } self.tunnelManager = managers?.first(where: { ($0.protocolConfiguration as? NETunnelProviderProtocol)? .providerBundleIdentifier == self.extensionBundleId }) ?? NETunnelProviderManager() completion(nil) } } /// 创建或更新 VPN 配置条目(首次安装时调用一次)。 func saveConfiguration(serverAddress: String = "pangolin", completion: @escaping (Error?) -> Void) { let manager = tunnelManager ?? NETunnelProviderManager() let proto = NETunnelProviderProtocol() proto.providerBundleIdentifier = extensionBundleId proto.serverAddress = serverAddress manager.protocolConfiguration = proto manager.localizedDescription = "穿山甲 Pangolin VPN" manager.isEnabled = true manager.saveToPreferences { [weak self] error in if error == nil { self?.tunnelManager = manager } completion(error) } } // ── 隧道控制 ────────────────────────────────────────────── /// 启动 VPN 隧道(通过 NETunnelProviderSession)。 /// [configJson]: sing-box 配置 JSON,作为 options 传入 PacketTunnelProvider.startTunnel。 func start(configJson: String, completion: @escaping (Error?) -> Void) { guard let manager = tunnelManager else { completion(makeError("VpnManager: tunnelManager not loaded. Call loadFromPreferences first.")) return } do { let options: [String: NSObject] = ["config": configJson as NSString] try manager.connection.startVPNTunnel(options: options) completion(nil) } catch { completion(error) } } /// 停止 VPN 隧道。 func stop() { tunnelManager?.connection.stopVPNTunnel() } /// 当前连接状态字符串(供 getStatus MethodChannel 方法使用)。 var statusString: String { switch tunnelManager?.connection.status ?? .invalid { case .invalid, .disconnected: return "off" case .connecting: return "connecting" case .connected: return "on" case .disconnecting: return "connecting" case .reasserting: return "connecting" @unknown default: return "error" } } // ── IPC 消息通道 ────────────────────────────────────────── /// 向 PacketTunnelProvider.handleAppMessage 发送消息。 func sendProviderMessage(_ data: Data, responseHandler: ((Data?, Error?) -> Void)?) { guard let session = tunnelManager?.connection as? NETunnelProviderSession else { responseHandler?(nil, makeError("VpnManager: no active NETunnelProviderSession")) return } do { try session.sendProviderMessage(data) { responseData in responseHandler?(responseData, nil) } } catch { responseHandler?(nil, error) } } // ── NEVPNStatus 订阅 ────────────────────────────────────── private func subscribeToVpnStatus() { statusObserver = NotificationCenter.default.addObserver( forName: NSNotification.Name.NEVPNStatusDidChange, object: nil, queue: .main ) { [weak self] _ in guard let self = self else { return } let s = self.statusString VpnStatusStreamHandler.shared.push(s) } } deinit { if let obs = statusObserver { NotificationCenter.default.removeObserver(obs) } } // ── 工具 ───────────────────────────────────────────────── private func makeError(_ message: String) -> NSError { NSError(domain: "VpnManager", code: -1, userInfo: [NSLocalizedDescriptionKey: message]) } }