From b25c8bbc2c97abbe065b8bee040a1d0000ea18a2 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Fri, 19 Jun 2026 11:07:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(client/macos):=20P1=20=E6=96=B9=E6=A1=88B?= =?UTF-8?q?=20=E9=AA=A8=E6=9E=B6=20=E2=80=94=20System=20Extension=20+=20NE?= =?UTF-8?q?TunnelProviderManager=20=E6=8E=A5=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把 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 --- client/lib/bridge/vpn_bridge_provider.dart | 15 +- client/macos/PacketTunnel/Info.plist | 38 ++++ .../PacketTunnel/PacketTunnel.entitlements | 17 ++ .../PacketTunnel/PacketTunnelProvider.swift | 124 +++++++++++++ client/macos/Runner/DebugProfile.entitlements | 6 + client/macos/Runner/MainFlutterWindow.swift | 5 + client/macos/Runner/Release.entitlements | 6 + client/macos/Runner/VpnChannel.swift | 164 ++++++++++++++++++ docs/p1-macos-system-extension.md | 98 +++++++++++ docs/vpn-core-embedding.md | 13 +- 10 files changed, 480 insertions(+), 6 deletions(-) create mode 100644 client/macos/PacketTunnel/Info.plist create mode 100644 client/macos/PacketTunnel/PacketTunnel.entitlements create mode 100644 client/macos/PacketTunnel/PacketTunnelProvider.swift create mode 100644 client/macos/Runner/VpnChannel.swift create mode 100644 docs/p1-macos-system-extension.md diff --git a/client/lib/bridge/vpn_bridge_provider.dart b/client/lib/bridge/vpn_bridge_provider.dart index de79e8e..063eab1 100644 --- a/client/lib/bridge/vpn_bridge_provider.dart +++ b/client/lib/bridge/vpn_bridge_provider.dart @@ -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((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(); }); diff --git a/client/macos/PacketTunnel/Info.plist b/client/macos/PacketTunnel/Info.plist new file mode 100644 index 0000000..7b85c14 --- /dev/null +++ b/client/macos/PacketTunnel/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Pangolin Tunnel + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + NetworkExtension + + NEMachServiceName + $(TeamIdentifierPrefix)group.com.pangolin.pangolin + NEProviderClasses + + com.apple.networkextension.packet-tunnel + $(PRODUCT_MODULE_NAME).PacketTunnelProvider + + + CFBundlePackageType + XPC! + + diff --git a/client/macos/PacketTunnel/PacketTunnel.entitlements b/client/macos/PacketTunnel/PacketTunnel.entitlements new file mode 100644 index 0000000..0dfbb92 --- /dev/null +++ b/client/macos/PacketTunnel/PacketTunnel.entitlements @@ -0,0 +1,17 @@ + + + + + + com.apple.developer.networking.networkextension + + packet-tunnel-provider-systemextension + + + com.apple.security.application-groups + + group.com.pangolin.pangolin + + + diff --git a/client/macos/PacketTunnel/PacketTunnelProvider.swift b/client/macos/PacketTunnel/PacketTunnelProvider.swift new file mode 100644 index 0000000..b92fead --- /dev/null +++ b/client/macos/PacketTunnel/PacketTunnelProvider.swift @@ -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)。 +} diff --git a/client/macos/Runner/DebugProfile.entitlements b/client/macos/Runner/DebugProfile.entitlements index 3712522..e5ff371 100644 --- a/client/macos/Runner/DebugProfile.entitlements +++ b/client/macos/Runner/DebugProfile.entitlements @@ -15,5 +15,11 @@ $(AppIdentifierPrefix)com.pangolin.pangolin + diff --git a/client/macos/Runner/MainFlutterWindow.swift b/client/macos/Runner/MainFlutterWindow.swift index 0e0402f..d688efd 100644 --- a/client/macos/Runner/MainFlutterWindow.swift +++ b/client/macos/Runner/MainFlutterWindow.swift @@ -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() } } diff --git a/client/macos/Runner/Release.entitlements b/client/macos/Runner/Release.entitlements index 9a61ba4..348cec0 100644 --- a/client/macos/Runner/Release.entitlements +++ b/client/macos/Runner/Release.entitlements @@ -11,5 +11,11 @@ $(AppIdentifierPrefix)com.pangolin.pangolin + diff --git a/client/macos/Runner/VpnChannel.swift b/client/macos/Runner/VpnChannel.swift new file mode 100644 index 0000000..15df2f7 --- /dev/null +++ b/client/macos/Runner/VpnChannel.swift @@ -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 } +} diff --git a/docs/p1-macos-system-extension.md b/docs/p1-macos-system-extension.md new file mode 100644 index 0000000..3a83474 --- /dev/null +++ b/docs/p1-macos-system-extension.md @@ -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) diff --git a/docs/vpn-core-embedding.md b/docs/vpn-core-embedding.md index acd2b34..711cd21 100644 --- a/docs/vpn-core-embedding.md +++ b/docs/vpn-core-embedding.md @@ -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 坑,需对齐版本)。