// PacketTunnelProvider.swift — Packet Tunnel Extension 完整实现(M3 PoC) // // Target: PacketTunnel (NEPacketTunnelProvider extension) // Bundle: com.pangolin.pangolinVpn.PacketTunnel // Entitlement: com.apple.developer.networking.networkextension / packet-tunnel-provider // // 架构概述: // 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。 import NetworkExtension import Darwin import Foundation import os.log #if canImport(Libbox) import Libbox #endif // MARK: – PacketTunnelProvider class PacketTunnelProvider: NEPacketTunnelProvider { // ── 常量 ──────────────────────────────────────────────────── 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 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 } let n = read(neFD, &buffer, bufSize) if n <= 0 { if errno == EINTR { continue } break } let data = Data(buffer[..> 4 let proto: NSNumber = version == 6 ? NSNumber(value: AF_INET6) : NSNumber(value: AF_INET) self.packetFlow.writePackets([data], withProtocols: [proto]) } 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 } 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) } libboxService = nil os_log("PacketTunnelProvider: libbox 内核已停止", log: log, type: .info) #endif } 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 } // MARK: – 配置解析工具 /// 从 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 } // 兼容 inet4_address 字符串或数组 if let addrStr = tun["inet4_address"] as? String { return addrStr.components(separatedBy: "/").first } if let addrArr = tun["inet4_address"] as? [String], let first = addrArr.first { return first.components(separatedBy: "/").first } 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 } } """ } // 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)))"]) } } // 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 } /// 进程信息查询(供 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