Pangolin Flutter 客户端 · 目标里程碑 TestFlight 公测 · 分支 feature/pangolin-ios · 2026-06-22
一句话:iOS 不是从零起步,而是停在一个 “M3 PoC” 半成品上 —— Xcode 工程、Swift 源码、entitlements、iPad 布局骨架都在,唯一硬阻塞是 Libbox.xcframework 只编了 macOS 切片。本方案把 iOS 隧道从「能编译的桩」推到「真机能连通、扛得住 NE 内存红线、可上 TestFlight」,iPad 跟着 iPhone 一起好(同一个 iOS target,非单独版本)。
| 维度 | 本轮做 | 本轮不做 |
|---|---|---|
| 连通性 | iPhone / iPad 真机真正连上网、出网走自有节点 | — |
| 分发 | App Store Connect 配置 + 描述文件 + TestFlight 外部测试 | 完整 App Store 上架审核合规(隐私清单细节、审核问答) |
| iPad | 布局适配 + 横屏(宽屏主从/侧边栏,6 屏不拉伸) | Split View / Stage Manager 动态重排、外接键盘/指针/拖拽 |
| 平台 | iOS 12+,设备族 iPhone + iPad(已是 "1,2") | tvOS / visionOS |
前置条件已具备:付费 Apple Developer 账号(TeamID BYL4KQHMTN),可上 App Store Connect。
| 件 | 状态 | 说明 |
|---|---|---|
| Xcode 工程接线 | 已有 | Runner + PacketTunnel 两 target 已接好;设备族 iPhone+iPad;部署目标 iOS 12 |
| iOS Swift 源码 | PoC 桩 | PacketTunnelProvider.swift(20KB) / VpnManager.swift / MemoryMonitor.swift 在,但 Provider 未真正接通 libbox |
| entitlements | 已有 | 正确的 iOS 格式:NE packet-tunnel-provider + App Group group.com.pangolin.pangolinVpn |
| Bundle ID | 已定 | 主 App com.pangolin.pangolinVpn,扩展 com.pangolin.pangolinVpn.PacketTunnel |
| Libbox.xcframework | 缺 iOS 切片 | 当前只有 macos-arm64_x86_64,缺 ios-arm64 + ios-arm64-simulator |
| DEVELOPMENT_TEAM | 未设 | iOS 工程未填 team(macOS 已填 BYL4KQHMTN) |
| iPad 布局 | 骨架 | shell/tablet_shell.dart + core/responsive/form_factor.dart 起步;design/ui_kits/tablet 有原型 |
| 关键修复点 | macOS | iOS |
|---|---|---|
| 后台队列启 libbox(防三方死锁) | ✓ | 半(有 DispatchQueue.global,未接启动序列) |
非空 LibboxOverrideOptions()(防 SIGSEGV) | ✓ | ✗ |
阻塞式 startDefaultInterfaceMonitor | ✓ | ✗ |
startOrReloadService 真正拉起 sing-box | ✓ | ✗ |
| DNS 劫持首条规则 | 服务端渲染下发 | 服务端渲染下发(同源,无需客户端改) |
仓库本就分 macos/PacketTunnel 与 ios/PacketTunnel。两端是真分叉:System Extension vs App Extension、App Group 前缀 BYL4KQHMTN. vs group.、iOS 独有 ~50MB 内存红线。接受可控重复,在 iOS Provider 文件头放一份「与 macOS 的 parity 对照」注释防漂移。
取舍:成本最低、最贴合现有结构。
把 interface monitor / libbox 启动序列 / 配置 IPC 抽成共享文件,平台只留差异 shim。无漂移,但要给 ios + macos 两个独立 Flutter 工程都接好跨 target 共享文件,Xcode 这块很折腾。抽象收益压不过两套工程的接线成本。
bash scripts/build-libbox.sh apple(不带 platform)→ 产出含 ios-arm64(真机) + ios-arm64-simulator 切片的 Libbox.xcframework,放 client/ios/Frameworks/。PacketTunnel 设 OTHER_LDFLAGS="" 切断 CocoaPods 继承;otool -L 验扩展二进制零 @rpath 外部依赖。LibboxOverrideOptions()、阻塞式 startDefaultInterfaceMonitor(阻塞到首个 path 更新再返回)、startOrReloadService 真正拉起 sing-box。BuildClientConfig 渲染原样下发,客户端不拼;DNS 劫持首条规则随配置下发。MemoryMonitor(已写好)在真机跑满配置打点,验 NE 进程峰值 < 设备 jetsam 上限。group.com.pangolin.pangolinVpn,无需改格式。ios/Runner.xcodeproj 的 DEVELOPMENT_TEAM = BYL4KQHMTN(主 App + 扩展两 target)。VpnManager.swift(主 App)↔ Provider 经 App Group UserDefaults 通 status/stats(含崩溃恢复读取)。shell/tablet_shell.dart + core/responsive/form_factor.dart 做成完整宽屏主从/侧边栏。design/ui_kits/tablet 原型还原。com.pangolin.pangolinVpn + extension ID + 勾 NE capability(packet-tunnel-provider) + 建 App Group。ITSAppUsesNonExemptEncryption、Archive、上传、TestFlight 外部测试 beta 审核(比正式审核轻)。iOS Network Extension 进程 ~50MB 内存硬顶(旧设备 ~15MB)× sing-box + 远程 rule-set。
配置层 clientconfig.go 的国内分流走远程拉 geoip-cn.srs / geosite-cn.srs,启动期下载进内存,在 NE 进程里是 jetsam 杀进程的主因。
两道闸:
MemoryMonitor 在真机跑满配置,拿到峰值 resident 再决定。.srs 预取(避免启动期下载进内存),必要时 iOS 走精简 rule-set。⚠️ TestFlight 推送以「内存实测通过」为前置闸门 —— 内存不过关不推。
| 风险 | 缓解 |
|---|---|
| iOS Provider 移植引入新死锁/崩溃 | 严格照 macos-sysext 排障 runbook 的铁律;真机 device console 验证 |
| 受限网络封高位端口 | REALITY 数据口优先走节点 443(与 iPhone/macOS 同策略) |
| iPad 布局漂移设计稿 | 对照 design/ui_kits/tablet 原型;遵守 token 单源 |
scripts/build-libbox.sh apple 产出含 iOS 切片的 xcframework,otool -L 验扩展零外部 @rpath 依赖。MemoryMonitor 真机峰值 < 设备 jetsam 上限(10 分钟稳定运行不被杀)。下一步:进 writing-plans 出逐步实施计划。本文档登记于 docs/index.html。