Files
pangolin/client/ios/Runner/VpnManager.swift
T
wangjia e129f094c2 feat(ios): M3 PoC — PacketTunnelProvider + entitlement + 内存监控 [tsk_nsobbj_rJdy0]
### 新增文件
- client/ios/PacketTunnel/MemoryMonitor.swift
  周期打点 NE 进程内存(os_proc_available_memory iOS13+ / mach_task_basic_info 兼容
  iOS12);每 10s 采样一次;availableMemory < 5MB 时输出 ⚠️ 警告;提供
  summaryString() 供 handleAppMessage/getMemory IPC 返回。

- client/ios/PacketTunnel/PacketTunnel.entitlements
  Extension 的 NE + App Group entitlement 文件;申请说明写入注释。

- client/ios/Runner/Runner.entitlements
  主 App 的 NE + App Group entitlement 文件;申请说明写入注释。

- doc/ne-entitlement-申请指引.md
  完整申请操作手册:Bundle ID 清单、Apple Developer Portal 步骤、
  专项审批英文申请文案(已脱敏,无红线词)、审批 lead time 说明。

### 修改文件
- client/ios/PacketTunnel/PacketTunnelProvider.swift
  完整 M3 实现(替换原骨架 TODO):
  · startTunnel:解析 configJson(options/App Group 两路回退)→
    buildNetworkSettings → setTunnelNetworkSettings → setupTunBridge
    → startLibbox → startPacketBridging → memoryMonitor.start()
  · stopTunnel:memoryMonitor.stop() → stopLibbox → closeTunBridge
  · handleAppMessage:JSON IPC 协议(getStatus/getMemory/selectOutbound)
  · LibboxPlatformInterface 扩展(#if canImport(Libbox)):
    openTun 返回 socketpair libbox 侧 fd,autoDetectInterfaceControl no-op,
    writeLog 转 os.log,useProcFS 返回 false
  · packetFlow 双向桥接(readFromPacketFlow/writeToPacketFlow 循环)
  · parseTunAddress/parseDNSServers 从 JSON 提取网络参数

- client/ios/Runner/VpnManager.swift
  · localizedDescription "穿山甲 Pangolin VPN" → "Pangolin 加速"(红线词修复)
  · 新增 appGroup 常量(group.com.pangolin.pangolinVpn)
  · start() 写入 App Group UserDefaults 缓存 configJson

- client/ios/Runner/Info.plist
  NSVPNUsageDescription "穿山甲使用 VPN…" → "Pangolin 使用网络加速通道…"(红线词修复)

- client/ios/PacketTunnel/Info.plist
  CFBundleDisplayName "穿山甲 Tunnel" → "Pangolin 加速通道"(红线词修复)

- client/ios/Runner.xcodeproj/project.pbxproj
  · Runner + PacketTunnel 所有 build config 添加 CODE_SIGN_ENTITLEMENTS
  · 新增 MemoryMonitor.swift 到 PacketTunnel Sources 构建阶段
  · 新增 Runner.entitlements / PacketTunnel.entitlements / MemoryMonitor.swift
    文件引用及分组

- app/kernel/build-ios.sh
  添加 M3 内存裁剪决策记录:gVisor→grpc→QUIC 优先裁减顺序及内存估算,
  等待真机 10min 压测结果后更新最终结论

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 17:44:11 +08:00

196 lines
7.9 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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 anywherelocalizedDescription 使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) {
guard let manager = tunnelManager else {
completion(makeError("VpnManager: tunnelManager not loaded. Call loadFromPreferences first."))
return
}
// App Group UserDefaults PacketTunnel extension
if let shared = UserDefaults(suiteName: appGroup) {
shared.set(configJson, forKey: "lastConfigJson")
}
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])
}
}