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:
@@ -13,11 +13,20 @@ import 'vpn_bridge_mock.dart';
|
||||
// Web 平台须先检查 kIsWeb(dart:io 在 Web 上不可用)。
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
/// P1 方案B 开关:macOS 是否走原生 System Extension(VpnNativeBridge)而非 PoC 的
|
||||
/// sudo 子进程(DesktopVpnBridge)。默认 false——待 PacketTunnel target/签名就绪、
|
||||
/// 原生侧联调通过后置 true。见 docs/p1-macos-system-extension.md。
|
||||
const bool kUseNativeVpnMacOS = false;
|
||||
|
||||
/// 全局单例 VpnBridge。Ref 生命周期内不变。
|
||||
final vpnBridgeProvider = Provider<VpnBridge>((ref) {
|
||||
if (!kIsWeb &&
|
||||
(Platform.isMacOS || Platform.isLinux || Platform.isWindows)) {
|
||||
return DesktopVpnBridge();
|
||||
if (!kIsWeb) {
|
||||
if (Platform.isMacOS && kUseNativeVpnMacOS) {
|
||||
return VpnNativeBridge();
|
||||
}
|
||||
if (Platform.isMacOS || Platform.isLinux || Platform.isWindows) {
|
||||
return DesktopVpnBridge();
|
||||
}
|
||||
}
|
||||
return VpnBridgeMock();
|
||||
});
|
||||
|
||||
@@ -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)。
|
||||
}
|
||||
@@ -15,5 +15,11 @@
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.pangolin.pangolin</string>
|
||||
</array>
|
||||
<!-- P1 方案B:接 System Extension 时在此加(注册 App Group 后,见 p1-macos-system-extension.md §3):
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.pangolin.pangolin</string>
|
||||
</array>
|
||||
-->
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -14,6 +14,11 @@ class MainFlutterWindow: NSWindow {
|
||||
|
||||
RegisterGeneratedPlugins(registry: flutterViewController)
|
||||
|
||||
// P1 方案B:VPN 原生控制通道。把 VpnChannel.swift 加入 Runner target(Compile Sources)
|
||||
// 后,解开下一行注册(见 docs/p1-macos-system-extension.md §3)。在此之前保持注释,
|
||||
// 以免引用未编译符号破坏 Runner 构建。
|
||||
// VpnChannel.register(with: flutterViewController.registrar(forPlugin: "VpnChannel"))
|
||||
|
||||
super.awakeFromNib()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,5 +11,11 @@
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.pangolin.pangolin</string>
|
||||
</array>
|
||||
<!-- P1 方案B:接 System Extension 时在此加(注册 App Group 后,见 p1-macos-system-extension.md §3):
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.pangolin.pangolin</string>
|
||||
</array>
|
||||
-->
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
// VpnChannel.swift — 主 app 侧 VPN 控制(方案B / P1 骨架)
|
||||
//
|
||||
// 把 Flutter 的 pangolin/vpn(MethodChannel)+ pangolin/vpn/status、pangolin/vpn/stats
|
||||
// (EventChannel)接到 NETunnelProviderManager:安装/启停 PacketTunnel System Extension、
|
||||
// 回传状态与速率。契约对齐 client/lib/bridge/vpn_bridge.dart 的 VpnNativeBridge。
|
||||
//
|
||||
// 注册:在 MainFlutterWindow.awakeFromNib 里 `VpnChannel.register(with: flutterViewController)`。
|
||||
//
|
||||
// ⚠️ 站外分发首启需先**激活 System Extension**(OSSystemExtensionRequest,见
|
||||
// activateSystemExtensionIfNeeded + 安装文档步骤 4),用户会在「系统设置→隐私与安全性」
|
||||
// 点允许;激活成功后 NETunnelProviderManager 才能加载该 provider。
|
||||
|
||||
import FlutterMacOS
|
||||
import NetworkExtension
|
||||
import SystemExtensions
|
||||
import os
|
||||
|
||||
private let log = Logger(subsystem: "com.pangolin.pangolin", category: "vpn")
|
||||
|
||||
final class VpnChannel: NSObject {
|
||||
private static let tunnelBundleId = "com.pangolin.pangolin.tunnel"
|
||||
|
||||
private var statusSink: FlutterEventSink?
|
||||
private var statsSink: FlutterEventSink?
|
||||
private var statusObserver: NSObjectProtocol?
|
||||
private var statsTimer: Timer?
|
||||
private var manager: NETunnelProviderManager?
|
||||
|
||||
static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let instance = VpnChannel()
|
||||
let method = FlutterMethodChannel(name: "pangolin/vpn",
|
||||
binaryMessenger: registrar.messenger)
|
||||
method.setMethodCallHandler(instance.handle)
|
||||
|
||||
FlutterEventChannel(name: "pangolin/vpn/status", binaryMessenger: registrar.messenger)
|
||||
.setStreamHandler(StatusStreamHandler(owner: instance))
|
||||
FlutterEventChannel(name: "pangolin/vpn/stats", binaryMessenger: registrar.messenger)
|
||||
.setStreamHandler(StatsStreamHandler(owner: instance))
|
||||
|
||||
instance.observeStatus()
|
||||
}
|
||||
|
||||
// ── MethodChannel ───────────────────────────────────────────────
|
||||
private func handle(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
case "start":
|
||||
guard let configJson = call.arguments as? String else {
|
||||
result(FlutterError(code: "bad_args", message: "expected config json", details: nil))
|
||||
return
|
||||
}
|
||||
Task { await self.start(configJson, result) }
|
||||
case "stop":
|
||||
Task { await self.stop(result) }
|
||||
case "getStatus":
|
||||
result(Self.statusString(manager?.connection.status ?? .invalid))
|
||||
case "selectOutbound":
|
||||
// TODO: 经 sendProviderMessage / libbox CommandClient 切 outbound。
|
||||
result(nil)
|
||||
case "getActiveOutbound":
|
||||
result("auto")
|
||||
case "setKillSwitch":
|
||||
// includeAllNetworks / on-demand 实现 kill switch(后续)。
|
||||
result(nil)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
private func start(_ configJson: String, _ result: @escaping FlutterResult) async {
|
||||
do {
|
||||
try await activateSystemExtensionIfNeeded()
|
||||
let mgr = try await loadOrCreateManager()
|
||||
self.manager = mgr
|
||||
try mgr.connection.startVPNTunnel(options: [
|
||||
"configContent": configJson as NSString,
|
||||
])
|
||||
result(nil)
|
||||
} catch {
|
||||
log.error("start failed: \(error.localizedDescription)")
|
||||
result(FlutterError(code: "start_failed", message: error.localizedDescription, details: nil))
|
||||
}
|
||||
}
|
||||
|
||||
private func stop(_ result: @escaping FlutterResult) async {
|
||||
manager?.connection.stopVPNTunnel()
|
||||
result(nil)
|
||||
}
|
||||
|
||||
// ── NETunnelProviderManager 装配 ────────────────────────────────
|
||||
private func loadOrCreateManager() async throws -> NETunnelProviderManager {
|
||||
let all = try await NETunnelProviderManager.loadAllFromPreferences()
|
||||
let mgr = all.first ?? NETunnelProviderManager()
|
||||
let proto = (mgr.protocolConfiguration as? NETunnelProviderProtocol) ?? NETunnelProviderProtocol()
|
||||
proto.providerBundleIdentifier = Self.tunnelBundleId
|
||||
proto.serverAddress = "Pangolin" // 仅展示用
|
||||
mgr.protocolConfiguration = proto
|
||||
mgr.localizedDescription = "Pangolin"
|
||||
mgr.isEnabled = true
|
||||
try await mgr.saveToPreferences()
|
||||
try await mgr.loadFromPreferences() // 保存后重载,拿到有效 connection
|
||||
return mgr
|
||||
}
|
||||
|
||||
// 站外 sysex 首次需用户授权激活。
|
||||
private func activateSystemExtensionIfNeeded() async throws {
|
||||
// TODO(P1): OSSystemExtensionRequest.activationRequest(
|
||||
// forExtensionWithIdentifier: tunnelBundleId, queue: .main) + delegate 等待结果。
|
||||
// 已激活则直接返回。详见安装文档步骤 4。
|
||||
}
|
||||
|
||||
// ── 状态 / 速率回传 ─────────────────────────────────────────────
|
||||
private func observeStatus() {
|
||||
statusObserver = NotificationCenter.default.addObserver(
|
||||
forName: .NEVPNStatusDidChange, object: nil, queue: .main
|
||||
) { [weak self] note in
|
||||
guard let conn = note.object as? NEVPNConnection else { return }
|
||||
self?.statusSink?(Self.statusString(conn.status))
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func onStatusListen(_ sink: @escaping FlutterEventSink) {
|
||||
statusSink = sink
|
||||
sink(Self.statusString(manager?.connection.status ?? .invalid))
|
||||
}
|
||||
fileprivate func onStatusCancel() { statusSink = nil }
|
||||
|
||||
fileprivate func onStatsListen(_ sink: @escaping FlutterEventSink) {
|
||||
statsSink = sink
|
||||
// TODO(P1): 接 libbox CommandClient 订阅实时速率;骨架先每秒推占位 0。
|
||||
statsTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
|
||||
self?.statsSink?(["up": 0, "down": 0, "uplinkTotal": 0, "downlinkTotal": 0])
|
||||
}
|
||||
}
|
||||
fileprivate func onStatsCancel() { statsTimer?.invalidate(); statsTimer = nil; statsSink = nil }
|
||||
|
||||
private static func statusString(_ s: NEVPNStatus) -> String {
|
||||
switch s {
|
||||
case .connected: return "on"
|
||||
case .connecting, .reasserting: return "connecting"
|
||||
case .disconnecting: return "disconnecting"
|
||||
case .disconnected, .invalid: return "off"
|
||||
@unknown default: return "error"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EventChannel 流处理器。
|
||||
private final class StatusStreamHandler: NSObject, FlutterStreamHandler {
|
||||
weak var owner: VpnChannel?
|
||||
init(owner: VpnChannel) { self.owner = owner }
|
||||
func onListen(withArguments _: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
||||
owner?.onStatusListen(events); return nil
|
||||
}
|
||||
func onCancel(withArguments _: Any?) -> FlutterError? { owner?.onStatusCancel(); return nil }
|
||||
}
|
||||
|
||||
private final class StatsStreamHandler: NSObject, FlutterStreamHandler {
|
||||
weak var owner: VpnChannel?
|
||||
init(owner: VpnChannel) { self.owner = owner }
|
||||
func onListen(withArguments _: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
||||
owner?.onStatsListen(events); return nil
|
||||
}
|
||||
func onCancel(withArguments _: Any?) -> FlutterError? { owner?.onStatsCancel(); return nil }
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
# P1 · macOS System Extension 接线指南(方案B)
|
||||
|
||||
> 状态:**骨架已落**(本仓库)。把 PoC 的 `sudo sing-box run` 外部二进制换成自包含、免 root
|
||||
> 的 NEPacketTunnelProvider(System Extension)+ 嵌入式 libbox。本文档列清单 + 待人工在
|
||||
> Xcode/开发者后台完成的步骤。总览见 [vpn-core-embedding.md](./vpn-core-embedding.md)。
|
||||
|
||||
## 0. 关键标识(已确定)
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| Team ID | **BYL4KQHMTN** |
|
||||
| 主 app bundle id | `com.pangolin.pangolin`(App ID 已注册,勾 Network Extensions + App Groups)|
|
||||
| 隧道扩展 bundle id | `com.pangolin.pangolin.tunnel`(**待注册**,同样勾 Network Extensions + App Groups)|
|
||||
| App Group | `group.com.pangolin.pangolin`(**待在 Identifiers → App Groups 注册**)|
|
||||
| 分发方式 | 站外 Developer ID + 公证(非 App Store)→ entitlement 用 `-systemextension` 后缀 |
|
||||
| 权限审批 | **无需**——Network Extensions 在 Identifiers 自助勾选即得(Packet Tunnel Provider 已 GA)|
|
||||
|
||||
## 1. 仓库里已就位的文件(骨架)
|
||||
|
||||
```
|
||||
client/macos/
|
||||
├─ Runner/
|
||||
│ ├─ DebugProfile.entitlements / Release.entitlements ← application-groups 已备(注释,待 §3.6 解开)
|
||||
│ ├─ MainFlutterWindow.swift ← VpnChannel 注册行已备(注释,待 §3.6 解开)
|
||||
│ └─ VpnChannel.swift ← 主app:NETunnelProviderManager 启停+状态/速率回传(待加入 target)
|
||||
└─ PacketTunnel/ ← 新 System Extension target 的源
|
||||
├─ PacketTunnelProvider.swift ← 扩展:LibboxSetup→NewService→start;openTun 建 TUN
|
||||
├─ PacketTunnel.entitlements ← packet-tunnel-provider-systemextension + app group
|
||||
└─ Info.plist ← NEProviderClasses → PacketTunnelProvider
|
||||
|
||||
client/lib/bridge/
|
||||
├─ vpn_bridge.dart ← VpnNativeBridge(method/event channel,已存在)
|
||||
└─ vpn_bridge_provider.dart ← kUseNativeVpnMacOS 开关(默认 false;原生就绪后置 true)
|
||||
```
|
||||
|
||||
**Channel 契约**(`VpnNativeBridge` ↔ `VpnChannel`):
|
||||
- MethodChannel `pangolin/vpn`:`start(configJson)` / `stop` / `getStatus` / `selectOutbound(tag)` / `getActiveOutbound` / `setKillSwitch(bool)`
|
||||
- EventChannel `pangolin/vpn/status`:状态字符串(`on`/`connecting`/`off`/…)
|
||||
- EventChannel `pangolin/vpn/stats`:每秒一帧 `{up, down, uplinkTotal, downlinkTotal}`
|
||||
|
||||
## 2. 开发者后台(你来做,几分钟)
|
||||
|
||||
1. **App Group**:Identifiers → App Groups → ➕ → `group.com.pangolin.pangolin`。
|
||||
2. 回到 App ID `com.pangolin.pangolin` 的 **App Groups** capability → Edit → 勾上该 group。
|
||||
3. 新建 App ID `com.pangolin.pangolin.tunnel`(扩展)→ 勾 **Network Extensions** + **App Groups**(关联同一 group)。
|
||||
4. **Developer ID Application** 证书(Certificates → ➕,若还没有)——签名 + 公证用。
|
||||
|
||||
## 3. Xcode:新增 System Extension target + 接 libbox(核心人工步骤)
|
||||
|
||||
> Flutter 工程开 `client/macos/Runner.xcworkspace`。
|
||||
|
||||
1. **编 libbox**:`bash scripts/build-libbox.sh apple macos` → 得 `Libbox.xcframework`(~204MB,不入 git)。
|
||||
2. **新 target**:File → New → Target → **Network Extension**(macOS)→ Packet Tunnel
|
||||
- Product Name:`PacketTunnel`,bundle id `com.pangolin.pangolin.tunnel`,语言 Swift。
|
||||
- 删掉模板生成的 `PacketTunnelProvider.swift`,把 `client/macos/PacketTunnel/` 下的三个文件
|
||||
加入该 target(Provider/Info.plist/entitlements);Info.plist 与 entitlements 设为本 target 用。
|
||||
3. **链接 libbox**:把 `Libbox.xcframework` 拖进工程,在 **PacketTunnel** target 的
|
||||
*Frameworks and Libraries* 加入(Embed & Sign);解开 `PacketTunnelProvider.swift` 里
|
||||
`import Libbox` 与被注释的真实接线段(`LibboxSetup`/`LibboxNewService`/`service.start()`)。
|
||||
4. **补平台接口**:`PangolinPlatformInterface` 对 `LibboxPlatformInterfaceProtocol` 的完整实现
|
||||
(openTun / writeLog / useProcFS / findConnectionOwner / defaultInterfaceMonitor /
|
||||
getInterfaces / underNetworkExtension / systemCertificates …)——**对照官方
|
||||
[sing-box-for-apple `ExtensionPlatformInterface`](https://github.com/SagerNet/sing-box-for-apple)
|
||||
逐方法移植**(NE 部分两端高度一致,可大段复用)。这是 P1 唯一"重"的代码块。
|
||||
5. **sysex 激活**:`VpnChannel.activateSystemExtensionIfNeeded()` 用
|
||||
`OSSystemExtensionRequest.activationRequest(forExtensionWithIdentifier: tunnelBundleId, queue:)`
|
||||
+ delegate 等结果;首启用户在「系统设置 → 隐私与安全性」点允许。
|
||||
6. **接主 app(Runner)**:
|
||||
- 把 `client/macos/Runner/VpnChannel.swift` 加入 **Runner** target 的 *Compile Sources*。
|
||||
- 解开 `MainFlutterWindow.swift` 里 `VpnChannel.register(...)` 那行注释。
|
||||
- 解开 `Runner/DebugProfile.entitlements`、`Release.entitlements` 里 `application-groups` 注释块
|
||||
(注册 App Group 后)。
|
||||
> 这三处在仓库里**有意保持注释/未接**,以免在 target/签名就绪前破坏现有 PoC 构建。
|
||||
7. **签名**:两个 target(Runner + PacketTunnel)都用 Team **BYL4KQHMTN** + Developer ID;
|
||||
开 Hardened Runtime;Runner 的 *System Extensions* capability 自动带出。
|
||||
|
||||
## 4. 速率/状态联调(libbox CommandServer)
|
||||
|
||||
- 扩展内 `LibboxNewCommandServer`(loopback)暴露状态/速率/连接;主 app 用 `LibboxCommandClient`
|
||||
订阅 → 回填 `pangolin/vpn/stats` 与 `…/status`(替换 `VpnChannel` 里的占位 timer)。
|
||||
- 连接页(`connect_page.dart`)与统计页已消费这两路流,**无需改动**。
|
||||
|
||||
## 5. 切换上线
|
||||
|
||||
- 全部联调通过(连真节点、出口 IP=节点、速率/延迟、Kill Switch、退出拆隧道、明暗/中英)后,
|
||||
把 `vpn_bridge_provider.dart` 的 `kUseNativeVpnMacOS` 置 **true**。
|
||||
- PoC 的 `DesktopVpnBridge`(sudo 子进程)保留作 fallback/调试,不删。
|
||||
|
||||
## 6. 待办清单(TODO,代码内已标 `TODO(P1)`)
|
||||
|
||||
- [ ] 开发者后台:App Group + 扩展 App ID + Developer ID 证书(§2)
|
||||
- [ ] Xcode:新 PacketTunnel target + 加入 `Libbox.xcframework`(§3.1–3.3)
|
||||
- [ ] 解开 `PacketTunnelProvider.swift` 真实接线 + 移植 `PangolinPlatformInterface`(§3.4)
|
||||
- [ ] `OSSystemExtensionRequest` 激活流程(§3.5)
|
||||
- [ ] `LibboxCommandClient` 接速率/状态(§4)
|
||||
- [ ] `setKillSwitch` / `selectOutbound` 真实实现
|
||||
- [ ] 签名 + 公证流水线(Team BYL4KQHMTN)
|
||||
- [ ] 联调通过 → `kUseNativeVpnMacOS = true`(§5)
|
||||
@@ -94,8 +94,10 @@
|
||||
|
||||
## 6. 签名 / 权限 / 分发(上线硬门槛)
|
||||
|
||||
- **Apple Network Extensions entitlement**:需向 Apple **申请**(Developer 账号 →
|
||||
Network Extensions capability,packet-tunnel-provider)。**审批是前置阻断项**,先提交。
|
||||
- **Apple Network Extensions entitlement**:✅ **已确认自助开通,无需 Apple 审批**——在
|
||||
Identifiers → App ID 的 Capabilities 里直接勾 **Network Extensions** 即得
|
||||
Packet Tunnel Provider(含 `-systemextension` 形态)。早年需申请的是 NEHotspotHelper(热点),
|
||||
与我们无关。**故此项不再是阻断项。**
|
||||
- **付费 Apple Developer 账号**(个人/公司)+ Developer ID 证书。
|
||||
- macOS:sysex 必须 **Developer ID 签名 + Hardened Runtime + 公证**;首次启用 sysex 用户需在
|
||||
「系统设置 → 隐私与安全性」允许。
|
||||
@@ -126,6 +128,11 @@ libbox CommandServer ──(gRPC/IPC)──▶ 原生订阅速率/状态 ──(
|
||||
2. **P1 · macOS System Extension(主力)**:新增 sysex target + NEPacketTunnelProvider +
|
||||
App Group + 签名公证;`NativeVpnBridge` 经 method channel 启停;连接页接 libbox 状态/速率。
|
||||
**达标即 macOS 不再需要外部 sing-box / sudo。**
|
||||
🚧 **骨架已落**:App/扩展 entitlements(App Group)、`VpnChannel`(NETunnelProviderManager
|
||||
启停+状态/速率回传)、`PacketTunnel/`(Provider+Info.plist+entitlements)、Dart 分派开关
|
||||
`kUseNativeVpnMacOS`(默认 false)。**接线指南**:[p1-macos-system-extension.md](./p1-macos-system-extension.md)。
|
||||
Network Extensions 权限**已确认自助开通、无需 Apple 审批**;Team ID `BYL4KQHMTN`。
|
||||
待人工:Xcode 建 target + 加 `Libbox.xcframework` + 移植 libbox 平台接口 + 签名公证。
|
||||
3. **P2 · Android**:VpnService + libbox.aar + PlatformInterfaceWrapper(参考官方)。
|
||||
4. **P3 · iOS**:复用 macOS PacketTunnel 代码 + iOS appex。
|
||||
5. **P4 · Windows/Linux**:libbox 服务/helper + wintun / setcap。
|
||||
@@ -134,7 +141,7 @@ libbox CommandServer ──(gRPC/IPC)──▶ 原生订阅速率/状态 ──(
|
||||
|
||||
## 9. 风险 / 阻断项
|
||||
|
||||
- **Apple Network Extensions entitlement 审批**(周期不定)——**最先提交**。
|
||||
- ~~Apple Network Extensions entitlement 审批~~ → **已排除**:自助开通,无审批(见 §6)。
|
||||
- 付费开发者账号 + 证书/公证流水线(CI 自动签名公证)。
|
||||
- libbox 与节点 sing-box **版本/配置兼容**:`clientconfig.go` 下发的 JSON 必须被嵌入 libbox 接受
|
||||
(已踩过 1.12+ DNS/urltest 坑,需对齐版本)。
|
||||
|
||||
Reference in New Issue
Block a user