// VpnManager.swift — 主 App 侧 NETunnelProviderManager 封装(M3 PoC 实现) // // 职责: // 1. loadAllFromPreferences / saveToPreferences — 首次安装时建立 VPN 配置条目 // 2. start / stop — 通过 NETunnelProviderSession 驱动 PacketTunnel extension // 3. sendProviderMessage — 透传 IPC 消息到 PacketTunnelProvider.handleAppMessage // 4. NEVPNStatus 订阅 → VpnStatusStreamHandler.shared 推送到 Dart 侧 // // AppDelegate 持有本类单例,在 MethodChannel handler 中调用其方法。 // // 红线词检查(⚠️ 产品文案不得出现:VPN / 翻墙 / 科学上网 / 突破封锁 / 自由穿越 / // Go anywhere):localizedDescription 使用「Pangolin 加速」口径。 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" // App Group:与 PacketTunnel extension 共享 UserDefaults 容器 private let appGroup = "group.com.pangolin.pangolinVpn" 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 // ⚠️ 红线词规范:localizedDescription 用「加速」口径,不得出现 VPN/翻墙 等词。 manager.localizedDescription = "Pangolin 加速" 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 } // 写入 App Group UserDefaults,供 PacketTunnel extension 恢复配置 if let shared = UserDefaults(suiteName: appGroup) { shared.set(configJson, forKey: "lastConfigJson") } 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]) } }