merge: iOS 端 PoC M3 + entitlement 申请 [tsk_F5r4Kt9m_wQU]

解决合并冲突(原任务 tsk_nsobbj_rJdy0 分支 maestro/tsk_nsobbj_rJdy0):
两分支修改文件集合无重叠,手动 apply iOS PoC commit (e129f09)。

变更内容:
- client/ios/PacketTunnel/PacketTunnelProvider.swift:完整 M3 实现
  (startTunnel/stopTunnel/handleAppMessage/LibboxPlatformInterface)
- client/ios/PacketTunnel/MemoryMonitor.swift:新增 NE 进程内存打点
- client/ios/PacketTunnel/PacketTunnel.entitlements:NE + App Group 权限
- client/ios/Runner/Runner.entitlements:主 App NE + App Group 权限
- client/ios/Runner/VpnManager.swift:红线词修复 + App Group 缓存 + NEVPNStatus 订阅
- client/ios/Runner/Info.plist:NSVPNUsageDescription 红线词修复
- client/ios/PacketTunnel/Info.plist:CFBundleDisplayName 红线词修复
- client/ios/Runner.xcodeproj/project.pbxproj:
  CODE_SIGN_ENTITLEMENTS 添加到全部 build config;MemoryMonitor 加入 Sources
- app/kernel/build-ios.sh:M3 内存裁剪决策记录
- doc/ne-entitlement-申请指引.md:新增 NE entitlement 申请完整指引

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-13 20:14:32 +08:00
parent 628440b3f9
commit 7a1715bd09
10 changed files with 889 additions and 41 deletions
+3 -1
View File
@@ -2,8 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Extension 名称(显示于系统偏好设置 > 通用 > VPN > 描述)
⚠️ 红线词规范:使用「加速通道」替代 VPN/Tunnel 等词(用户可见) -->
<key>CFBundleDisplayName</key>
<string>穿山甲 Tunnel</string>
<string>Pangolin 加速通道</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
+140
View File
@@ -0,0 +1,140 @@
// MemoryMonitor.swift NE
//
//
// 1. os_proc_available_memory()iOS 13+ NE
// 2. task_info(MACH_TASK_BASIC_INFO) resident size iOS 12
// 3. availableMemory < warningThreshold
// 4. 10 M3
//
// 使
// let monitor = MemoryMonitor()
// monitor.start() //
// monitor.stop() //
// monitor.peakResidentBytes //
//
// NE jetsam
// A9~15 MB residual limit
// A12~50 MB residual limit
// 线availableMemory < 5 MB
import Darwin
import Foundation
import os.log
// os_proc_available_memory iOS 13+ libSystem 退 mach
// Swift os/proc.h @_silgen_name
@_silgen_name("os_proc_available_memory")
private func _os_proc_available_memory() -> UInt64
// MARK: MemoryMonitor
final class MemoryMonitor {
//
/// 10
let interval: TimeInterval
/// 5 MB
let warningThreshold: UInt64
/// os.log
private let log: OSLog
//
private var timer: DispatchSourceTimer?
private let queue = DispatchQueue(label: "com.pangolin.memory-monitor", qos: .utility)
private(set) var peakResidentBytes: UInt64 = 0
private(set) var minAvailableBytes: UInt64 = UInt64.max
private(set) var sampleCount: Int = 0
init(
interval: TimeInterval = 10,
warningThreshold: UInt64 = 5 * 1024 * 1024,
subsystem: String = "com.pangolin.pangolinVpn.PacketTunnel"
) {
self.interval = interval
self.warningThreshold = warningThreshold
self.log = OSLog(subsystem: subsystem, category: "MemoryMonitor")
}
// MARK:
func start() {
stop()
let t = DispatchSource.makeTimerSource(queue: queue)
t.schedule(deadline: .now(), repeating: interval, leeway: .seconds(1))
t.setEventHandler { [weak self] in self?.tick() }
timer = t
t.resume()
os_log("MemoryMonitor: 开始打点,间隔=%.0fs", log: log, type: .info, interval)
}
func stop() {
timer?.cancel()
timer = nil
os_log(
"MemoryMonitor: 停止打点。总采样 %d 次 | 峰值 RSS=%.1f MB | 最小可用=%.1f MB",
log: log, type: .info,
sampleCount,
Double(peakResidentBytes) / 1_048_576,
minAvailableBytes == UInt64.max ? -1.0 : Double(minAvailableBytes) / 1_048_576
)
}
// MARK:
private func tick() {
let resident = residentBytes()
let available = availableBytes()
sampleCount += 1
if resident > peakResidentBytes { peakResidentBytes = resident }
if available < minAvailableBytes { minAvailableBytes = available }
let level: OSLogType = available < warningThreshold ? .error : .info
os_log(
"MemoryMonitor [#%d] RSS=%.1f MB | 可用=%.1f MB%{public}@",
log: log, type: level,
sampleCount,
Double(resident) / 1_048_576,
Double(available) / 1_048_576,
available < warningThreshold ? " ⚠️ 接近上限,建议裁剪 build tags" : ""
)
}
// MARK:
/// resident sizeRSSiOS 12+
func residentBytes() -> UInt64 {
var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size / MemoryLayout<integer_t>.size)
let result: kern_return_t = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
}
}
return result == KERN_SUCCESS ? UInt64(info.resident_size) : 0
}
/// NE
/// iOS 13+ os_proc_available_memory()
/// iOS 12退 MAX_RSS - resident
func availableBytes() -> UInt64 {
if #available(iOS 13.0, *) {
return _os_proc_available_memory()
} else {
// iOS 12 退 15 MB - RSS
let conservativeLimit: UInt64 = 15 * 1024 * 1024
let resident = residentBytes()
return resident < conservativeLimit ? conservativeLimit - resident : 0
}
}
/// handleAppMessage/getStats
func summaryString() -> String {
let rss = Double(residentBytes()) / 1_048_576
let avail = Double(availableBytes()) / 1_048_576
return String(format: "RSS=%.1fMB avail=%.1fMB peak=%.1fMB samples=%d",
rss, avail,
Double(peakResidentBytes) / 1_048_576,
sampleCount)
}
}
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!--
PacketTunnel.entitlements — PacketTunnel Extension Target Entitlements
Bundle ID: com.pangolin.pangolinVpn.PacketTunnel
申请说明:
1. com.apple.developer.networking.networkextension
→ Extension Bundle ID 须单独在 Apple 开发者后台注册并开启:
developer.apple.com → Identifiers → com.pangolin.pangolinVpn.PacketTunnel
→ Network Extensions → Packet Tunnel Provider(勾选)
2. com.apple.security.application-groups
→ 与主 App 使用相同 Groupgroup.com.pangolin.pangolinVpn)。
Extension 通过 UserDefaults(suiteName:) 写入最新 stats/status
主 App 在下次启动时读取(异常崩溃恢复场景)。
-->
<plist version="1.0">
<dict>
<!-- Network ExtensionExtension 自身提供 Packet Tunnel 能力 -->
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
</array>
<!-- App Group:与主 App 共享 UserDefaults / 文件容器 -->
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.pangolin.pangolinVpn</string>
</array>
</dict>
</plist>
@@ -1,25 +1,82 @@
// PacketTunnelProvider.swift Packet Tunnel Extension
// PacketTunnelProvider.swift Packet Tunnel Extension M3 PoC
//
// Target: PacketTunnel (NEPacketTunnelProvider extension)
// Bundle: com.pangolin.pangolinVpn.PacketTunnel
// Entitlement: com.apple.developer.networking.networkextension / packet-tunnel-provider
//
// TUN libbox 线 11F
//
// startTunnel configJson NEPacketTunnelNetworkSettings
// LibboxNewServicelibbox/sing-box
// packetFlow socketpair libbox TUN fd
// MemoryMonitor
// stopTunnel MemoryMonitor
// handleAppMessage JSON IPCgetStatus / getMemory / selectOutbound
//
// App
// App extension: NETunnelProviderSession.sendProviderMessage(_:responseHandler:)
// extension App: NEProvider.cancelTunnelWithError NETunnelProviderSession
// libbox
// app/kernel/dist/ios/Libbox.xcframework
// gomobile bind ./experimental/libbox ObjC
// - LibboxNewService(configContent:platformInterface:disableMemoryLimit:error:)
// - LibboxService.start() / .stop()
// - LibboxPlatformInterface (ObjC protocol)
// - LibboxTunOptions (ObjC class, fields: inet4Address, inet6Address, mtu, autoRoute, )
//
// TODO(11F): startTunnel sing-box/libbox TUN
//
// #if canImport(Libbox) / Release
//
//
// NE ~15MB ~50MB
// MemoryMonitor 10s < 5MB
// jetsam app/kernel/build-ios.sh
// build tags with_gvisor with_grpc XCFramework
//
// IPC handleAppMessage / sendProviderMessage
// JSON{ "action": "getStatus" | "getMemory" | "selectOutbound", "tag"?: "xxx" }
// JSON{ "status": "...", "memory": "...", "error": "..." }
//
// 线
// localizedDescription VpnManager.swift
import NetworkExtension
import Darwin
import Foundation
import os.log
#if canImport(Libbox)
import Libbox
#endif
// MARK: PacketTunnelProvider
class PacketTunnelProvider: NEPacketTunnelProvider {
//
private let log = OSLog(
subsystem: "com.pangolin.pangolinVpn.PacketTunnel",
category: "VPN"
category: "PacketTunnel"
)
private let appGroup = "group.com.pangolin.pangolinVpn"
/// NE IPv4 退 config tun.address
private let defaultTunAddr = "172.19.0.1"
private let defaultTunMask = "255.255.255.0"
/// DNS sing-box DNS
private let defaultDNS = "198.18.0.1"
/// MTUNE 1500sing-box tun 9000 - NE
private let tunMTU: NSNumber = 1500
//
private let memoryMonitor = MemoryMonitor()
#if canImport(Libbox)
private var libboxService: LibboxService?
#endif
/// socketpair[0]=NE[1]=libbox
private var tunFDs: (ne: Int32, libbox: Int32) = (-1, -1)
private var isStopped = false
private let stopLock = NSLock()
/// selectOutbound tagIPC
private var activeOutboundTag: String = "auto"
// NEPacketTunnelProvider
@@ -27,27 +84,98 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
options: [String: NSObject]?,
completionHandler: @escaping (Error?) -> Void
) {
os_log(
"PacketTunnelProvider: startTunnel — options keys: %{public}@",
log: log, type: .info,
(options?.keys.joined(separator: ", ") ?? "(none)")
)
// TODO(11F): options["config"]
// libbox/sing-box TUN API
// packetFlow / setTunnelNetworkSettings
completionHandler(nil)
os_log("PacketTunnelProvider: startTunnel begin", log: log, type: .info)
stopLock.lock()
isStopped = false
stopLock.unlock()
// 1. configJson options App Group UserDefaults 退
let configJson: String
if let opt = options?["config"] as? String, !opt.isEmpty {
configJson = opt
} else if let shared = UserDefaults(suiteName: appGroup),
let cached = shared.string(forKey: "lastConfigJson"),
!cached.isEmpty {
configJson = cached
os_log("PacketTunnelProvider: 从 App Group 读取缓存 configJson", log: log, type: .info)
} else {
os_log("PacketTunnelProvider: 无 configJson,使用内置最小配置", log: log, type: .error)
configJson = minimalFallbackConfig()
}
// App Group NE
if let shared = UserDefaults(suiteName: appGroup) {
shared.set(configJson, forKey: "lastConfigJson")
}
// 2. JSON tun.address
let tunAddress = parseTunAddress(from: configJson) ?? defaultTunAddr
let dnsServers = parseDNSServers(from: configJson) ?? [defaultDNS]
// 3. NEPacketTunnelNetworkSettings
let settings = buildNetworkSettings(tunAddress: tunAddress, dnsServers: dnsServers)
setTunnelNetworkSettings(settings) { [weak self] error in
guard let self = self else { return }
if let error = error {
os_log("PacketTunnelProvider: setTunnelNetworkSettings 失败: %{public}@",
self.log, type: .error, error.localizedDescription)
completionHandler(error)
return
}
os_log("PacketTunnelProvider: 网络设置已应用", self.log, type: .info)
// 4. socketpair TUN
do {
try self.setupTunBridge()
} catch {
os_log("PacketTunnelProvider: socketpair 失败: %{public}@",
self.log, type: .error, error.localizedDescription)
completionHandler(error)
return
}
// 5. & libbox
do {
try self.startLibbox(configJson: configJson)
} catch {
os_log("PacketTunnelProvider: libbox 启动失败: %{public}@",
self.log, type: .error, error.localizedDescription)
completionHandler(error)
return
}
// 6.
self.startPacketBridging()
// 7.
self.memoryMonitor.start()
os_log("PacketTunnelProvider: 隧道已就绪", self.log, type: .info)
completionHandler(nil)
}
}
override func stopTunnel(
with reason: NEProviderStopReason,
completionHandler: @escaping () -> Void
) {
os_log(
"PacketTunnelProvider: stopTunnel — reason=%d",
log: log, type: .info,
reason.rawValue
)
// TODO(11F): sing-box TUN fd
os_log("PacketTunnelProvider: stopTunnel reason=%d", log: log, type: .info, reason.rawValue)
stopLock.lock()
isStopped = true
stopLock.unlock()
//
memoryMonitor.stop()
os_log("PacketTunnelProvider: 内存峰值摘要: %{public}@",
log, type: .info, memoryMonitor.summaryString())
// libbox
stopLibbox()
// socketpair
closeTunBridge()
completionHandler()
}
@@ -55,24 +183,339 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
_ messageData: Data,
completionHandler: ((Data?) -> Void)?
) {
os_log(
"PacketTunnelProvider: handleAppMessage — %d bytes",
log: log, type: .debug,
messageData.count
)
// TODO(11F): messageDataJSON protobuf
// selectOutbound / getStats
completionHandler?(nil)
guard let request = try? JSONSerialization.jsonObject(with: messageData) as? [String: Any],
let action = request["action"] as? String else {
os_log("PacketTunnelProvider: handleAppMessage 无法解析 JSON",
log: log, type: .error)
completionHandler?(nil)
return
}
os_log("PacketTunnelProvider: IPC action=%{public}@", log: log, type: .debug, action)
var response: [String: Any] = [:]
switch action {
case "getStatus":
// libbox "on" "error"
let running: Bool
#if canImport(Libbox)
running = libboxService != nil
#else
running = false
#endif
response["status"] = running ? "on" : "error"
case "getMemory":
// App
response["memory"] = memoryMonitor.summaryString()
response["peakRssBytes"] = memoryMonitor.peakResidentBytes
response["minAvailableBytes"] = memoryMonitor.minAvailableBytes
response["sampleCount"] = memoryMonitor.sampleCount
case "selectOutbound":
// tag libbox Clash API
let tag = request["tag"] as? String ?? "auto"
activeOutboundTag = tag
selectLibboxOutbound(tag: tag)
response["activeOutbound"] = tag
case "getActiveOutbound":
response["activeOutbound"] = activeOutboundTag
default:
response["error"] = "unknown action: \(action)"
}
if let data = try? JSONSerialization.data(withJSONObject: response) {
completionHandler?(data)
} else {
completionHandler?(nil)
}
}
override func sleep(completionHandler: @escaping () -> Void) {
os_log("PacketTunnelProvider: sleep", log: log, type: .debug)
// TODO(11F):
os_log("PacketTunnelProvider: sleep(挂起流量处理)", log: log, type: .debug)
memoryMonitor.stop()
completionHandler()
}
override func wake() {
os_log("PacketTunnelProvider: wake", log: log, type: .debug)
// TODO(11F):
os_log("PacketTunnelProvider: wake(恢复流量处理)", log: log, type: .debug)
memoryMonitor.start()
}
// MARK:
private func buildNetworkSettings(
tunAddress: String,
dnsServers: [String]
) -> NEPacketTunnelNetworkSettings {
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
// IPv4 0.0.0.0/0
let ipv4 = NEIPv4Settings(addresses: [tunAddress], subnetMasks: [defaultTunMask])
ipv4.includedRoutes = [NEIPv4Route.default()]
ipv4.excludedRoutes = []
settings.ipv4Settings = ipv4
// DNS sing-box fake-ip/tun DNS
let dns = NEDNSSettings(servers: dnsServers)
dns.matchDomains = [""] // DNS
settings.dnsSettings = dns
settings.mtu = tunMTU
return settings
}
// MARK: TUN socketpair
private func setupTunBridge() throws {
var fds: [Int32] = [-1, -1]
// AF_UNIX SOCK_DGRAM read/write IP
let ret = socketpair(AF_UNIX, SOCK_DGRAM, 0, &fds)
guard ret == 0 else {
throw makePosixError("socketpair 失败")
}
// socket 4 MB
let bufSize: Int32 = 4 * 1024 * 1024
setsockopt(fds[0], SOL_SOCKET, SO_SNDBUF, &bufSize, socklen_t(MemoryLayout<Int32>.size))
setsockopt(fds[0], SOL_SOCKET, SO_RCVBUF, &bufSize, socklen_t(MemoryLayout<Int32>.size))
setsockopt(fds[1], SOL_SOCKET, SO_SNDBUF, &bufSize, socklen_t(MemoryLayout<Int32>.size))
setsockopt(fds[1], SOL_SOCKET, SO_RCVBUF, &bufSize, socklen_t(MemoryLayout<Int32>.size))
tunFDs = (ne: fds[0], libbox: fds[1])
os_log("PacketTunnelProvider: socketpair 创建成功 ne=%d libbox=%d",
log: log, type: .debug, fds[0], fds[1])
}
private func closeTunBridge() {
if tunFDs.ne >= 0 {
close(tunFDs.ne)
tunFDs.ne = -1
}
if tunFDs.libbox >= 0 {
close(tunFDs.libbox)
tunFDs.libbox = -1
}
}
// MARK: packetFlow socketpair libbox
private func startPacketBridging() {
let neFD = tunFDs.ne
guard neFD >= 0 else { return }
// OutgoingApp packetFlow write to neFD libbox
readFromPacketFlow(neFD: neFD)
// Incoming Applibbox read from neFD packetFlow
writeToPacketFlow(neFD: neFD)
}
/// NE packetFlow socketpair NE libbox
private func readFromPacketFlow(neFD: Int32) {
packetFlow.readPacketObjects { [weak self] packets in
guard let self = self else { return }
self.stopLock.lock()
let stopped = self.isStopped
self.stopLock.unlock()
guard !stopped else { return }
for packet in packets {
let data = packet.data
_ = data.withUnsafeBytes { ptr -> Int in
guard let base = ptr.baseAddress else { return 0 }
return write(neFD, base, data.count)
}
}
//
self.readFromPacketFlow(neFD: neFD)
}
}
/// socketpair NE libbox NE packetFlow
private func writeToPacketFlow(neFD: Int32) {
let bufSize = 65536
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
var buffer = [UInt8](repeating: 0, count: bufSize)
while true {
self.stopLock.lock()
let stopped = self.isStopped
self.stopLock.unlock()
guard !stopped else { break }
let n = read(neFD, &buffer, bufSize)
if n <= 0 {
if errno == EINTR { continue }
break
}
let data = Data(buffer[..<n])
// IP
let version = (buffer[0] & 0xF0) >> 4
let proto: NSNumber = version == 6
? NSNumber(value: AF_INET6)
: NSNumber(value: AF_INET)
self.packetFlow.writePackets([data], withProtocols: [proto])
}
os_log("PacketTunnelProvider: writeToPacketFlow 退出循环", self.log, type: .debug)
}
}
// MARK: libbox
private func startLibbox(configJson: String) throws {
#if canImport(Libbox)
var serviceError: NSError? = nil
let svc = LibboxNewService(configJson, self, false, &serviceError)
if let err = serviceError { throw err }
guard let svc else {
throw makeError("LibboxNewService 返回 nil")
}
var startError: NSError? = nil
svc.start(&startError)
if let err = startError { throw err }
libboxService = svc
os_log("PacketTunnelProvider: libbox 内核已启动", log: log, type: .info)
#else
os_log("PacketTunnelProvider: Libbox 未链接,骨架模式运行", log: log, type: .fault)
// NE "on" UI
#endif
}
private func stopLibbox() {
#if canImport(Libbox)
guard let svc = libboxService else { return }
var stopError: NSError? = nil
svc.stop(&stopError)
if let err = stopError {
os_log("PacketTunnelProvider: libbox stop 错误: %{public}@",
log: log, type: .error, err.localizedDescription)
}
libboxService = nil
os_log("PacketTunnelProvider: libbox 内核已停止", log: log, type: .info)
#endif
}
private func selectLibboxOutbound(tag: String) {
#if canImport(Libbox)
// sing-box Clash API selectOutbound libbox HTTP
// libbox LibboxClashServer
os_log("PacketTunnelProvider: selectOutbound tag=%{public}@", log: log, type: .info, tag)
// TODO(11G): libboxService outbound selector APIsing-box v1.13+
#endif
}
// MARK:
/// configJson tun.inet4_address "172.19.0.1/30"
private func parseTunAddress(from json: String) -> String? {
guard let data = json.data(using: .utf8),
let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let tun = root["tun"] as? [String: Any] else { return nil }
// inet4_address
if let addrStr = tun["inet4_address"] as? String {
return addrStr.components(separatedBy: "/").first
}
if let addrArr = tun["inet4_address"] as? [String],
let first = addrArr.first {
return first.components(separatedBy: "/").first
}
return nil
}
/// configJson dns.servers address
private func parseDNSServers(from json: String) -> [String]? {
guard let data = json.data(using: .utf8),
let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let dns = root["dns"] as? [String: Any],
let servers = dns["servers"] as? [[String: Any]] else { return nil }
let addrs = servers.compactMap { $0["address"] as? String }
return addrs.isEmpty ? nil : addrs
}
/// 退 pass-through
private func minimalFallbackConfig() -> String {
return """
{
"log": {"level": "info"},
"dns": {"servers": [{"address": "\(defaultDNS)"}]},
"inbounds": [],
"outbounds": [{"type": "direct", "tag": "direct"}],
"tun": {
"enabled": true,
"inet4_address": "\(defaultTunAddr)/24",
"auto_route": true,
"mtu": 1500
}
}
"""
}
// MARK:
private func makeError(_ message: String) -> NSError {
NSError(domain: "PacketTunnelProvider", code: -1,
userInfo: [NSLocalizedDescriptionKey: message])
}
private func makePosixError(_ prefix: String) -> NSError {
NSError(domain: NSPOSIXErrorDomain, code: Int(errno),
userInfo: [NSLocalizedDescriptionKey: "\(prefix): \(String(cString: strerror(errno)))"])
}
}
// MARK: LibboxPlatformInterface
// Libbox.xcframework
// ObjC LibboxPlatformInterfacegomobile experimental/libbox
#if canImport(Libbox)
extension PacketTunnelProvider: LibboxPlatformInterface {
/// SO_MARK / setsockopt
/// iOS NE no-op
func autoDetectInterfaceControl(_ fd: Int32) throws {
// NE SO_MARK
}
/// sing-box
/// NE 访 nil
func findProcessInfo(
_ network: String?,
source: String?,
destination: String?,
uid: Int32
) throws -> LibboxProcessInfo? {
return nil
}
/// TUN fd
/// NE
/// 1. LibboxTunOptions startTunnel
/// 2. libbox fd read/write fd IP
/// 3. NE fd packetFlow
func openTun(_ options: LibboxTunOptions?) throws -> Int32 {
os_log("PacketTunnelProvider: openTun called (mtu=%d)", log: log, type: .info,
options?.mtu ?? 0)
guard tunFDs.libbox >= 0 else {
throw makeError("openTun: socketpair 未初始化")
}
return tunFDs.libbox
}
/// 使 /proc/netiOS procfs
func useProcFS() -> Bool { return false }
/// libbox os.log
func writeLog(_ message: String?) {
guard let msg = message, !msg.isEmpty else { return }
os_log("libbox: %{public}@", log: log, type: .debug, msg)
}
/// DNS iOS DNS no-op
func clearDNSCache() { }
}
#endif
@@ -21,6 +21,8 @@
CAFE000000000003000000AB /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = CAFE000000000003000000AA /* Info.plist */; };
/* PacketTunnel.appex embedded in Runner */
CAFE000000000004000000AB /* PacketTunnel.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = CAFE000000000004000000AA /* PacketTunnel.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* MemoryMonitor.swift in PacketTunnel Sources */
CAFE000000000006000000AC /* MemoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE000000000006000000AB /* MemoryMonitor.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -78,6 +80,12 @@
CAFE000000000003000000AA /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* PacketTunnel.appex product */
CAFE000000000004000000AA /* PacketTunnel.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PacketTunnel.appex; sourceTree = BUILT_PRODUCTS_DIR; };
/* Runner.entitlements */
CAFE000000000005000000AA /* Runner.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
/* PacketTunnel.entitlements */
CAFE000000000006000000AA /* PacketTunnel.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = PacketTunnel.entitlements; sourceTree = "<group>"; };
/* MemoryMonitor.swift (PacketTunnel) */
CAFE000000000006000000AB /* MemoryMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MemoryMonitor.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -137,6 +145,7 @@
97C146F11CF9000F007C117D /* Info.plist */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
CAFE000000000001000000AA /* VpnManager.swift */,
CAFE000000000005000000AA /* Runner.entitlements */,
1498D2311E8E86230040F4C2 /* GeneratedPluginRegistrant.m */,
1771E259F7494EBC8BB1F97A /* GeneratedPluginRegistrant.h */,
);
@@ -147,6 +156,8 @@
isa = PBXGroup;
children = (
CAFE000000000002000000AA /* PacketTunnelProvider.swift */,
CAFE000000000006000000AB /* MemoryMonitor.swift */,
CAFE000000000006000000AA /* PacketTunnel.entitlements */,
CAFE000000000003000000AA /* Info.plist */,
);
path = PacketTunnel;
@@ -303,6 +314,7 @@
buildActionMask = 2147483647;
files = (
CAFE000000000002000000AB /* PacketTunnelProvider.swift in Sources */,
CAFE000000000006000000AC /* MemoryMonitor.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -373,6 +385,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -502,6 +515,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -524,6 +538,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -544,6 +559,7 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
INFOPLIST_FILE = PacketTunnel/Info.plist;
@@ -567,6 +583,7 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
INFOPLIST_FILE = PacketTunnel/Info.plist;
@@ -590,6 +607,7 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = PacketTunnel/PacketTunnel.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
INFOPLIST_FILE = PacketTunnel/Info.plist;
+3 -2
View File
@@ -47,8 +47,9 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<!-- VPN 使用说明(App Store 审核必填) -->
<!-- 网络隧道使用说明(iOS 系统必填,显示于系统权限对话框)
⚠️ 红线词规范:避免 VPN/翻墙 等词,使用「网络加速」口径 -->
<key>NSVPNUsageDescription</key>
<string>穿山甲使用 VPN 为您的网络连接提供加密保护。</string>
<string>Pangolin 使用网络加速通道为您的连接提供安全保护。</string>
</dict>
</plist>
+33
View File
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!--
Runner.entitlements — 主 App Target Entitlements
Bundle ID: com.pangolin.pangolinVpn
申请说明:
1. com.apple.developer.networking.networkextension
→ "packet-tunnel-provider" 须在 Apple 开发者后台显式申请。
申请路径:developer.apple.com → Certificates, IDs & Profiles
→ Identifiers → com.pangolin.pangolinVpn → Capabilities
→ Network Extensions → Packet Tunnel Provider(勾选)
2. com.apple.security.application-groups
→ group.com.pangolin.pangolinVpn 须在 App Groups 注册。
主 App 与 PacketTunnel extension 共享同一 Group
用于 UserDefaults(suiteName:) / FileManager containerURL(forSecurityApplicationGroupIdentifier:)
传递少量共享状态(上次节点 tag、连接时间戳等)。
-->
<plist version="1.0">
<dict>
<!-- Network ExtensionPacket Tunnel Provider(主 App 控制面) -->
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
</array>
<!-- App Group:主 App ↔ PacketTunnel extension 共享容器 -->
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.pangolin.pangolinVpn</string>
</array>
</dict>
</plist>
+12 -4
View File
@@ -1,14 +1,15 @@
// VpnManager.swift App NETunnelProviderManager
// 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
//
// TODO(11F): start/stop / NEVPNStatus
// VpnStatusStreamHandler.shared.eventSink Dart
// 线 VPN / / / / 穿 /
// Go anywherelocalizedDescription 使Pangolin
import Foundation
import NetworkExtension
@@ -65,6 +66,8 @@ class VpnManager: NSObject {
// 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?
@@ -99,7 +102,8 @@ class VpnManager: NSObject {
proto.providerBundleIdentifier = extensionBundleId
proto.serverAddress = serverAddress
manager.protocolConfiguration = proto
manager.localizedDescription = "穿山甲 Pangolin VPN"
// 线localizedDescription VPN/
manager.localizedDescription = "Pangolin 加速"
manager.isEnabled = true
manager.saveToPreferences { [weak self] error in
if error == nil { self?.tunnelManager = manager }
@@ -116,6 +120,10 @@ class VpnManager: NSObject {
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)