From b621d1754aa789a0c735064d941149d7534ca452 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Fri, 19 Jun 2026 15:20:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(client/macos):=20PacketTunnelProvider=20li?= =?UTF-8?q?bbox=20=E9=9B=86=E6=88=90(=E7=BC=96=E8=AF=91+=E9=93=BE=E6=8E=A5?= =?UTF-8?q?=E9=80=9A=E8=BF=87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CommandServer 装配(LibboxSetup→NewCommandServer→start→startOrReloadService)+ 完整 LibboxPlatformInterface:openTun(TunOptions→NEPacketTunnelNetworkSettings, getpeername+utun ctl_id 取 fd)、默认接口监控(NWPathMonitor)、接口枚举、socket 绑定;LibboxCommandServerHandler 回调。 gomobile 桥接要点:实现接口用 XxxProtocol、接口型参数/返回用 (any XxxProtocol)、 三个方法被 Swift 重命名(autoDetectControl/send/usePlatformAutoDetectControl)、 CTLIOCGINFO 按 _IOWR 定义计算。 链接需补系统框架:SystemConfiguration + AppKit + libresolv(libbox 内部 系统代理/网络监控代码引用)。 Co-Authored-By: Claude Opus 4.8 --- .../PacketTunnel/PacketTunnelProvider.swift | 356 +++++++++++++----- 1 file changed, 261 insertions(+), 95 deletions(-) diff --git a/client/macos/PacketTunnel/PacketTunnelProvider.swift b/client/macos/PacketTunnel/PacketTunnelProvider.swift index 800444b..4205c9e 100644 --- a/client/macos/PacketTunnel/PacketTunnelProvider.swift +++ b/client/macos/PacketTunnel/PacketTunnelProvider.swift @@ -1,135 +1,301 @@ -// PacketTunnelProvider.swift — Pangolin macOS System Extension(方案B / P1 骨架) +// PacketTunnelProvider.swift — Pangolin macOS 隧道扩展(方案B / P1) // -// 角色:运行在独立 sysex 进程的 NEPacketTunnelProvider。把嵌入的 sing-box(libbox) -// 拉起来建 TUN,免 root、自包含(替代 PoC 的 `sudo sing-box run` 外部二进制)。 +// NEPacketTunnelProvider 子进程,把嵌入的 sing-box(libbox)拉起来建 TUN,免 root、 +// 自包含。数据流见 docs/p1-macos-system-extension.md。 // -// 数据流(详见 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 补齐(本文件只搭关键路径)。 +// gomobile 约定:每个 Go 接口生成同名 protocol + class;Swift 里实现用 `XxxProtocol`, +// 参数/返回用同名 class。NE 生命周期 override 用 completion-handler(规避 Swift 6.2 +// async→ObjC 桥接 thunk 的编译器崩溃)。 +import Darwin +import Foundation +import Libbox +import Network import NetworkExtension import os -// import Libbox // ← gomobile bind 产物;加入 xcframework 后解开 private let log = Logger(subsystem: "com.pangolin.pangolin.PacketTunnel", category: "provider") +private let appGroup = "group.com.pangolin.pangolin" -class PacketTunnelProvider: NEPacketTunnelProvider { - // libbox 服务句柄(类型来自 Libbox 模块,加入 xcframework 后改为 LibboxBoxService)。 - private var boxService: AnyObject? - private var platformInterface: PangolinPlatformInterface? +final class PacketTunnelProvider: NEPacketTunnelProvider { + private var commandServer: LibboxCommandServer? + private var platform: PangolinPlatformInterface? - // App Group 共享容器:放 sing-box 工作目录(缓存/日志)。主 app 与扩展同路径。 - private static let appGroup = "group.com.pangolin.pangolin" - - // 用 completion-handler 形式(而非 async):Swift 6.2 编译器在为 async 重写 - // 生成 ObjC 桥接 thunk 时会崩(emitInjectLoadableEnum),completion-handler 直接 - // 就是 @objc 原型,绕开该 bug。成功调 completionHandler(nil),失败传 error。 override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) { log.info("startTunnel") + do { + let configContent = try resolveConfig(options) - // 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 let base = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { + throw simpleError("no app group container") + } + let work = base.appendingPathComponent("work", isDirectory: true) + try? FileManager.default.createDirectory(at: work, withIntermediateDirectories: true) + + let setup = LibboxSetupOptions() + setup.basePath = base.path + setup.workingPath = work.path + setup.tempPath = NSTemporaryDirectory() + var setupErr: NSError? + LibboxSetup(setup, &setupErr) + if let setupErr { throw setupErr } + + let platform = PangolinPlatformInterface(provider: self) + self.platform = platform + + var newErr: NSError? + guard let server = LibboxNewCommandServer(self, platform, &newErr) else { + throw newErr ?? simpleError("LibboxNewCommandServer returned nil") + } + try server.start() + try server.startOrReloadService(configContent, options: nil) + self.commandServer = server + + log.info("startTunnel: service started") + completionHandler(nil) + } catch { + log.error("startTunnel failed: \(error.localizedDescription)") + completionHandler(error) } - guard !configContent.isEmpty else { - completionHandler(NSError(domain: "pangolin.tunnel", code: 1, - userInfo: [NSLocalizedDescriptionKey: "empty sing-box config"])) - return - } - - // 2) libbox 基础路径(全部落在 App Group 容器内,扩展沙盒可写)。 - guard let base = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: Self.appGroup) else { - completionHandler(NSError(domain: "pangolin.tunnel", code: 2, - userInfo: [NSLocalizedDescriptionKey: "no app group container"])) - return - } - 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 { completionHandler(setupError); return } - - let platform = PangolinPlatformInterface(provider: self) - self.platformInterface = platform - - var newError: NSError? - guard let service = LibboxNewService(configContent, platform, &newError) else { - completionHandler(newError ?? NSError(domain: "pangolin.tunnel", code: 3)); return - } - self.boxService = service - do { try service.start() } catch { completionHandler(error); return } - completionHandler(nil) // 隧道已起 - return - ──────────────────────────────────────────────────────────────────── */ - - // 骨架占位:xcframework 接入前,直接回错,避免静默假成功。 - completionHandler(NSError(domain: "pangolin.tunnel", code: 99, userInfo: [ - NSLocalizedDescriptionKey: - "Libbox 尚未接入(见 docs/p1-macos-system-extension.md 步骤 3)", - ])) } override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { log.info("stopTunnel reason=\(reason.rawValue)") - /* if let service = boxService as? LibboxBoxService { try? service.close() } */ - boxService = nil - platformInterface = nil + try? commandServer?.closeService() + commandServer?.close() + commandServer = nil + platform?.stopMonitor() + platform = nil completionHandler() } - // 主 app 经 sendProviderMessage 下发的控制(切节点等);也可改走 libbox CommandServer。 override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { completionHandler?(nil) } - private static func readSharedConfig() throws -> String { + private func resolveConfig(_ options: [String: NSObject]?) throws -> String { + if let inline = options?["configContent"] as? String, !inline.isEmpty { + return inline + } guard let base = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { - throw NSError(domain: "pangolin.tunnel", code: 2) + throw simpleError("no app group container") } - let url = base.appendingPathComponent("config.json") - return try String(contentsOf: url, encoding: .utf8) + let content = (try? String(contentsOf: base.appendingPathComponent("config.json"), + encoding: .utf8)) ?? "" + guard !content.isEmpty else { throw simpleError("empty sing-box config") } + return content } } -/// 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 { +// ── LibboxCommandServerHandler:服务端事件回调 ────────────────────────────── +extension PacketTunnelProvider: LibboxCommandServerHandlerProtocol { + func serviceReload() throws { + if let cfg = try? resolveConfig(nil) { + try commandServer?.startOrReloadService(cfg, options: nil) + } + } + func serviceStop() throws { cancelTunnelWithError(nil) } + func postServiceClose() {} + func getSystemProxyStatus() throws -> LibboxSystemProxyStatus { + let s = LibboxSystemProxyStatus(); s.available = false; s.enabled = false; return s + } + func setSystemProxyEnabled(_ enabled: Bool) throws {} + func writeStatus(_ message: LibboxStatusMessage?) {} + func writeLogs(_ messageList: (any LibboxLogIteratorProtocol)?) {} + func writeConnectionEvents(_ events: LibboxConnectionEvents?) {} + func writeGroups(_ message: (any LibboxOutboundGroupIteratorProtocol)?) {} + func writeDebugMessage(_ message: String?) {} + func setDefaultLogLevel(_ level: Int32) {} + func initializeClashMode(_ modeList: (any LibboxStringIteratorProtocol)?, currentMode: String?) {} + func updateClashMode(_ newMode: String?) {} + func clearLogs() {} + func connected() {} + func disconnected(_ message: String?) {} +} + +// ── libbox ↔ NetworkExtension 桥(LibboxPlatformInterface)────────────────── +final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol { private weak var provider: NEPacketTunnelProvider? + private var monitor: NWPathMonitor? + private var defaultInterfaceIndex: Int32 = -1 + private let monitorQueue = DispatchQueue(label: "pangolin.tunnel.pathmonitor") init(provider: NEPacketTunnelProvider) { self.provider = provider + super.init() } - // 示意:openTun 的核心是把 libbox 的网络配置翻译成 NEPacketTunnelNetworkSettings。 - // 真实签名以 Libbox 头文件为准(LibboxTunOptions / 返回 tun fd)。 + func stopMonitor() { monitor?.cancel(); monitor = nil } + + // MARK: openTun —— 据 TunOptions 建 NEPacketTunnelNetworkSettings,返回 utun fd + func openTun(_ options: (any LibboxTunOptionsProtocol)?, ret0_: UnsafeMutablePointer?) throws { + guard let options, let provider else { throw simpleError("openTun: no options/provider") } + let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1") + settings.mtu = NSNumber(value: options.getMTU()) + + let v4addrs = routePrefixes(options.getInet4Address()) + if !v4addrs.isEmpty { + let ipv4 = NEIPv4Settings(addresses: v4addrs.map(\.address), + subnetMasks: v4addrs.map(\.mask)) + ipv4.includedRoutes = includedRoutes4(options) + settings.ipv4Settings = ipv4 + } + let v6addrs = routePrefixes(options.getInet6Address()) + if !v6addrs.isEmpty { + let ipv6 = NEIPv6Settings(addresses: v6addrs.map(\.address), + networkPrefixLengths: v6addrs.map { NSNumber(value: $0.prefix) }) + ipv6.includedRoutes = includedRoutes6(options) + settings.ipv6Settings = ipv6 + } + if let dns = try? options.getDNSServerAddress(), !dns.value.isEmpty { + settings.dnsSettings = NEDNSSettings(servers: [dns.value]) + } + + let sem = DispatchSemaphore(value: 0) + var applyError: Error? + provider.setTunnelNetworkSettings(settings) { err in applyError = err; sem.signal() } + sem.wait() + if let applyError { throw applyError } + + guard let fd = Self.tunFd() else { throw simpleError("openTun: utun fd not found") } + ret0_?.pointee = fd + } + + // MARK: 默认接口监控 + func startDefaultInterfaceMonitor(_ listener: (any LibboxInterfaceUpdateListenerProtocol)?) throws { + let m = NWPathMonitor() + monitor = m + m.pathUpdateHandler = { [weak self] path in + guard let self else { return } + let iface = path.availableInterfaces.first { path.usesInterfaceType($0.type) } + ?? path.availableInterfaces.first + let name = iface?.name ?? "" + let index = Int32(iface?.index ?? 0) + self.defaultInterfaceIndex = index + listener?.updateDefaultInterface(name, interfaceIndex: index, + isExpensive: path.isExpensive, + isConstrained: path.isConstrained) + } + m.start(queue: monitorQueue) + } + func closeDefaultInterfaceMonitor(_ listener: (any LibboxInterfaceUpdateListenerProtocol)?) throws { + stopMonitor() + } + + // MARK: 把 socket 绑到默认物理接口 + func autoDetectControl(_ fd: Int32) throws { + guard defaultInterfaceIndex > 0 else { return } + var idx = defaultInterfaceIndex + _ = setsockopt(fd, IPPROTO_IP, IP_BOUND_IF, &idx, socklen_t(MemoryLayout.size)) + _ = setsockopt(fd, IPPROTO_IPV6, IPV6_BOUND_IF, &idx, socklen_t(MemoryLayout.size)) + } + func usePlatformAutoDetectControl() -> Bool { true } + + // MARK: 系统接口枚举 + func getInterfaces() throws -> LibboxNetworkInterfaceIteratorProtocol { + InterfaceIterator(items: Self.enumerateInterfaces()) + } + + // MARK: 其余协议方法(取合理默认) + func underNetworkExtension() -> Bool { true } + func useProcFS() -> Bool { false } + func includeAllNetworks() -> Bool { false } + func clearDNSCache() {} + func readWIFIState() -> LibboxWIFIState? { nil } + func systemCertificates() -> (any LibboxStringIteratorProtocol)? { nil } + func localDNSTransport() -> (any LibboxLocalDNSTransportProtocol)? { nil } + func send(_ notification: LibboxNotification?) throws {} + func findConnectionOwner(_ ipProtocol: Int32, sourceAddress: String?, sourcePort: Int32, + destinationAddress: String?, destinationPort: Int32) throws -> LibboxConnectionOwner { + throw simpleError("findConnectionOwner: unsupported") + } + + // ── 工具 ── + private func routePrefixes(_ it: (any LibboxRoutePrefixIteratorProtocol)?) -> [(address: String, mask: String, prefix: Int32)] { + var out: [(address: String, mask: String, prefix: Int32)] = [] + guard let it else { return out } + while it.hasNext() { + if let p = it.next() { out.append((address: p.address(), mask: p.mask(), prefix: p.prefix())) } + } + return out + } + private func includedRoutes4(_ o: any LibboxTunOptionsProtocol) -> [NEIPv4Route] { + let r = routePrefixes(o.getInet4RouteAddress()) + if r.isEmpty { return [NEIPv4Route.default()] } + return r.map { NEIPv4Route(destinationAddress: $0.address, subnetMask: $0.mask) } + } + private func includedRoutes6(_ o: any LibboxTunOptionsProtocol) -> [NEIPv6Route] { + let r = routePrefixes(o.getInet6RouteAddress()) + if r.isEmpty { return [NEIPv6Route.default()] } + return r.map { NEIPv6Route(destinationAddress: $0.address, + networkPrefixLength: NSNumber(value: $0.prefix)) } + } + + // utun fd:getpeername 得 sockaddr_ctl,匹配 utun 控制 id(WireGuard 技法) + private static func tunFd() -> Int32? { + var ctlInfo = ctl_info() + withUnsafeMutableBytes(of: &ctlInfo.ctl_name) { ptr in + _ = "com.apple.net.utun_control".withCString { + strncpy(ptr.baseAddress!.assumingMemoryBound(to: CChar.self), $0, ptr.count - 1) + } + } + // CTLIOCGINFO = _IOWR('N', 3, struct ctl_info)(Swift 不导出该宏,按定义算) + let ctliocginfo: UInt = 0x8000_0000 | 0x4000_0000 + | ((UInt(MemoryLayout.stride) & 0x1fff) << 16) + | (UInt(UInt8(ascii: "N")) << 8) | 3 + for fd in Int32(0)...1024 { + var addr = sockaddr_ctl() + var len = socklen_t(MemoryLayout.size) + let ok = withUnsafeMutablePointer(to: &addr) { p in + p.withMemoryRebound(to: sockaddr.self, capacity: 1) { getpeername(fd, $0, &len) } + } + if ok == 0, addr.sc_family == AF_SYSTEM { + if ioctl(fd, ctliocginfo, &ctlInfo) == 0, addr.sc_id == ctlInfo.ctl_id { + return fd + } + } + } + return nil + } + + private static func enumerateInterfaces() -> [LibboxNetworkInterface] { + var result: [String: LibboxNetworkInterface] = [:] + var head: UnsafeMutablePointer? + guard getifaddrs(&head) == 0 else { return [] } + defer { freeifaddrs(head) } + var cur = head + while let p = cur { + let name = String(cString: p.pointee.ifa_name) + if result[name] == nil { + let i = LibboxNetworkInterface() + i.name = name + i.index = Int32(if_nametoindex(p.pointee.ifa_name)) + result[name] = i + } + cur = p.pointee.ifa_next + } + return Array(result.values) + } +} + +// LibboxNetworkInterfaceIterator 适配器。 +final class InterfaceIterator: NSObject, LibboxNetworkInterfaceIteratorProtocol { + private var idx = 0 + private let items: [LibboxNetworkInterface] + init(items: [LibboxNetworkInterface]) { self.items = items } + func hasNext() -> Bool { idx < items.count } + func next() -> LibboxNetworkInterface? { + guard idx < items.count else { return nil } + defer { idx += 1 } + return items[idx] + } +} + +private func simpleError(_ msg: String) -> NSError { + NSError(domain: "pangolin.tunnel", code: 1, userInfo: [NSLocalizedDescriptionKey: msg]) }