feat(client/macos): PacketTunnelProvider libbox 集成(编译+链接通过)
CommandServer 装配(LibboxSetup→NewCommandServer→start→startOrReloadService)+ 完整 LibboxPlatformInterface:openTun(TunOptions→NEPacketTunnelNetworkSettings, getpeername+utun ctl_id 取 fd)、默认接口监控(NWPathMonitor)、接口枚举、socket 绑定;LibboxCommandServerHandler 回调。 gomobile 桥接要点:实现接口用 XxxProtocol、接口型参数/返回用 (any XxxProtocol)、 三个方法被 Swift 重命名(autoDetectControl/send/usePlatformAutoDetectControl)、 CTLIOCGINFO 按 _IOWR 定义计算。 链接需补系统框架:SystemConfiguration + AppKit + libresolv(libbox 内部 系统代理/网络监控代码引用)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,135 +1,301 @@
|
||||
// PacketTunnelProvider.swift — Pangolin macOS System Extension(方案B / P1 骨架)
|
||||
// PacketTunnelProvider.swift — Pangolin macOS 隧道扩展(方案B / P1)
|
||||
//
|
||||
// 角色:运行在独立 sysex 进程的 NEPacketTunnelProvider。把嵌入的 sing-box(libbox)
|
||||
// 拉起来建 TUN,免 root、自包含(替代 PoC 的 `sudo sing-box run` 外部二进制)。
|
||||
// NEPacketTunnelProvider 子进程,把嵌入的 sing-box(libbox)拉起来建 TUN,免 root、
|
||||
// 自包含。数据流见 docs/p1-macos-system-extension.md。
|
||||
//
|
||||
// 数据流(详见 docs/p1-macos-system-extension.md):
|
||||
// 主 app(Flutter)经 NETunnelProviderManager.startVPNTunnel(options:) 下发 sing-box
|
||||
// JSON → 本扩展 startTunnel 收到 → LibboxSetup(App Group 路径)→ LibboxNewService(json,
|
||||
// platformInterface)→ start;libbox 回调 platformInterface.openTun 时,我们据其网络配置
|
||||
// 建 NEPacketTunnelNetworkSettings + setTunnelNetworkSettings,把 packetFlow 的 fd 交回。
|
||||
// 运行态速率/状态经 libbox CommandServer(loopback)被主 app 的 CommandClient 订阅。
|
||||
//
|
||||
// ⚠️ 编译前置:
|
||||
// 1. 工程已加入 Libbox.xcframework(scripts/build-libbox.sh apple macos 产出)。
|
||||
// 2. 本 target 链接 NetworkExtension + Libbox。
|
||||
// 3. PangolinPlatformInterface 对 LibboxPlatformInterfaceProtocol 的**完整**实现需
|
||||
// 对照官方 sing-box-for-apple 的 ExtensionPlatformInterface 补齐(本文件只搭关键路径)。
|
||||
// gomobile 约定:每个 Go 接口生成同名 protocol + class;Swift 里实现用 `XxxProtocol`,
|
||||
// 参数/返回用同名 class。NE 生命周期 override 用 completion-handler(规避 Swift 6.2
|
||||
// async→ObjC 桥接 thunk 的编译器崩溃)。
|
||||
|
||||
import Darwin
|
||||
import Foundation
|
||||
import Libbox
|
||||
import Network
|
||||
import NetworkExtension
|
||||
import os
|
||||
// import Libbox // ← gomobile bind 产物;加入 xcframework 后解开
|
||||
|
||||
private let log = Logger(subsystem: "com.pangolin.pangolin.PacketTunnel", category: "provider")
|
||||
private let appGroup = "group.com.pangolin.pangolin"
|
||||
|
||||
class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
// libbox 服务句柄(类型来自 Libbox 模块,加入 xcframework 后改为 LibboxBoxService)。
|
||||
private var boxService: AnyObject?
|
||||
private var platformInterface: PangolinPlatformInterface?
|
||||
final class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
private var commandServer: LibboxCommandServer?
|
||||
private var platform: PangolinPlatformInterface?
|
||||
|
||||
// App Group 共享容器:放 sing-box 工作目录(缓存/日志)。主 app 与扩展同路径。
|
||||
private static let appGroup = "group.com.pangolin.pangolin"
|
||||
|
||||
// 用 completion-handler 形式(而非 async):Swift 6.2 编译器在为 async 重写
|
||||
// 生成 ObjC 桥接 thunk 时会崩(emitInjectLoadableEnum),completion-handler 直接
|
||||
// 就是 @objc 原型,绕开该 bug。成功调 completionHandler(nil),失败传 error。
|
||||
override func startTunnel(options: [String: NSObject]?,
|
||||
completionHandler: @escaping (Error?) -> Void) {
|
||||
log.info("startTunnel")
|
||||
do {
|
||||
let configContent = try resolveConfig(options)
|
||||
|
||||
// 1) sing-box JSON:优先取 startVPNTunnel 下发的 options["configContent"],
|
||||
// 否则回退读 App Group 共享文件(主 app 写入)。
|
||||
let configContent: String
|
||||
if let inline = options?["configContent"] as? String, !inline.isEmpty {
|
||||
configContent = inline
|
||||
} else {
|
||||
configContent = (try? Self.readSharedConfig()) ?? ""
|
||||
guard let base = FileManager.default
|
||||
.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
|
||||
throw simpleError("no app group container")
|
||||
}
|
||||
let work = base.appendingPathComponent("work", isDirectory: true)
|
||||
try? FileManager.default.createDirectory(at: work, withIntermediateDirectories: true)
|
||||
|
||||
let setup = LibboxSetupOptions()
|
||||
setup.basePath = base.path
|
||||
setup.workingPath = work.path
|
||||
setup.tempPath = NSTemporaryDirectory()
|
||||
var setupErr: NSError?
|
||||
LibboxSetup(setup, &setupErr)
|
||||
if let setupErr { throw setupErr }
|
||||
|
||||
let platform = PangolinPlatformInterface(provider: self)
|
||||
self.platform = platform
|
||||
|
||||
var newErr: NSError?
|
||||
guard let server = LibboxNewCommandServer(self, platform, &newErr) else {
|
||||
throw newErr ?? simpleError("LibboxNewCommandServer returned nil")
|
||||
}
|
||||
try server.start()
|
||||
try server.startOrReloadService(configContent, options: nil)
|
||||
self.commandServer = server
|
||||
|
||||
log.info("startTunnel: service started")
|
||||
completionHandler(nil)
|
||||
} catch {
|
||||
log.error("startTunnel failed: \(error.localizedDescription)")
|
||||
completionHandler(error)
|
||||
}
|
||||
guard !configContent.isEmpty else {
|
||||
completionHandler(NSError(domain: "pangolin.tunnel", code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "empty sing-box config"]))
|
||||
return
|
||||
}
|
||||
|
||||
// 2) libbox 基础路径(全部落在 App Group 容器内,扩展沙盒可写)。
|
||||
guard let base = FileManager.default
|
||||
.containerURL(forSecurityApplicationGroupIdentifier: Self.appGroup) else {
|
||||
completionHandler(NSError(domain: "pangolin.tunnel", code: 2,
|
||||
userInfo: [NSLocalizedDescriptionKey: "no app group container"]))
|
||||
return
|
||||
}
|
||||
let work = base.appendingPathComponent("work", isDirectory: true)
|
||||
try? FileManager.default.createDirectory(at: work, withIntermediateDirectories: true)
|
||||
|
||||
/* ── 加入 Libbox.xcframework 后解开以下真实接线 ──────────────────────
|
||||
var setupError: NSError?
|
||||
let setup = LibboxSetupOptions()
|
||||
setup.basePath = base.path
|
||||
setup.workingPath = work.path
|
||||
setup.tempPath = NSTemporaryDirectory()
|
||||
LibboxSetup(setup, &setupError)
|
||||
if let setupError { completionHandler(setupError); return }
|
||||
|
||||
let platform = PangolinPlatformInterface(provider: self)
|
||||
self.platformInterface = platform
|
||||
|
||||
var newError: NSError?
|
||||
guard let service = LibboxNewService(configContent, platform, &newError) else {
|
||||
completionHandler(newError ?? NSError(domain: "pangolin.tunnel", code: 3)); return
|
||||
}
|
||||
self.boxService = service
|
||||
do { try service.start() } catch { completionHandler(error); return }
|
||||
completionHandler(nil) // 隧道已起
|
||||
return
|
||||
──────────────────────────────────────────────────────────────────── */
|
||||
|
||||
// 骨架占位:xcframework 接入前,直接回错,避免静默假成功。
|
||||
completionHandler(NSError(domain: "pangolin.tunnel", code: 99, userInfo: [
|
||||
NSLocalizedDescriptionKey:
|
||||
"Libbox 尚未接入(见 docs/p1-macos-system-extension.md 步骤 3)",
|
||||
]))
|
||||
}
|
||||
|
||||
override func stopTunnel(with reason: NEProviderStopReason,
|
||||
completionHandler: @escaping () -> Void) {
|
||||
log.info("stopTunnel reason=\(reason.rawValue)")
|
||||
/* if let service = boxService as? LibboxBoxService { try? service.close() } */
|
||||
boxService = nil
|
||||
platformInterface = nil
|
||||
try? commandServer?.closeService()
|
||||
commandServer?.close()
|
||||
commandServer = nil
|
||||
platform?.stopMonitor()
|
||||
platform = nil
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
// 主 app 经 sendProviderMessage 下发的控制(切节点等);也可改走 libbox CommandServer。
|
||||
override func handleAppMessage(_ messageData: Data,
|
||||
completionHandler: ((Data?) -> Void)?) {
|
||||
completionHandler?(nil)
|
||||
}
|
||||
|
||||
private static func readSharedConfig() throws -> String {
|
||||
private func resolveConfig(_ options: [String: NSObject]?) throws -> String {
|
||||
if let inline = options?["configContent"] as? String, !inline.isEmpty {
|
||||
return inline
|
||||
}
|
||||
guard let base = FileManager.default
|
||||
.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
|
||||
throw NSError(domain: "pangolin.tunnel", code: 2)
|
||||
throw simpleError("no app group container")
|
||||
}
|
||||
let url = base.appendingPathComponent("config.json")
|
||||
return try String(contentsOf: url, encoding: .utf8)
|
||||
let content = (try? String(contentsOf: base.appendingPathComponent("config.json"),
|
||||
encoding: .utf8)) ?? ""
|
||||
guard !content.isEmpty else { throw simpleError("empty sing-box config") }
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
/// libbox ↔ NetworkExtension 桥(LibboxPlatformInterfaceProtocol)。
|
||||
///
|
||||
/// 关键职责是 openTun:libbox 把所需网络参数(地址/路由/DNS/MTU)交给我们,我们据此建
|
||||
/// NEPacketTunnelNetworkSettings、setTunnelNetworkSettings,并返回 packetFlow 对应的 tun fd。
|
||||
///
|
||||
/// ⚠️ 本类只是骨架声明。完整协议(openTun / writeLog / useProcFS / findConnectionOwner /
|
||||
/// defaultInterfaceMonitor / getInterfaces / underNetworkExtension / systemCertificates 等)
|
||||
/// 需对照官方 ExtensionPlatformInterface 补齐后才能编译通过。见安装文档步骤 3。
|
||||
final class PangolinPlatformInterface: NSObject {
|
||||
// ── LibboxCommandServerHandler:服务端事件回调 ──────────────────────────────
|
||||
extension PacketTunnelProvider: LibboxCommandServerHandlerProtocol {
|
||||
func serviceReload() throws {
|
||||
if let cfg = try? resolveConfig(nil) {
|
||||
try commandServer?.startOrReloadService(cfg, options: nil)
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
// 示意:openTun 的核心是把 libbox 的网络配置翻译成 NEPacketTunnelNetworkSettings。
|
||||
// 真实签名以 Libbox 头文件为准(LibboxTunOptions / 返回 tun fd)。
|
||||
func stopMonitor() { monitor?.cancel(); monitor = 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())
|
||||
|
||||
let v4addrs = routePrefixes(options.getInet4Address())
|
||||
if !v4addrs.isEmpty {
|
||||
let ipv4 = NEIPv4Settings(addresses: v4addrs.map(\.address),
|
||||
subnetMasks: v4addrs.map(\.mask))
|
||||
ipv4.includedRoutes = includedRoutes4(options)
|
||||
settings.ipv4Settings = ipv4
|
||||
}
|
||||
let v6addrs = routePrefixes(options.getInet6Address())
|
||||
if !v6addrs.isEmpty {
|
||||
let ipv6 = NEIPv6Settings(addresses: v6addrs.map(\.address),
|
||||
networkPrefixLengths: v6addrs.map { NSNumber(value: $0.prefix) })
|
||||
ipv6.includedRoutes = includedRoutes6(options)
|
||||
settings.ipv6Settings = ipv6
|
||||
}
|
||||
if let dns = try? options.getDNSServerAddress(), !dns.value.isEmpty {
|
||||
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
|
||||
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)
|
||||
}
|
||||
m.start(queue: monitorQueue)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return Array(result.values)
|
||||
}
|
||||
}
|
||||
|
||||
// LibboxNetworkInterfaceIterator 适配器。
|
||||
final class InterfaceIterator: NSObject, LibboxNetworkInterfaceIteratorProtocol {
|
||||
private var idx = 0
|
||||
private let items: [LibboxNetworkInterface]
|
||||
init(items: [LibboxNetworkInterface]) { self.items = items }
|
||||
func hasNext() -> Bool { idx < items.count }
|
||||
func next() -> LibboxNetworkInterface? {
|
||||
guard idx < items.count else { return nil }
|
||||
defer { idx += 1 }
|
||||
return items[idx]
|
||||
}
|
||||
}
|
||||
|
||||
private func simpleError(_ msg: String) -> NSError {
|
||||
NSError(domain: "pangolin.tunnel", code: 1, userInfo: [NSLocalizedDescriptionKey: msg])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user