feat(client/ios): Provider 对齐 macOS libbox 集成(CommandServer+utun fd)+ 内存监控
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 的 <TeamID>. 前缀)。
|
||||
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<Int32>.size))
|
||||
setsockopt(fds[0], SOL_SOCKET, SO_RCVBUF, &bufSize, socklen_t(MemoryLayout<Int32>.size))
|
||||
setsockopt(fds[1], SOL_SOCKET, SO_SNDBUF, &bufSize, socklen_t(MemoryLayout<Int32>.size))
|
||||
setsockopt(fds[1], SOL_SOCKET, SO_RCVBUF, &bufSize, socklen_t(MemoryLayout<Int32>.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[..<n])
|
||||
// 根据 IP 版本首字节判断协议族
|
||||
let version = (buffer[0] & 0xF0) >> 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<Int32>?) 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<Int32>.size))
|
||||
_ = setsockopt(fd, IPPROTO_IPV6, IPV6_BOUND_IF, &idx, socklen_t(MemoryLayout<Int32>.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<ctl_info>.stride) & 0x1fff) << 16)
|
||||
| (UInt(UInt8(ascii: "N")) << 8) | 3
|
||||
for fd in Int32(0)...1024 {
|
||||
var addr = sockaddr_ctl()
|
||||
var len = socklen_t(MemoryLayout<sockaddr_ctl>.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<ifaddrs>?
|
||||
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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user