# 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"`。 表面像"找不到扩展",实则是 三个叠加的工程配置 bug 让 `sysextd` 在 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

诡异之处:

- 扩展明明在 `Contents/Library/SystemExtensions/` 里,`sysextd` 日志也打了 `attempting to realize`(说明找到了)。
- 签名、公证、staple、Gatekeeper 全部通过;`codesign --verify --deep --strict` 通过。
- `sysextd` 在验签通过后 ~10ms 内静默 `xpc_connection_cancel`,自身不打印任何拒绝原因。
- 从不弹"允许扩展"批准框——在批准阶段之前就被拒。

## 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` 并弹出"允许扩展"框。

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

- cara 的环境完全正常——能 realize 第三方 Developer ID 系统扩展。
- 所以 100% 是我们自己的包有问题(用户的直觉:"是我们代码的问题")。

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

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

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

这是 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 链接标志的继承。
- 从 Embed Frameworks 移除 `Libbox.xcframework`(它是静态库,只需 Link,不需 Embed)。

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

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

我们扩展的产物名是 `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` 自动跟随,无需手改。

### 根因 ③ 扩展 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>

### 附带一并修正的项

- App Group 格式:`group.com.pangolin.pangolin`(iOS 式)→ macOS 原生 `BYL4KQHMTN.com.pangolin.pangolin`(主 app + 扩展 entitlements + Swift `appGroup` 常量三处同步)。
- NEMachServiceName:必须以扩展所属 App Group 为前缀 → `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 重签。

## 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 只 Link 不 Embed;动态 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。