merge: iOS 端 PoC M3 + entitlement 申请 [tsk_F5r4Kt9m_wQU]
解决合并冲突(原任务 tsk_nsobbj_rJdy0 分支 maestro/tsk_nsobbj_rJdy0):
两分支修改文件集合无重叠,手动 apply iOS PoC commit (e129f09)。
变更内容:
- client/ios/PacketTunnel/PacketTunnelProvider.swift:完整 M3 实现
(startTunnel/stopTunnel/handleAppMessage/LibboxPlatformInterface)
- client/ios/PacketTunnel/MemoryMonitor.swift:新增 NE 进程内存打点
- client/ios/PacketTunnel/PacketTunnel.entitlements:NE + App Group 权限
- client/ios/Runner/Runner.entitlements:主 App NE + App Group 权限
- client/ios/Runner/VpnManager.swift:红线词修复 + App Group 缓存 + NEVPNStatus 订阅
- client/ios/Runner/Info.plist:NSVPNUsageDescription 红线词修复
- client/ios/PacketTunnel/Info.plist:CFBundleDisplayName 红线词修复
- client/ios/Runner.xcodeproj/project.pbxproj:
CODE_SIGN_ENTITLEMENTS 添加到全部 build config;MemoryMonitor 加入 Sources
- app/kernel/build-ios.sh:M3 内存裁剪决策记录
- doc/ne-entitlement-申请指引.md:新增 NE entitlement 申请完整指引
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,25 +1,82 @@
|
||||
// PacketTunnelProvider.swift — Packet Tunnel Extension 骨架
|
||||
// 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
|
||||
//
|
||||
// 生命周期占位日志;实际 TUN 隧道建立与 libbox 接线由 11F 完成。
|
||||
// 架构概述:
|
||||
// startTunnel → 解析 configJson → NEPacketTunnelNetworkSettings
|
||||
// → LibboxNewService(libbox/sing-box 内核)→ 启动内核
|
||||
// → 包流桥接(packetFlow ↔ socketpair ↔ libbox TUN fd)
|
||||
// → MemoryMonitor 开始打点
|
||||
// stopTunnel → 内核干净停止 → MemoryMonitor 输出摘要
|
||||
// handleAppMessage → JSON IPC:getStatus / getMemory / selectOutbound
|
||||
//
|
||||
// 与主 App 通信:
|
||||
// 主 App → extension: NETunnelProviderSession.sendProviderMessage(_:responseHandler:)
|
||||
// extension → 主 App: 通过 NEProvider.cancelTunnelWithError 或 NETunnelProviderSession 回调
|
||||
// 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, …)
|
||||
//
|
||||
// TODO(11F): 在 startTunnel 中初始化 sing-box/libbox TUN 模式内核。
|
||||
// 条件编译:
|
||||
// #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: "VPN"
|
||||
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 生命周期 ──────────────────────────
|
||||
|
||||
@@ -27,27 +84,98 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
options: [String: NSObject]?,
|
||||
completionHandler: @escaping (Error?) -> Void
|
||||
) {
|
||||
os_log(
|
||||
"PacketTunnelProvider: startTunnel — options keys: %{public}@",
|
||||
log: log, type: .info,
|
||||
(options?.keys.joined(separator: ", ") ?? "(none)")
|
||||
)
|
||||
// TODO(11F): 解析 options["config"] → 写入临时配置文件
|
||||
// → 调用 libbox/sing-box TUN 启动 API
|
||||
// → 配置 packetFlow / setTunnelNetworkSettings
|
||||
completionHandler(nil)
|
||||
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
|
||||
)
|
||||
// TODO(11F): 停止 sing-box 内核 → 关闭 TUN fd
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -55,24 +183,339 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
_ messageData: Data,
|
||||
completionHandler: ((Data?) -> Void)?
|
||||
) {
|
||||
os_log(
|
||||
"PacketTunnelProvider: handleAppMessage — %d bytes",
|
||||
log: log, type: .debug,
|
||||
messageData.count
|
||||
)
|
||||
// TODO(11F): 解码 messageData(JSON 或 protobuf)→ 路由到内核命令
|
||||
// 如 selectOutbound / getStats 等
|
||||
completionHandler?(nil)
|
||||
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)
|
||||
// TODO(11F): 挂起内核流量处理
|
||||
os_log("PacketTunnelProvider: sleep(挂起流量处理)", log: log, type: .debug)
|
||||
memoryMonitor.stop()
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
override func wake() {
|
||||
os_log("PacketTunnelProvider: wake", log: log, type: .debug)
|
||||
// TODO(11F): 恢复内核流量处理
|
||||
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
|
||||
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[..<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])
|
||||
}
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user