From 381a621d778ea87ab026f1f13927e24aaf348064 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Mon, 22 Jun 2026 20:58:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(client/ios):=20Provider=20=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=20macOS=20libbox=20=E9=9B=86=E6=88=90(CommandServer+u?= =?UTF-8?q?tun=20fd)+=20=E5=86=85=E5=AD=98=E7=9B=91=E6=8E=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../PacketTunnel/PacketTunnelProvider.swift | 786 +++++++----------- 1 file changed, 301 insertions(+), 485 deletions(-) diff --git a/client/ios/PacketTunnel/PacketTunnelProvider.swift b/client/ios/PacketTunnel/PacketTunnelProvider.swift index f22a214..fa0cc06 100644 --- a/client/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/client/ios/PacketTunnel/PacketTunnelProvider.swift @@ -1,521 +1,337 @@ -// PacketTunnelProvider.swift — Packet Tunnel Extension 完整实现(M3 PoC) +// PacketTunnelProvider.swift — Pangolin iOS 隧道扩展 // -// Target: PacketTunnel (NEPacketTunnelProvider extension) -// Bundle: com.pangolin.pangolinVpn.PacketTunnel -// Entitlement: com.apple.developer.networking.networkextension / packet-tunnel-provider +// NEPacketTunnelProvider 子进程,把嵌入的 sing-box(libbox)拉起来建 TUN,免 root、 +// 自包含。对齐 macOS 金标准集成方式(LibboxNewCommandServer + LibboxPlatformInterfaceProtocol +// + 真 utun fd)。 // -// 架构概述: -// startTunnel → 解析 configJson → NEPacketTunnelNetworkSettings -// → LibboxNewService(libbox/sing-box 内核)→ 启动内核 -// → 包流桥接(packetFlow ↔ socketpair ↔ libbox TUN fd) -// → MemoryMonitor 开始打点 -// stopTunnel → 内核干净停止 → MemoryMonitor 输出摘要 -// handleAppMessage → JSON IPC:getStatus / getMemory / selectOutbound -// -// libbox 依赖: -// 编译时需将 app/kernel/dist/ios/Libbox.xcframework 嵌入工程。 -// gomobile bind ./experimental/libbox 生成 ObjC 桥: -// - LibboxNewService(configContent:platformInterface:disableMemoryLimit:error:) -// - LibboxService.start() / .stop() -// - LibboxPlatformInterface (ObjC protocol) -// - LibboxTunOptions (ObjC class, fields: inet4Address, inet6Address, mtu, autoRoute, …) -// -// 条件编译: -// #if canImport(Libbox) → 真机/设备 Release;否则走骨架路径,仅记录日志。 -// -// 内存上限(最大风险): -// 旧设备 NE 进程 ~15MB;新设备 ~50MB。 -// MemoryMonitor 每 10s 打点;若可用内存 < 5MB 输出 ⚠️ 警告。 -// 若在真机测试中触顶(jetsam 杀进程),回 app/kernel/build-ios.sh 裁减 -// build tags(优先去 with_gvisor → with_grpc),重出 XCFramework。 -// -// IPC 协议(handleAppMessage / sendProviderMessage): -// 请求 JSON:{ "action": "getStatus" | "getMemory" | "selectOutbound", "tag"?: "xxx" } -// 响应 JSON:{ "status": "...", "memory": "...", "error": "..." } -// -// 红线词检查: -// 本文件无用户可见文案,所有 localizedDescription 见 VpnManager.swift。 +// gomobile 约定:每个 Go 接口生成同名 protocol + class;Swift 里实现用 `XxxProtocol`, +// 参数/返回用同名 class。NE 生命周期 override 用 completion-handler(规避 Swift 6.2 +// async→ObjC 桥接 thunk 的编译器崩溃)。 -import NetworkExtension import Darwin import Foundation -import os.log - -#if canImport(Libbox) import Libbox -#endif +import Network +import NetworkExtension +import os -// MARK: – PacketTunnelProvider +private let log = Logger(subsystem: "com.pangolin.pangolinVpn.PacketTunnel", category: "provider") +// iOS App Group 用 group. 前缀(非 macOS 的 . 前缀)。 +private let appGroup = "group.com.pangolin.pangolinVpn" -class PacketTunnelProvider: NEPacketTunnelProvider { +final class PacketTunnelProvider: NEPacketTunnelProvider { + private var commandServer: LibboxCommandServer? + private var platform: PangolinPlatformInterface? + private let memoryMonitor = MemoryMonitor() // ← iOS 新增 - // ── 常量 ──────────────────────────────────────────────────── - private let log = OSLog( - subsystem: "com.pangolin.pangolinVpn.PacketTunnel", - category: "PacketTunnel" - ) - private let appGroup = "group.com.pangolin.pangolinVpn" - /// NE 进程默认隧道 IPv4 地址(回退值,正常由 config 中 tun.address 覆盖) - private let defaultTunAddr = "172.19.0.1" - private let defaultTunMask = "255.255.255.0" - /// 隧道内部 DNS(由 sing-box DNS 接管) - private let defaultDNS = "198.18.0.1" - /// MTU(NE 建议值 1500,sing-box tun 默认 9000 - 受 NE 限制降低) - private let tunMTU: NSNumber = 1500 - - // ── 运行时状态 ────────────────────────────────────────────── - private let memoryMonitor = MemoryMonitor() - -#if canImport(Libbox) - private var libboxService: LibboxService? -#endif - - /// 桥接用 socketpair([0]=NE侧,[1]=libbox侧) - private var tunFDs: (ne: Int32, libbox: Int32) = (-1, -1) - private var isStopped = false - private let stopLock = NSLock() - - /// 上次 selectOutbound 的 tag(IPC 查询用) - private var activeOutboundTag: String = "auto" - - // ── NEPacketTunnelProvider 生命周期 ────────────────────────── - - override func startTunnel( - options: [String: NSObject]?, - completionHandler: @escaping (Error?) -> Void - ) { - os_log("PacketTunnelProvider: startTunnel begin", log: log, type: .info) - stopLock.lock() - isStopped = false - stopLock.unlock() - - // 1. 取 configJson(从 options 或 App Group UserDefaults 回退) - let configJson: String - if let opt = options?["config"] as? String, !opt.isEmpty { - configJson = opt - } else if let shared = UserDefaults(suiteName: appGroup), - let cached = shared.string(forKey: "lastConfigJson"), - !cached.isEmpty { - configJson = cached - os_log("PacketTunnelProvider: 从 App Group 读取缓存 configJson", log: log, type: .info) - } else { - os_log("PacketTunnelProvider: 无 configJson,使用内置最小配置", log: log, type: .error) - configJson = minimalFallbackConfig() - } - - // 缓存到 App Group,供下次 NE 重启恢复 - if let shared = UserDefaults(suiteName: appGroup) { - shared.set(configJson, forKey: "lastConfigJson") - } - - // 2. 解析网络参数(优先从 JSON 读 tun.address,失败走默认值) - let tunAddress = parseTunAddress(from: configJson) ?? defaultTunAddr - let dnsServers = parseDNSServers(from: configJson) ?? [defaultDNS] - - // 3. 配置 NEPacketTunnelNetworkSettings - let settings = buildNetworkSettings(tunAddress: tunAddress, dnsServers: dnsServers) - - setTunnelNetworkSettings(settings) { [weak self] error in - guard let self = self else { return } - if let error = error { - os_log("PacketTunnelProvider: setTunnelNetworkSettings 失败: %{public}@", - self.log, type: .error, error.localizedDescription) - completionHandler(error) - return - } - os_log("PacketTunnelProvider: 网络设置已应用", self.log, type: .info) - - // 4. 建立 socketpair TUN 桥 - do { - try self.setupTunBridge() - } catch { - os_log("PacketTunnelProvider: socketpair 失败: %{public}@", - self.log, type: .error, error.localizedDescription) - completionHandler(error) - return - } - - // 5. 初始化 & 启动 libbox 内核 - do { - try self.startLibbox(configJson: configJson) - } catch { - os_log("PacketTunnelProvider: libbox 启动失败: %{public}@", - self.log, type: .error, error.localizedDescription) - completionHandler(error) - return - } - - // 6. 启动包流桥接 - self.startPacketBridging() - - // 7. 启动内存监控 - self.memoryMonitor.start() - - os_log("PacketTunnelProvider: 隧道已就绪", self.log, type: .info) - completionHandler(nil) - } - } - - override func stopTunnel( - with reason: NEProviderStopReason, - completionHandler: @escaping () -> Void - ) { - os_log("PacketTunnelProvider: stopTunnel reason=%d", log: log, type: .info, reason.rawValue) - stopLock.lock() - isStopped = true - stopLock.unlock() - - // 停止内存监控,输出峰值摘要 - memoryMonitor.stop() - os_log("PacketTunnelProvider: 内存峰值摘要: %{public}@", - log, type: .info, memoryMonitor.summaryString()) - - // 停止 libbox 内核 - stopLibbox() - - // 关闭 socketpair - closeTunBridge() - - completionHandler() - } - - override func handleAppMessage( - _ messageData: Data, - completionHandler: ((Data?) -> Void)? - ) { - guard let request = try? JSONSerialization.jsonObject(with: messageData) as? [String: Any], - let action = request["action"] as? String else { - os_log("PacketTunnelProvider: handleAppMessage 无法解析 JSON", - log: log, type: .error) - completionHandler?(nil) - return - } - - os_log("PacketTunnelProvider: IPC action=%{public}@", log: log, type: .debug, action) - - var response: [String: Any] = [:] - - switch action { - - case "getStatus": - // libbox 内核运行 → "on";否则 "error" - let running: Bool -#if canImport(Libbox) - running = libboxService != nil -#else - running = false -#endif - response["status"] = running ? "on" : "error" - - case "getMemory": - // 返回当前内存打点摘要(供主 App 展示内存状态) - response["memory"] = memoryMonitor.summaryString() - response["peakRssBytes"] = memoryMonitor.peakResidentBytes - response["minAvailableBytes"] = memoryMonitor.minAvailableBytes - response["sampleCount"] = memoryMonitor.sampleCount - - case "selectOutbound": - // 切换出口节点 tag(转发给 libbox Clash API) - let tag = request["tag"] as? String ?? "auto" - activeOutboundTag = tag - selectLibboxOutbound(tag: tag) - response["activeOutbound"] = tag - - case "getActiveOutbound": - response["activeOutbound"] = activeOutboundTag - - default: - response["error"] = "unknown action: \(action)" - } - - if let data = try? JSONSerialization.data(withJSONObject: response) { - completionHandler?(data) - } else { - completionHandler?(nil) - } - } - - override func sleep(completionHandler: @escaping () -> Void) { - os_log("PacketTunnelProvider: sleep(挂起流量处理)", log: log, type: .debug) - memoryMonitor.stop() - completionHandler() - } - - override func wake() { - os_log("PacketTunnelProvider: wake(恢复流量处理)", log: log, type: .debug) - memoryMonitor.start() - } - - // MARK: – 网络设置构建 - - private func buildNetworkSettings( - tunAddress: String, - dnsServers: [String] - ) -> NEPacketTunnelNetworkSettings { - let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1") - - // IPv4 设置(全量路由:默认路由 0.0.0.0/0) - let ipv4 = NEIPv4Settings(addresses: [tunAddress], subnetMasks: [defaultTunMask]) - ipv4.includedRoutes = [NEIPv4Route.default()] - ipv4.excludedRoutes = [] - settings.ipv4Settings = ipv4 - - // DNS(由 sing-box fake-ip/tun DNS 接管) - let dns = NEDNSSettings(servers: dnsServers) - dns.matchDomains = [""] // 拦截所有域名 DNS - settings.dnsSettings = dns - - settings.mtu = tunMTU - return settings - } - - // MARK: – TUN 桥(socketpair) - - private func setupTunBridge() throws { - var fds: [Int32] = [-1, -1] - // AF_UNIX SOCK_DGRAM:每次 read/write 保留消息边界,适合 IP 包 - let ret = socketpair(AF_UNIX, SOCK_DGRAM, 0, &fds) - guard ret == 0 else { - throw makePosixError("socketpair 失败") - } - // 提升 socket 缓冲区到 4 MB(避免大流量丢包) - let bufSize: Int32 = 4 * 1024 * 1024 - setsockopt(fds[0], SOL_SOCKET, SO_SNDBUF, &bufSize, socklen_t(MemoryLayout.size)) - setsockopt(fds[0], SOL_SOCKET, SO_RCVBUF, &bufSize, socklen_t(MemoryLayout.size)) - setsockopt(fds[1], SOL_SOCKET, SO_SNDBUF, &bufSize, socklen_t(MemoryLayout.size)) - setsockopt(fds[1], SOL_SOCKET, SO_RCVBUF, &bufSize, socklen_t(MemoryLayout.size)) - tunFDs = (ne: fds[0], libbox: fds[1]) - os_log("PacketTunnelProvider: socketpair 创建成功 ne=%d libbox=%d", - log: log, type: .debug, fds[0], fds[1]) - } - - private func closeTunBridge() { - if tunFDs.ne >= 0 { - close(tunFDs.ne) - tunFDs.ne = -1 - } - if tunFDs.libbox >= 0 { - close(tunFDs.libbox) - tunFDs.libbox = -1 - } - } - - // MARK: – 包流桥接(packetFlow ↔ socketpair ↔ libbox) - - private func startPacketBridging() { - let neFD = tunFDs.ne - guard neFD >= 0 else { return } - - // Outgoing(App → 外网):packetFlow → write to neFD → libbox 读 - readFromPacketFlow(neFD: neFD) - - // Incoming(外网 → App):libbox 写 → read from neFD → packetFlow - writeToPacketFlow(neFD: neFD) - } - - /// 从 NE packetFlow 读出出站包,写入 socketpair NE 侧(libbox 从另一侧读)。 - private func readFromPacketFlow(neFD: Int32) { - packetFlow.readPacketObjects { [weak self] packets in - guard let self = self else { return } - self.stopLock.lock() - let stopped = self.isStopped - self.stopLock.unlock() - guard !stopped else { return } - - for packet in packets { - let data = packet.data - _ = data.withUnsafeBytes { ptr -> Int in - guard let base = ptr.baseAddress else { return 0 } - return write(neFD, base, data.count) - } - } - // 尾递归:持续读取 - self.readFromPacketFlow(neFD: neFD) - } - } - - /// 从 socketpair NE 侧读 libbox 写入的入站包,写回 NE packetFlow。 - private func writeToPacketFlow(neFD: Int32) { - let bufSize = 65536 + override func startTunnel(options: [String: NSObject]?, + completionHandler: @escaping (Error?) -> Void) { + log.info("startTunnel") + // libbox 启动必须放后台队列:startOrReloadService 会同步回调 openTun → + // setTunnelNetworkSettings,其完成回调要在 provider 队列上投递;若在 provider + // 队列(NE 调 startTunnel 所在队列)同步跑 startOrReloadService 会三方死锁。 DispatchQueue.global(qos: .userInitiated).async { [weak self] in - guard let self = self else { return } - var buffer = [UInt8](repeating: 0, count: bufSize) - while true { - self.stopLock.lock() - let stopped = self.isStopped - self.stopLock.unlock() - guard !stopped else { break } + guard let self else { return } + do { + let configContent = try self.resolveConfig(options) - let n = read(neFD, &buffer, bufSize) - if n <= 0 { - if errno == EINTR { continue } - break + guard let base = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { + throw simpleError("no app group container") } - let data = Data(buffer[..> 4 - let proto: NSNumber = version == 6 - ? NSNumber(value: AF_INET6) - : NSNumber(value: AF_INET) - self.packetFlow.writePackets([data], withProtocols: [proto]) + 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() + // 必须传非空 options:此版本 libbox 的 StartOrReloadService 会解引用 options, + // 传 nil 会在 command_server.go:175 触发 SIGSEGV(空指针)。 + try server.startOrReloadService(configContent, options: LibboxOverrideOptions()) + self.commandServer = server + self.memoryMonitor.start() // ← iOS 新增 + log.info("startTunnel: service started") + completionHandler(nil) + } catch { + log.error("startTunnel failed: \(error.localizedDescription, privacy: .public)") + completionHandler(error) } - os_log("PacketTunnelProvider: writeToPacketFlow 退出循环", self.log, type: .debug) } } - // MARK: – libbox 生命周期 - - private func startLibbox(configJson: String) throws { -#if canImport(Libbox) - var serviceError: NSError? = nil - let svc = LibboxNewService(configJson, self, false, &serviceError) - if let err = serviceError { throw err } - guard let svc else { - throw makeError("LibboxNewService 返回 nil") - } - var startError: NSError? = nil - svc.start(&startError) - if let err = startError { throw err } - libboxService = svc - os_log("PacketTunnelProvider: libbox 内核已启动", log: log, type: .info) -#else - os_log("PacketTunnelProvider: Libbox 未链接,骨架模式运行", log: log, type: .fault) - // 骨架模式:不抛错,让 NE 连接进入 "on" 状态(用于 UI 验收) -#endif + override func stopTunnel(with reason: NEProviderStopReason, + completionHandler: @escaping () -> Void) { + log.info("stopTunnel reason=\(reason.rawValue)") + memoryMonitor.stop() // ← iOS 新增 + log.info("memory peak: \(self.memoryMonitor.summaryString(), privacy: .public)") // ← iOS 新增 + try? commandServer?.closeService() + commandServer?.close() + commandServer = nil + platform?.stopMonitor() + platform = nil + completionHandler() } - private func stopLibbox() { -#if canImport(Libbox) - guard let svc = libboxService else { return } - var stopError: NSError? = nil - svc.stop(&stopError) - if let err = stopError { - os_log("PacketTunnelProvider: libbox stop 错误: %{public}@", - log: log, type: .error, err.localizedDescription) + override func handleAppMessage(_ messageData: Data, + completionHandler: ((Data?) -> Void)?) { + guard let req = try? JSONSerialization.jsonObject(with: messageData) as? [String: Any], + let action = req["action"] as? String else { completionHandler?(nil); return } + var resp: [String: Any] = [:] + switch action { + case "getStatus": + resp["status"] = (commandServer != nil) ? "on" : "error" + case "getMemory": + resp["memory"] = memoryMonitor.summaryString() + resp["peakRssBytes"] = memoryMonitor.peakResidentBytes + default: + resp["error"] = "unknown action: \(action)" } - libboxService = nil - os_log("PacketTunnelProvider: libbox 内核已停止", log: log, type: .info) -#endif + completionHandler?((try? JSONSerialization.data(withJSONObject: resp)) ?? nil) } - private func selectLibboxOutbound(tag: String) { -#if canImport(Libbox) - // sing-box Clash API selectOutbound(通过 libbox 内部 HTTP 调用) - // libbox 暴露了 LibboxClashServer 或类似接口;此处记录并由内核异步处理。 - os_log("PacketTunnelProvider: selectOutbound tag=%{public}@", log: log, type: .info, tag) - // TODO(11G): 调用 libboxService 的 outbound selector API(sing-box v1.13+ 支持) -#endif + private func resolveConfig(_ options: [String: NSObject]?) throws -> String { + if let inline = options?["config"] as? String, !inline.isEmpty { + return inline + } + if let shared = UserDefaults(suiteName: appGroup), + let cached = shared.string(forKey: "lastConfigJson"), !cached.isEmpty { + return cached + } + guard let base = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: appGroup) else { + throw simpleError("no app group container") + } + let content = (try? String(contentsOf: base.appendingPathComponent("config.json"), + encoding: .utf8)) ?? "" + guard !content.isEmpty else { throw simpleError("empty sing-box config") } + return content + } +} + +// ── LibboxCommandServerHandler:服务端事件回调 ────────────────────────────── +extension PacketTunnelProvider: LibboxCommandServerHandlerProtocol { + func serviceReload() throws { + if let cfg = try? resolveConfig(nil) { + try commandServer?.startOrReloadService(cfg, options: LibboxOverrideOptions()) + } + } + 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() } - // MARK: – 配置解析工具 + func stopMonitor() { monitor?.cancel(); monitor = nil } - /// 从 configJson 中提取 tun.inet4_address 第一条(如 "172.19.0.1/30")。 - private func parseTunAddress(from json: String) -> String? { - guard let data = json.data(using: .utf8), - let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - let tun = root["tun"] as? [String: Any] else { return 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()) - // 兼容 inet4_address 字符串或数组 - if let addrStr = tun["inet4_address"] as? String { - return addrStr.components(separatedBy: "/").first + 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 } - if let addrArr = tun["inet4_address"] as? [String], - let first = addrArr.first { - return first.components(separatedBy: "/").first + 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 + // 必须阻塞到首个 path 更新再返回:否则 sing-box 紧接着下载远程 rule-set 时 + // defaultInterfaceIndex 仍为 -1,autoDetectControl 跳过绑接口 → "no available + // network interface"。对齐 sing-box-for-apple 的实现。 + let firstUpdate = DispatchSemaphore(value: 0) + var signaled = false + 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) + if !signaled { signaled = true; firstUpdate.signal() } + } + m.start(queue: monitorQueue) + _ = firstUpdate.wait(timeout: .now() + 5) + } + 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 } - /// 从 configJson 中提取 dns.servers 数组(取 address 字段)。 - private func parseDNSServers(from json: String) -> [String]? { - guard let data = json.data(using: .utf8), - let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - let dns = root["dns"] as? [String: Any], - let servers = dns["servers"] as? [[String: Any]] else { return nil } - - let addrs = servers.compactMap { $0["address"] as? String } - return addrs.isEmpty ? nil : addrs - } - - /// 极简内置配置(在完全没有外部配置时的最后回退,只做 pass-through 测试用)。 - private func minimalFallbackConfig() -> String { - return """ - { - "log": {"level": "info"}, - "dns": {"servers": [{"address": "\(defaultDNS)"}]}, - "inbounds": [], - "outbounds": [{"type": "direct", "tag": "direct"}], - "tun": { - "enabled": true, - "inet4_address": "\(defaultTunAddr)/24", - "auto_route": true, - "mtu": 1500 - } + 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 } - """ - } - - // MARK: – 错误工具 - - private func makeError(_ message: String) -> NSError { - NSError(domain: "PacketTunnelProvider", code: -1, - userInfo: [NSLocalizedDescriptionKey: message]) - } - - private func makePosixError(_ prefix: String) -> NSError { - NSError(domain: NSPOSIXErrorDomain, code: Int(errno), - userInfo: [NSLocalizedDescriptionKey: "\(prefix): \(String(cString: strerror(errno)))"]) + return Array(result.values) } } -// MARK: – LibboxPlatformInterface 协议实现 -// 仅在 Libbox.xcframework 链入时编译。 -// ObjC 协议名:LibboxPlatformInterface(gomobile 生成,见 experimental/libbox)。 - -#if canImport(Libbox) -extension PacketTunnelProvider: LibboxPlatformInterface { - - /// 接口控制(SO_MARK / setsockopt 等保护套接字不走隧道)。 - /// iOS NE 环境无需手动设置(系统自动处理),实现为 no-op。 - func autoDetectInterfaceControl(_ fd: Int32) throws { - // NE 自动处理接口路由,无需 SO_MARK +// 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] } - - /// 进程信息查询(供 sing-box 记录连接来源进程)。 - /// NE 沙盒环境无权访问其他进程信息,返回 nil。 - func findProcessInfo( - _ network: String?, - source: String?, - destination: String?, - uid: Int32 - ) throws -> LibboxProcessInfo? { - return nil - } - - /// 创建 TUN 接口并返回 fd。 - /// 在 NE 环境中: - /// 1. 从 LibboxTunOptions 读取网络参数(覆盖 startTunnel 中的默认值)。 - /// 2. 将 libbox 侧 fd 返回给内核(内核通过 read/write 此 fd 收发 IP 包)。 - /// 3. NE 侧 fd 用于 packetFlow 桥接循环。 - func openTun(_ options: LibboxTunOptions?) throws -> Int32 { - os_log("PacketTunnelProvider: openTun called (mtu=%d)", log: log, type: .info, - options?.mtu ?? 0) - guard tunFDs.libbox >= 0 else { - throw makeError("openTun: socketpair 未初始化") - } - return tunFDs.libbox - } - - /// 不使用 /proc/net(iOS 无 procfs)。 - func useProcFS() -> Bool { return false } - - /// libbox 日志输出(转 os.log)。 - func writeLog(_ message: String?) { - guard let msg = message, !msg.isEmpty else { return } - os_log("libbox: %{public}@", log: log, type: .debug, msg) - } - - /// 清除 DNS 缓存(iOS 无系统 DNS 缓存接口,no-op)。 - func clearDNSCache() { } } -#endif + +private func simpleError(_ msg: String) -> NSError { + NSError(domain: "pangolin.tunnel", code: 1, userInfo: [NSLocalizedDescriptionKey: msg]) +}