Files
pangolin/client/ios/Runner/VpnManager.swift
T
wangjia c84b6ea7da merge: maestro/tsk_iP5-_Ztq5THx [桥接 API 面 Channel 契约 + 三端原生骨架] [tsk_rcNKc3GQHBUd]
合并冲突补救:原分支从 merge base 7871512 分叉,仅修改 client/ 目录,
与 main 新增的 server/(apierr/idgen/CONVENTIONS.md) 完全不重叠,
无真实文件冲突,手动应用 feature branch 的全部 client/ 改动。

client 侧新增:
- lib/bridge/vpn_bridge.dart      — Dart↔原生通道契约(冻结)
- lib/bridge/vpn_bridge_mock.dart — 假内核,UI 联调用
- lib/bridge/kernel_process.dart  — 桌面子进程管理接口
- ios/PacketTunnel/{Info.plist,PacketTunnelProvider.swift} — NEPacketTunnel 骨架
- ios/Runner/VpnManager.swift     — NETunnelProviderManager 封装
- android/.../PangolinVpnService.kt — VpnService 骨架
- android/.../VpnEventBus.kt      — Application 级状态总线
- test/bridge/vpn_bridge_mock_test.dart — 桥接层单元测试

client 侧修改:
- android/.../MainActivity.kt    — 注册三通道(MethodChannel + 2×EventChannel)
- android/.../AndroidManifest.xml — 声明 FOREGROUND_SERVICE + PangolinVpnService
- ios/Runner/AppDelegate.swift   — 注册通道 + VpnManager 初始化
- ios/Runner/Info.plist          — 新增 NSVPNUsageDescription
- ios/Runner.xcodeproj/project.pbxproj — 添加 PacketTunnel extension target
- lib/widgets/connect_button.dart — VpnStatus 迁移到 bridge/vpn_bridge.dart
- lib/widgets/home_shell.dart    — error 状态 UI 分支补全
2026-06-13 14:58:38 +08:00

188 lines
7.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// VpnManager.swift App NETunnelProviderManager
//
//
// 1. loadAllFromPreferences / saveToPreferences VPN
// 2. start / stop NETunnelProviderSession PacketTunnel extension
// 3. sendProviderMessage IPC PacketTunnelProvider.handleAppMessage
//
// AppDelegate MethodChannel handler
//
// TODO(11F): start/stop / NEVPNStatus
// VpnStatusStreamHandler.shared.eventSink Dart
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"
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
manager.localizedDescription = "穿山甲 Pangolin VPN"
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) {
guard let manager = tunnelManager else {
completion(makeError("VpnManager: tunnelManager not loaded. Call loadFromPreferences first."))
return
}
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)
}
}
deinit {
if let obs = statusObserver {
NotificationCenter.default.removeObserver(obs)
}
}
//
private func makeError(_ message: String) -> NSError {
NSError(domain: "VpnManager", code: -1, userInfo: [NSLocalizedDescriptionKey: message])
}
}