feat(client/macos): P1 方案B 骨架 — System Extension + NETunnelProviderManager 接线
把 PoC 的 sudo sing-box 外部二进制换成自包含、免 root 的 NEPacketTunnelProvider (System Extension)+ 嵌入 libbox 的生产架构铺好骨架(不破坏现有 PoC 构建)。 - PacketTunnel/:扩展 target 源 — PacketTunnelProvider(LibboxSetup→NewService→start, openTun 建 NEPacketTunnelNetworkSettings)、Info.plist(NEProviderClasses)、 entitlements(packet-tunnel-provider-systemextension + App Group) - Runner/VpnChannel.swift:主 app 经 NETunnelProviderManager 启停 + 状态/速率回传, 对齐 Dart 侧 VpnNativeBridge 的 pangolin/vpn channel 契约 - vpn_bridge_provider.dart:kUseNativeVpnMacOS 开关(默认 false,联调通过后置 true) - docs/p1-macos-system-extension.md:文件清单 + Xcode/签名步骤 + 待办 (Team BYL4KQHMTN;Network Extensions 已确认自助开通、无需 Apple 审批) 非破坏:新源文件未入 build target、注册行/app-group entitlements 均注释、gate 默认 false。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Pangolin Tunnel</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<!-- System Extension 主体声明:NEPacketTunnelProvider。
|
||||
NetworkExtension 框架据此把扩展类与 packet-tunnel 类型绑定。 -->
|
||||
<key>NetworkExtension</key>
|
||||
<dict>
|
||||
<key>NEMachServiceName</key>
|
||||
<string>$(TeamIdentifierPrefix)group.com.pangolin.pangolin</string>
|
||||
<key>NEProviderClasses</key>
|
||||
<dict>
|
||||
<key>com.apple.networkextension.packet-tunnel</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Packet Tunnel Provider(System Extension 形态:站外 Developer ID 分发用 -systemextension 后缀)。
|
||||
在 Identifiers → com.pangolin.pangolin.tunnel 勾 Network Extensions 即获得此权限。 -->
|
||||
<key>com.apple.developer.networking.networkextension</key>
|
||||
<array>
|
||||
<string>packet-tunnel-provider-systemextension</string>
|
||||
</array>
|
||||
<!-- 与主 app 共享配置/状态(同一 App Group) -->
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.pangolin.pangolin</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,124 @@
|
||||
// PacketTunnelProvider.swift — Pangolin macOS System Extension(方案B / P1 骨架)
|
||||
//
|
||||
// 角色:运行在独立 sysex 进程的 NEPacketTunnelProvider。把嵌入的 sing-box(libbox)
|
||||
// 拉起来建 TUN,免 root、自包含(替代 PoC 的 `sudo sing-box run` 外部二进制)。
|
||||
//
|
||||
// 数据流(详见 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 补齐(本文件只搭关键路径)。
|
||||
|
||||
import NetworkExtension
|
||||
import os
|
||||
// import Libbox // ← gomobile bind 产物;加入 xcframework 后解开
|
||||
|
||||
private let log = Logger(subsystem: "com.pangolin.pangolin.tunnel", category: "provider")
|
||||
|
||||
class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
// libbox 服务句柄(类型来自 Libbox 模块,加入 xcframework 后改为 LibboxBoxService)。
|
||||
private var boxService: AnyObject?
|
||||
private var platformInterface: PangolinPlatformInterface?
|
||||
|
||||
// App Group 共享容器:放 sing-box 工作目录(缓存/日志)。主 app 与扩展同路径。
|
||||
private static let appGroup = "group.com.pangolin.pangolin"
|
||||
|
||||
override func startTunnel(options: [String: NSObject]?) async throws {
|
||||
log.info("startTunnel")
|
||||
|
||||
// 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 !configContent.isEmpty else {
|
||||
throw NSError(domain: "pangolin.tunnel", code: 1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "empty sing-box config"])
|
||||
}
|
||||
|
||||
// 2) libbox 基础路径(全部落在 App Group 容器内,sysess 沙盒可写)。
|
||||
guard let base = FileManager.default
|
||||
.containerURL(forSecurityApplicationGroupIdentifier: Self.appGroup) else {
|
||||
throw NSError(domain: "pangolin.tunnel", code: 2,
|
||||
userInfo: [NSLocalizedDescriptionKey: "no app group container"])
|
||||
}
|
||||
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 { throw setupError }
|
||||
|
||||
let platform = PangolinPlatformInterface(provider: self)
|
||||
self.platformInterface = platform
|
||||
|
||||
var newError: NSError?
|
||||
guard let service = LibboxNewService(configContent, platform, &newError) else {
|
||||
throw newError ?? NSError(domain: "pangolin.tunnel", code: 3)
|
||||
}
|
||||
self.boxService = service
|
||||
try service.start()
|
||||
──────────────────────────────────────────────────────────────────── */
|
||||
|
||||
// 骨架占位:xcframework 接入前,startTunnel 直接抛错,避免静默假成功。
|
||||
throw NSError(domain: "pangolin.tunnel", code: 99, userInfo: [
|
||||
NSLocalizedDescriptionKey:
|
||||
"Libbox 尚未接入(见 docs/p1-macos-system-extension.md 步骤 3)",
|
||||
])
|
||||
}
|
||||
|
||||
override func stopTunnel(with reason: NEProviderStopReason) async {
|
||||
log.info("stopTunnel reason=\(reason.rawValue)")
|
||||
/* if let service = boxService as? LibboxBoxService { try? service.close() } */
|
||||
boxService = nil
|
||||
platformInterface = nil
|
||||
}
|
||||
|
||||
// 主 app 经 sendProviderMessage 下发的控制(切节点等);也可改走 libbox CommandServer。
|
||||
override func handleAppMessage(_ messageData: Data) async -> Data? {
|
||||
nil
|
||||
}
|
||||
|
||||
private static func readSharedConfig() throws -> String {
|
||||
guard let base = FileManager.default
|
||||
.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
|
||||
throw NSError(domain: "pangolin.tunnel", code: 2)
|
||||
}
|
||||
let url = base.appendingPathComponent("config.json")
|
||||
return try String(contentsOf: url, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
private weak var provider: NEPacketTunnelProvider?
|
||||
|
||||
init(provider: NEPacketTunnelProvider) {
|
||||
self.provider = provider
|
||||
}
|
||||
|
||||
// 示意:openTun 的核心是把 libbox 的网络配置翻译成 NEPacketTunnelNetworkSettings。
|
||||
// 真实签名以 Libbox 头文件为准(LibboxTunOptions / 返回 tun fd)。
|
||||
}
|
||||
Reference in New Issue
Block a user