// PacketTunnelProvider.swift — Pangolin macOS System Extension(方案B / P1 骨架) // // 角色:运行在独立 sysex 进程的 NEPacketTunnelProvider。把嵌入的 sing-box(libbox) // 拉起来建 TUN,免 root、自包含(替代 PoC 的 `sudo sing-box run` 外部二进制)。 // // 数据流(详见 docs/p1-macos-system-extension.md): // 主 app(Flutter)经 NETunnelProviderManager.startVPNTunnel(options:) 下发 sing-box // JSON → 本扩展 startTunnel 收到 → LibboxSetup(App Group 路径)→ LibboxNewService(json, // platformInterface)→ start;libbox 回调 platformInterface.openTun 时,我们据其网络配置 // 建 NEPacketTunnelNetworkSettings + setTunnelNetworkSettings,把 packetFlow 的 fd 交回。 // 运行态速率/状态经 libbox CommandServer(loopback)被主 app 的 CommandClient 订阅。 // // ⚠️ 编译前置: // 1. 工程已加入 Libbox.xcframework(scripts/build-libbox.sh apple macos 产出)。 // 2. 本 target 链接 NetworkExtension + Libbox。 // 3. PangolinPlatformInterface 对 LibboxPlatformInterfaceProtocol 的**完整**实现需 // 对照官方 sing-box-for-apple 的 ExtensionPlatformInterface 补齐(本文件只搭关键路径)。 import NetworkExtension import os // import Libbox // ← gomobile bind 产物;加入 xcframework 后解开 private let log = Logger(subsystem: "com.pangolin.pangolin.tunnel", category: "provider") class PacketTunnelProvider: NEPacketTunnelProvider { // libbox 服务句柄(类型来自 Libbox 模块,加入 xcframework 后改为 LibboxBoxService)。 private var boxService: AnyObject? private var platformInterface: PangolinPlatformInterface? // App Group 共享容器:放 sing-box 工作目录(缓存/日志)。主 app 与扩展同路径。 private static let appGroup = "group.com.pangolin.pangolin" override func startTunnel(options: [String: NSObject]?) async throws { log.info("startTunnel") // 1) sing-box JSON:优先取 startVPNTunnel 下发的 options["configContent"], // 否则回退读 App Group 共享文件(主 app 写入)。 let configContent: String if let inline = options?["configContent"] as? String, !inline.isEmpty { configContent = inline } else { configContent = (try? Self.readSharedConfig()) ?? "" } guard !configContent.isEmpty else { throw NSError(domain: "pangolin.tunnel", code: 1, userInfo: [NSLocalizedDescriptionKey: "empty sing-box config"]) } // 2) libbox 基础路径(全部落在 App Group 容器内,sysess 沙盒可写)。 guard let base = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: Self.appGroup) else { throw NSError(domain: "pangolin.tunnel", code: 2, userInfo: [NSLocalizedDescriptionKey: "no app group container"]) } let work = base.appendingPathComponent("work", isDirectory: true) try? FileManager.default.createDirectory(at: work, withIntermediateDirectories: true) /* ── 加入 Libbox.xcframework 后解开以下真实接线 ────────────────────── var setupError: NSError? let setup = LibboxSetupOptions() setup.basePath = base.path setup.workingPath = work.path setup.tempPath = NSTemporaryDirectory() LibboxSetup(setup, &setupError) if let setupError { throw setupError } let platform = PangolinPlatformInterface(provider: self) self.platformInterface = platform var newError: NSError? guard let service = LibboxNewService(configContent, platform, &newError) else { throw newError ?? NSError(domain: "pangolin.tunnel", code: 3) } self.boxService = service try service.start() ──────────────────────────────────────────────────────────────────── */ // 骨架占位:xcframework 接入前,startTunnel 直接抛错,避免静默假成功。 throw NSError(domain: "pangolin.tunnel", code: 99, userInfo: [ NSLocalizedDescriptionKey: "Libbox 尚未接入(见 docs/p1-macos-system-extension.md 步骤 3)", ]) } override func stopTunnel(with reason: NEProviderStopReason) async { log.info("stopTunnel reason=\(reason.rawValue)") /* if let service = boxService as? LibboxBoxService { try? service.close() } */ boxService = nil platformInterface = nil } // 主 app 经 sendProviderMessage 下发的控制(切节点等);也可改走 libbox CommandServer。 override func handleAppMessage(_ messageData: Data) async -> Data? { nil } private static func readSharedConfig() throws -> String { guard let base = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { throw NSError(domain: "pangolin.tunnel", code: 2) } let url = base.appendingPathComponent("config.json") return try String(contentsOf: url, encoding: .utf8) } } /// libbox ↔ NetworkExtension 桥(LibboxPlatformInterfaceProtocol)。 /// /// 关键职责是 openTun:libbox 把所需网络参数(地址/路由/DNS/MTU)交给我们,我们据此建 /// NEPacketTunnelNetworkSettings、setTunnelNetworkSettings,并返回 packetFlow 对应的 tun fd。 /// /// ⚠️ 本类只是骨架声明。完整协议(openTun / writeLog / useProcFS / findConnectionOwner / /// defaultInterfaceMonitor / getInterfaces / underNetworkExtension / systemCertificates 等) /// 需对照官方 ExtensionPlatformInterface 补齐后才能编译通过。见安装文档步骤 3。 final class PangolinPlatformInterface: NSObject { private weak var provider: NEPacketTunnelProvider? init(provider: NEPacketTunnelProvider) { self.provider = provider } // 示意:openTun 的核心是把 libbox 的网络配置翻译成 NEPacketTunnelNetworkSettings。 // 真实签名以 Libbox 头文件为准(LibboxTunOptions / 返回 tun fd)。 }