feat(client/ios): Provider 对齐 macOS libbox 集成(CommandServer+utun fd)+ 内存监控

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-22 20:58:03 +08:00
parent 33a3508489
commit 381a621d77
+301 -485
View File
@@ -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
// LibboxNewServicelibbox/sing-box
// packetFlow socketpair libbox TUN fd
// MemoryMonitor
// stopTunnel MemoryMonitor
// handleAppMessage JSON IPCgetStatus / 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`,
// / classNE override completion-handler( Swift 6.2
// asyncObjC 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"
/// MTUNE 1500sing-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 tagIPC
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 }
// OutgoingApp packetFlow write to neFD libbox
readFromPacketFlow(neFD: neFD)
// Incoming Applibbox 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 APIsing-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 LibboxPlatformInterfacegomobile 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/netiOS 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])
}