bf84492eef
各原生侧把 libbox/Clash 的实时上下行/延迟经冻结的 pangolin/vpn/stats channel 生产真实帧: - iOS/macOS 新增 StatsClient.swift(LibboxNewCommandClient,statusInterval 1s, uplink/downlink/总量 + urltest 延迟映射契约字段),VpnManager/VpnChannel 状态 观察驱动 start/stop,pbxproj 登记。 - Android PangolinVpnService.kt 延迟修复。 契约字段 uploadBytes/downloadBytes/uploadSpeed/downloadSpeed/urltestResults 不改。 真机验证(连接后实时跳动)为运行时步骤,见计划 Phase1 待办。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
247 lines
11 KiB
Swift
247 lines
11 KiB
Swift
// VpnManager.swift — 主 App 侧 NETunnelProviderManager 封装(M3 PoC 实现)
|
||
//
|
||
// 职责:
|
||
// 1. loadAllFromPreferences / saveToPreferences — 首次安装时建立 VPN 配置条目
|
||
// 2. start / stop — 通过 NETunnelProviderSession 驱动 PacketTunnel extension
|
||
// 3. sendProviderMessage — 透传 IPC 消息到 PacketTunnelProvider.handleAppMessage
|
||
// 4. NEVPNStatus 订阅 → VpnStatusStreamHandler.shared 推送到 Dart 侧
|
||
//
|
||
// AppDelegate 持有本类单例,在 MethodChannel handler 中调用其方法。
|
||
//
|
||
// 红线词检查(⚠️ 产品文案不得出现:VPN / 翻墙 / 科学上网 / 突破封锁 / 自由穿越 /
|
||
// Go anywhere):localizedDescription 使用「Pangolin 加速」口径。
|
||
|
||
import Foundation
|
||
import NetworkExtension
|
||
|
||
// ── 流处理器(Flutter EventChannel stream handlers)───────────────
|
||
|
||
/// status 流:推送 "off" / "connecting" / "on" / "error" 字符串
|
||
class VpnStatusStreamHandler: NSObject, FlutterStreamHandler {
|
||
static let shared = VpnStatusStreamHandler()
|
||
private(set) var eventSink: FlutterEventSink?
|
||
|
||
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
||
eventSink = events
|
||
return nil
|
||
}
|
||
|
||
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
||
eventSink = nil
|
||
return nil
|
||
}
|
||
|
||
func push(_ status: String) {
|
||
DispatchQueue.main.async { self.eventSink?(status) }
|
||
}
|
||
}
|
||
|
||
/// stats 流:推送 [String: Any] 字典,字段同 vpn_bridge.dart VpnStatsEvent schema
|
||
class VpnStatsStreamHandler: NSObject, FlutterStreamHandler {
|
||
static let shared = VpnStatsStreamHandler()
|
||
private(set) var eventSink: FlutterEventSink?
|
||
|
||
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
||
eventSink = events
|
||
return nil
|
||
}
|
||
|
||
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
||
eventSink = nil
|
||
return nil
|
||
}
|
||
|
||
func push(_ stats: [String: Any]) {
|
||
DispatchQueue.main.async { self.eventSink?(stats) }
|
||
}
|
||
}
|
||
|
||
// ── VPN 管理器 ────────────────────────────────────────────────────
|
||
|
||
class VpnManager: NSObject {
|
||
|
||
static let shared = VpnManager()
|
||
|
||
private var tunnelManager: NETunnelProviderManager?
|
||
|
||
// Bundle ID 与 PacketTunnel target PRODUCT_BUNDLE_IDENTIFIER 保持同步
|
||
private let extensionBundleId = "com.pangolin.pangolinVpn.PacketTunnel"
|
||
// App Group:与 PacketTunnel extension 共享 UserDefaults 容器
|
||
private let appGroup = "group.com.pangolin.pangolinVpn"
|
||
|
||
private var statusObserver: NSObjectProtocol?
|
||
|
||
private override init() {
|
||
super.init()
|
||
subscribeToVpnStatus()
|
||
}
|
||
|
||
// ── 偏好设置加载与保存 ─────────────────────────────────────
|
||
|
||
/// 从系统偏好加载已存在的 VPN 配置(首次为空列表)。
|
||
/// 必须在调用 start() 前完成。
|
||
func loadFromPreferences(completion: @escaping (Error?) -> Void) {
|
||
NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, error in
|
||
guard let self = self else { return }
|
||
if let error = error {
|
||
completion(error)
|
||
return
|
||
}
|
||
self.tunnelManager = managers?.first(where: {
|
||
($0.protocolConfiguration as? NETunnelProviderProtocol)?
|
||
.providerBundleIdentifier == self.extensionBundleId
|
||
}) ?? NETunnelProviderManager()
|
||
completion(nil)
|
||
}
|
||
}
|
||
|
||
/// 创建或更新 VPN 配置条目(首次安装时调用一次)。
|
||
func saveConfiguration(serverAddress: String = "pangolin", completion: @escaping (Error?) -> Void) {
|
||
let manager = tunnelManager ?? NETunnelProviderManager()
|
||
let proto = NETunnelProviderProtocol()
|
||
proto.providerBundleIdentifier = extensionBundleId
|
||
proto.serverAddress = serverAddress
|
||
manager.protocolConfiguration = proto
|
||
// ⚠️ 红线词规范:localizedDescription 用「加速」口径,不得出现 VPN/翻墙 等词。
|
||
manager.localizedDescription = "Pangolin 加速"
|
||
manager.isEnabled = true
|
||
manager.saveToPreferences { [weak self] error in
|
||
if error == nil { self?.tunnelManager = manager }
|
||
completion(error)
|
||
}
|
||
}
|
||
|
||
// ── 隧道控制 ──────────────────────────────────────────────
|
||
|
||
/// 启动 VPN 隧道(通过 NETunnelProviderSession)。
|
||
/// [configJson]: sing-box 配置 JSON,作为 options 传入 PacketTunnelProvider.startTunnel。
|
||
func start(configJson: String, completion: @escaping (Error?) -> Void) {
|
||
// 写入 App Group UserDefaults,供 PacketTunnel extension 恢复配置
|
||
if let shared = UserDefaults(suiteName: appGroup) {
|
||
shared.set(configJson, forKey: "lastConfigJson")
|
||
}
|
||
// iOS 必须先把配置存盘并启用(saveToPreferences,首启触发系统「允许 VPN 配置」授权),
|
||
// 再重载拿到有效 connection,否则对未持久化的 manager 调 startVPNTunnel 会抛 NEVPNError。
|
||
// 对齐 macOS VpnChannel.loadOrCreateManager。
|
||
NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, error in
|
||
guard let self = self else { return }
|
||
if let error = error { completion(error); return }
|
||
let manager = managers?.first(where: {
|
||
($0.protocolConfiguration as? NETunnelProviderProtocol)?
|
||
.providerBundleIdentifier == self.extensionBundleId
|
||
}) ?? NETunnelProviderManager()
|
||
|
||
let proto = (manager.protocolConfiguration as? NETunnelProviderProtocol)
|
||
?? NETunnelProviderProtocol()
|
||
proto.providerBundleIdentifier = self.extensionBundleId
|
||
proto.serverAddress = "Pangolin"
|
||
manager.protocolConfiguration = proto
|
||
// ⚠️ 红线词规范:localizedDescription 用「加速」口径,不得出现 VPN/翻墙 等词。
|
||
manager.localizedDescription = "Pangolin 加速"
|
||
manager.isEnabled = true
|
||
|
||
manager.saveToPreferences { saveErr in
|
||
if let saveErr = saveErr { completion(saveErr); return }
|
||
// 保存后必须重载,拿到有效 connection。
|
||
manager.loadFromPreferences { loadErr in
|
||
if let loadErr = loadErr { completion(loadErr); return }
|
||
self.tunnelManager = manager
|
||
do {
|
||
let options: [String: NSObject] = ["config": configJson as NSString]
|
||
try manager.connection.startVPNTunnel(options: options)
|
||
completion(nil)
|
||
} catch {
|
||
completion(error)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 停止 VPN 隧道。
|
||
func stop() {
|
||
tunnelManager?.connection.stopVPNTunnel()
|
||
}
|
||
|
||
/// 当前连接状态字符串(供 getStatus MethodChannel 方法使用)。
|
||
var statusString: String {
|
||
switch tunnelManager?.connection.status ?? .invalid {
|
||
case .invalid, .disconnected: return "off"
|
||
case .connecting: return "connecting"
|
||
case .connected: return "on"
|
||
case .disconnecting: return "connecting"
|
||
case .reasserting: return "connecting"
|
||
@unknown default: return "error"
|
||
}
|
||
}
|
||
|
||
// ── IPC 消息通道 ──────────────────────────────────────────
|
||
|
||
/// 向 PacketTunnelProvider.handleAppMessage 发送消息。
|
||
func sendProviderMessage(_ data: Data, responseHandler: ((Data?, Error?) -> Void)?) {
|
||
guard let session = tunnelManager?.connection as? NETunnelProviderSession else {
|
||
responseHandler?(nil, makeError("VpnManager: no active NETunnelProviderSession"))
|
||
return
|
||
}
|
||
do {
|
||
try session.sendProviderMessage(data) { responseData in
|
||
responseHandler?(responseData, nil)
|
||
}
|
||
} catch {
|
||
responseHandler?(nil, error)
|
||
}
|
||
}
|
||
|
||
// ── NEVPNStatus 订阅 ──────────────────────────────────────
|
||
|
||
private func subscribeToVpnStatus() {
|
||
statusObserver = NotificationCenter.default.addObserver(
|
||
forName: NSNotification.Name.NEVPNStatusDidChange,
|
||
object: nil,
|
||
queue: .main
|
||
) { [weak self] _ in
|
||
guard let self = self else { return }
|
||
let s = self.statusString
|
||
VpnStatusStreamHandler.shared.push(s)
|
||
// 实时统计 CommandClient 跟随连接状态启停:
|
||
// connected → 连扩展 CommandServer 订阅 box stats;断开 → 停。
|
||
switch self.tunnelManager?.connection.status ?? .invalid {
|
||
case .connected:
|
||
StatsClient.shared.start()
|
||
case .disconnected, .invalid:
|
||
StatsClient.shared.stop()
|
||
default:
|
||
break
|
||
}
|
||
}
|
||
// 隧道(扩展)常驻:app 重启时它可能早已 connected,此后不会再发 .connected 状态
|
||
// 变化 → 上面 observer 不触发、StatsClient 永不 start。启动时主动探一次现存连接:
|
||
// 拿到 tunnelManager,已连则立刻拉起统计。幂等(StatsClient.start 自带去重)。
|
||
NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, _ in
|
||
guard let self = self else { return }
|
||
if let mgr = managers?.first(where: {
|
||
($0.protocolConfiguration as? NETunnelProviderProtocol)?
|
||
.providerBundleIdentifier == self.extensionBundleId
|
||
}) {
|
||
self.tunnelManager = mgr
|
||
}
|
||
if self.tunnelManager?.connection.status == .connected {
|
||
VpnStatusStreamHandler.shared.push(self.statusString)
|
||
StatsClient.shared.start()
|
||
}
|
||
}
|
||
}
|
||
|
||
deinit {
|
||
if let obs = statusObserver {
|
||
NotificationCenter.default.removeObserver(obs)
|
||
}
|
||
}
|
||
|
||
// ── 工具 ─────────────────────────────────────────────────
|
||
|
||
private func makeError(_ message: String) -> NSError {
|
||
NSError(domain: "VpnManager", code: -1, userInfo: [NSLocalizedDescriptionKey: message])
|
||
}
|
||
}
|