b25c8bbc2c
把 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>
165 lines
7.3 KiB
Swift
165 lines
7.3 KiB
Swift
// 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 }
|
||
}
|