macOS PacketTunnel 系统扩展 realize 失败(OSSystemExtensionErrorDomain code=4)踩坑复盘

Pangolin 客户端 · P1 原生隧道 · 2026-06-21 · 在 macOS 15.3.2 / 26 上排查与修复

一句话结论: 客户端内嵌的 PacketTunnel 系统扩展(NEPacketTunnelProvider)一直无法激活,报 code=4 "Extension not found in App bundle. Unable to find any matched extension"。 表面像"找不到扩展",实则是 三个叠加的工程配置 bugsysextd 在 realize/暂存/分类校验三个不同阶段先后拒绝。 逐个修复后扩展成功 activated enabled、隧道连通。

1. 现象

客户端点"连接"时,主 app 调 OSSystemExtensionRequest.activationRequest 激活内嵌的 packet-tunnel 系统扩展,但回调恒为失败:

sysext didFailWithError ✗ domain=OSSystemExtensionErrorDomain code=4
desc=Extension not found in App bundle. Unable to find any matched extension
     with identifier: com.pangolin.pangolin.PacketTunnel

诡异之处:

2. 走过的弯路(全部排除)

以下都查过并确认不是根因,记录下来免得后人重复:

假设结论
签名 / 公证 / 时间戳 / hardened runtime / get-task-allow全部正确
entitlements 变体(packet-tunnel-provider-systemextension)正确,且描述文件已授权
Info.plist 用 NetworkExtension/NEProviderClasses(而非 appex 的 NSExtension)正确(sysext 就该用这个)
Xcode 26 beta / macOS 26.2 SDK 工具链太新证伪:Tailscale 用更新的 SDK 26.5 照样能用
App 文件归属(user vs root:wheel)改 root:wheel 无效
App 不在 /Applications / LaunchServices 注册 / 翻译重定位都正常,无效
systemextensionsctl reset 清缓存无效
App Group 用 iOS 式 group. 前缀确实该改成 macOS 原生格式,但单独改它仍失败

3. 破局方法:在同一台机器上对照一个"能用的"开源同类

关键转折是不再盲猜,而是拿一个已知能用、且分发模型完全相同的开源 app 在同一台机器上对照。我们选了 Tailscale 独立版(io.tailscale.ipn.macsys):同样是 Developer ID 公证 + NEPacketTunnelProvider 系统扩展分发(非 App Store appex)。

把本机能用的 Tailscale.app 直接拷到出问题的测试机(cara,干净 macOS 15.3.2,Intel),它的扩展一路走完 validating → staging → validating_by_category → activated_waiting_for_user 并弹出"允许扩展"框。

这一步同时证明了两件事:

附:也对照了 Shadowrocket,但它是 App Store 的 appex 模型(NSExtension、系统预信任、不走 sysextd 批准流),与我们不是一个机制,不能直接类比。要对照必须找同为 Developer ID system extension 的实现。

4. 三个根因(按 sysextd 处理阶段排序)

根因 ① 扩展不自包含 staging 前被拒

这是 Flutter + 扩展工程的经典坑。工程用 CocoaPods,项目级配置经 Release.xcconfig 注入了链接所有 pod 框架的 OTHER_LDFLAGSPacketTunnel target 没有自己的 OTHER_LDFLAGS,于是继承了项目级的,把主 app 的 Flutter 插件 flutter_secure_storage_macos.framework 也链进了扩展——而该框架并不在扩展 bundle 内:

$ otool -L PacketTunnel.systemextension/Contents/MacOS/PacketTunnel
  @rpath/flutter_secure_storage_macos.framework/.../flutter_secure_storage_macos  ← 悬空依赖!
  /System/Library/Frameworks/...

同时,gomobile 产出的 Libbox.xcframework 实为静态库(ar archive),已被静态链进扩展二进制,却又被 Embed Frameworks 冗余地塞了一份畸形的 Libbox.framework 进扩展。

system extension 必须自包含(realize 时会被拷到 /Library/SystemExtensions/<UUID>/ 独立运行,相对 rpath 回不到主 app)。

修复:

验证:otool -L 扩展二进制应只剩 /System/.../usr/lib/...,无任何 @rpath;Contents/ 下不再有 Frameworks/ 目录(与 Tailscale 一致)。

根因 ② 扩展 bundle 名 ≠ bundle 标识符 staging 阶段

我们扩展的产物名是 PacketTunnel.systemextension,而 CFBundleIdentifiercom.pangolin.pangolin.PacketTunnel。Tailscale 的产物名 = 标识符(io.tailscale.ipn.macsys.network-extension.systemextension)。

