feat(client/macos): P1 方案B 骨架 — System Extension + NETunnelProviderManager 接线
ci-pangolin / Lint — shellcheck (push) Has been cancelled
ci-pangolin / OpenAPI Sync Check (push) Has been cancelled
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Has been cancelled
ci-pangolin / Flutter — analyze + test (push) Has been cancelled

把 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:
wangjia
2026-06-19 11:07:36 +08:00
parent 64a1a64c3f
commit b25c8bbc2c
10 changed files with 480 additions and 6 deletions
+38
View File
@@ -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),
/// NEPacketTunnelNetworkSettingssetTunnelNetworkSettings, 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)
}