// PacketTunnelProvider.swift — Pangolin iOS 隧道扩展 // // NEPacketTunnelProvider 子进程,把嵌入的 sing-box(libbox)拉起来建 TUN,免 root、 // 自包含。对齐 macOS 金标准集成方式(LibboxNewCommandServer + LibboxPlatformInterfaceProtocol // + 真 utun fd)。 // // 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 private let log = Logger(subsystem: "com.pangolin.pangolinVpn.PacketTunnel", category: "provider") // iOS App Group 用 group. 前缀(非 macOS 的 . 前缀)。 private let appGroup = "group.com.pangolin.pangolinVpn" final class PacketTunnelProvider: NEPacketTunnelProvider { private var commandServer: LibboxCommandServer? private var platform: PangolinPlatformInterface? private let memoryMonitor = MemoryMonitor() // ← iOS 新增 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 else { return } do { let configContent = try self.resolveConfig(options) 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() // 必须传非空 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) } } } 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() } 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)" } completionHandler?((try? JSONSerialization.data(withJSONObject: resp)) ?? nil) } 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() } 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 { let dnsSettings = NEDNSSettings(servers: [dns.value]) // iOS 必须显式 matchDomains=[""] 才会把「所有」DNS 查询都送进隧道; // 否则系统把隧道 DNS 当补充解析器,多数查询仍走系统直连 DNS(en0), // 导致被墙域名遭 DNS 污染、隧道连上也打不开网站。"" = 匹配所有域名。 dnsSettings.matchDomains = [""] settings.dnsSettings = dnsSettings } 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 } 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]) }