在受影响的 macOS 上,sysextd 必须能按标识符把 bundle 拷进暂存区;名字对不上时,在 attempting to realize 之后直接静默 cancel,进不了 staging。改名后日志立刻出现 [Staging] Imported: .../com.pangolin.pangolin.PacketTunnel.systemextension

修复:PacketTunnel target 的 PRODUCT_NAME$(TARGET_NAME) 改为 com.pangolin.pangolin.PacketTunnel(三个配置 Debug/Release/Profile 都改)。

连带:PRODUCT_MODULE_NAME 会变成 com_pangolin_pangolin_PacketTunnel,而 Info.plist 的 NEProviderClasses$(PRODUCT_MODULE_NAME).PacketTunnelProvider 自动跟随,无需手改。

根因 ③ 扩展 Info.plist 缺 NSSystemExtensionUsageDescription category 校验阶段

过了 staging 后,日志终于给出明确原因:

com.pangolin.pangolin.PacketTunnel: extension failed category property check:
extensions belonging to the com.apple.system_extension.network_extension category
require the presence of the 'NSSystemExtensionUsageDescription' property.
→ uninstalling invalid extension

网络扩展类别强制要求扩展自己的 Info.plist 里有 NSSystemExtensionUsageDescription。我们之前只在主 app 的 Info.plist 加了——不顶用。Tailscale 的扩展 Info.plist 里就有这个键。

修复:PacketTunnel/Info.plist 顶层加:

<key>NSSystemExtensionUsageDescription</key>
<string>Pangolin 需要安装网络扩展以提供安全的网络连接。</string>

附带一并修正的项

5. 成功的 sysextd 日志长这样

attempting to realize extension with identifier com.pangolin.pangolin.PacketTunnel
[Staging] Imported: .../com.pangolin.pangolin.PacketTunnel.systemextension
advancing state from staging to validating
advancing state from validating to validating_by_category
Category delegate com.apple.system_extension.network_extension returned error (null)
advancing state ... to activated_waiting_for_user
observer 'notify user' reached a success state    # ← 弹"允许扩展"框

# 用户在 系统设置 → 隐私与安全性 点允许后:
sysext didFinishWithResult ✓ result=0 (completed)
$ systemextensionsctl list
  * * BYL4KQHMTN com.pangolin.pangolin.PacketTunnel (1.0/1) [activated enabled]

6. 给后人的排查 checklist(Developer ID 网络系统扩展)

  1. 先找同模型的能用实现对照(Tailscale 独立版 / Mullvad),在同一台机器上验证环境没问题,把范围锁到自己的包。
  2. 扩展二进制 otool -L 必须@rpath 外部依赖(自包含)。警惕 CocoaPods/Flutter 的 OTHER_LDFLAGS 继承。
  3. 扩展 bundle 名 = CFBundleIdentifier(设 PRODUCT_NAME = 标识符)。
  4. 扩展 Info.plist 必须有 NSSystemExtensionUsageDescription(不是主 app 的)。
  5. App Group 用 macOS 原生 <TeamID>.<name>;NEMachServiceName 以该 group 为前缀。
  6. 沙箱扩展按需补 network.client/server、文件访问等能力。
  7. 静态库 xcframework 只 LinkEmbed;动态 framework 才需 Embed + CodeSignOnCopy。
  8. 排查时:log stream --debug --predicate 'process=="sysextd" OR process=="syspolicyd"',看卡在 realize / staging / validating_by_category 哪一步;category 校验失败会打出明确缺什么键。

7. 已知环境坑:macOS 26 (Tahoe) 回归

在 macOS 26 上,即使包完全正确,sysextd 仍可能报 no policy, cannot allow apps outside /Applications(app 明明在 /Applications)。这是 Apple 在 Tahoe 的已知回归(开发者论坛多人复现、非 MDM 个人机),不是我们能修的,也不影响 macOS 15/14 的真实用户。开发机若是 26,本机调试可考虑关 SIP 后开 systemextensionsctl developer on,或在 15/14 机器上验证。


改动文件:client/macos/PacketTunnel/Info.plist · client/macos/PacketTunnel/PacketTunnel.entitlements · client/macos/Runner/Release.entitlements · client/macos/PacketTunnel/PacketTunnelProvider.swift · client/macos/Runner.xcodeproj/project.pbxproj(OTHER_LDFLAGS/PRODUCT_NAME/移除 Embed Libbox/加 timestamp)。验证机:cara,干净 macOS 15.3.2 Intel。