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"。
表面像"找不到扩展",实则是 三个叠加的工程配置 bug 让 sysextd 在 realize/暂存/分类校验三个不同阶段先后拒绝。
逐个修复后扩展成功 activated enabled、隧道连通。
客户端点"连接"时,主 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
诡异之处:
Contents/Library/SystemExtensions/ 里,sysextd 日志也打了 attempting to realize(说明找到了)。codesign --verify --deep --strict 通过。sysextd 在验签通过后 ~10ms 内静默 xpc_connection_cancel,自身不打印任何拒绝原因。以下都查过并确认不是根因,记录下来免得后人重复:
| 假设 | 结论 |
|---|---|
| 签名 / 公证 / 时间戳 / 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 原生格式,但单独改它仍失败 |
关键转折是不再盲猜,而是拿一个已知能用、且分发模型完全相同的开源 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 的实现。
这是 Flutter + 扩展工程的经典坑。工程用 CocoaPods,项目级配置经 Release.xcconfig 注入了链接所有 pod 框架的 OTHER_LDFLAGS。PacketTunnel 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)。
修复:
PacketTunnel 的 Debug/Release 配置加 OTHER_LDFLAGS = "";,切断对项目级 pod 链接标志的继承。Libbox.xcframework(它是静态库,只需 Link,不需 Embed)。验证:otool -L 扩展二进制应只剩 /System/... 与 /usr/lib/...,无任何 @rpath;Contents/ 下不再有 Frameworks/ 目录(与 Tailscale 一致)。
我们扩展的产物名是 PacketTunnel.systemextension,而 CFBundleIdentifier 是 com.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 自动跟随,无需手改。
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>
group.com.pangolin.pangolin(iOS 式)→ macOS 原生 BYL4KQHMTN.com.pangolin.pangolin(主 app + 扩展 entitlements + Swift appGroup 常量三处同步)。BYL4KQHMTN.com.pangolin.pangolin.PacketTunnel。com.apple.security.network.client / network.server(packet tunnel 要对外建连)。get-task-allow=false、Xcode 签名加 --timestamp(OTHER_CODE_SIGN_FLAGS)、构建期把静态 Libbox 以 Developer ID 重签。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]
otool -L 必须零 @rpath 外部依赖(自包含)。警惕 CocoaPods/Flutter 的 OTHER_LDFLAGS 继承。PRODUCT_NAME = 标识符)。NSSystemExtensionUsageDescription(不是主 app 的)。<TeamID>.<name>;NEMachServiceName 以该 group 为前缀。network.client/server、文件访问等能力。log stream --debug --predicate 'process=="sysextd" OR process=="syspolicyd"',看卡在 realize / staging / validating_by_category 哪一步;category 校验失败会打出明确缺什么键。在 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。