docs+test: macOS 隧道踩坑复盘/规则 + VPN 测试调研 + 黑盒测试工具
- docs/macos-sysext-realize-troubleshooting.html:补"运行时"章节(libbox 空指针、 接口时序、队列死锁、DNS 劫持)+ 运行时 checklist。 - docs/vpn-testing-research.md:VPN 测试调研(行业方法论 + GFW 专项 + Pangolin 分层测试项 + 三层测试工具设计)。 - CLAUDE.md:新增「client/ macOS 原生隧道」铁律章节(构建发版/系统扩展 realize 要求/ libbox+NE 集成/配置由服务端渲染/已知坑)。 - scripts/vpn_test.sh:客户端侧黑盒测试工具(出口 IP/DNS/可达性/延迟/吞吐/IPv6 泄漏, PASS/WARN/FAIL,可远程跑)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEHzjEcFzvGwgbxT6Wbt6c
This commit is contained in:
@@ -159,6 +159,51 @@ sysext didFinishWithResult ✓ result=0 (completed)
|
||||
$ systemextensionsctl list
|
||||
* * BYL4KQHMTN com.pangolin.pangolin.PacketTunnel (1.0/1) <span class="g">[activated enabled]</span></pre>
|
||||
|
||||
<h2>5.5 加载之后:从"扩展能起"到"真正连通"(运行时)</h2>
|
||||
<p>系统扩展能 realize/激活只是第一步。让内嵌的 libbox(sing-box)真正建起隧道、能上网,又踩了四个坑(均在 <code>PacketTunnelProvider.swift</code> / 服务端配置):</p>
|
||||
|
||||
<div class="card root">
|
||||
<h3>运行时 ① libbox <code>startOrReloadService(options:)</code> 传 nil → 空指针崩溃 <span class="tag bad">扩展进程 SIGABRT</span></h3>
|
||||
<p>扩展进程启动 360ms 后自杀(SIGABRT)。把 stderr 重定向到 App Group 容器文件后抓到 Go panic:</p>
|
||||
<pre><span class="r">panic: runtime error: invalid memory address or nil pointer dereference</span>
|
||||
libbox.(*CommandServer).StartOrReloadService(server, {config}, <span class="r">0x0</span>) command_server.go:175</pre>
|
||||
<p>此版本 libbox 会解引用第三个 <code>options</code> 参数(虽标 <code>_Nullable</code>)。<strong>修复:</strong>传非空 <code>LibboxOverrideOptions()</code> 而非 <code>nil</code>。</p>
|
||||
</div>
|
||||
|
||||
<div class="card root">
|
||||
<h3>运行时 ② 默认接口监控异步返回 → <code>no available network interface</code> <span class="tag bad">启动报错</span></h3>
|
||||
<p><code>startDefaultInterfaceMonitor</code> 启动 <code>NWPathMonitor</code> 后立即返回,但首个 path 回调是异步晚到的;sing-box 紧接着的网络操作拿到的默认接口索引还是 -1。<strong>修复:</strong>用信号量**阻塞到首个 path 更新再返回**(带 5s 超时),对齐 sing-box-for-apple。</p>
|
||||
</div>
|
||||
|
||||
<div class="card root">
|
||||
<h3>运行时 ③ provider 队列三方死锁 → 隧道永远卡 connecting <span class="tag bad">最隐蔽</span></h3>
|
||||
<p>openTun 打点显示卡在 <code>setTunnelNetworkSettings</code> 的信号量等待,回调永不触发:</p>
|
||||
<ul>
|
||||
<li>NE 在 <b>provider 队列</b>上调 <code>startTunnel</code> → 同步调 <code>startOrReloadService</code>(阻塞该队列)</li>
|
||||
<li>sing-box 在 Go 线程调 openTun → <code>sem.wait()</code> 等 <code>setTunnelNetworkSettings</code> 完成</li>
|
||||
<li>而该完成回调**正要在被阻塞的 provider 队列上投递** → 死锁</li>
|
||||
</ul>
|
||||
<p><strong>修复:</strong>把 libbox 启动整段放进后台队列(<code>DispatchQueue.global().async</code>),<code>startTunnel</code> 立即返回、provider 队列腾出来投递回调。</p>
|
||||
</div>
|
||||
|
||||
<div class="card root">
|
||||
<h3>运行时 ④ 缺 DNS 劫持 → 隧道连上但打不开网站 <span class="tag bad">最后一关</span></h3>
|
||||
<p>隧道起来了、TCP 能经 REALITY 出海,但域名打不开。box.log 显示发往隧道 DNS 的查询走了直连:</p>
|
||||
<pre>inbound packet connection to <span class="y">172.19.0.2:53</span>
|
||||
router: match ip_cidr=[..<span class="r">172.16.0.0/12</span>..] => route(<span class="r">direct</span>) <span class="c"># DNS 被 LAN 规则吞去直连 → 解析失败</span></pre>
|
||||
<p>隧道 DNS 地址 172.19.0.2 落在 LAN 直连规则 172.16/12 内,被路由成直连(发往不存在的主机)→ 解析全失败。<strong>修复(服务端 <code>clientconfig.go</code>):</strong>route.rules 首条加 <code>{"action":"hijack-dns","port":[53]}</code>(排在 LAN 规则之前),把 :53 查询交给 sing-box DNS 模块。</p>
|
||||
<p class="small">注:sing-box 1.13 的 <code>protocol:"dns"</code> 匹配需先 sniff;按目的端口 53 匹配最稳、不依赖 sniff。</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p><strong>另一个发版必知:每次构建必递增 <code>CFBundleVersion</code>。</strong> <code>sysextd</code> 按(标识符, 版本)去重;同版本号重装**不会替换**已激活的旧扩展,跑的还是旧代码(排查时极易被误导)。</p>
|
||||
</div>
|
||||
|
||||
<p><strong>连通验证(从命令行):</strong></p>
|
||||
<pre>$ curl https://api.ipify.org → <span class="g">103.119.13.48</span> <span class="c"># 出口=节点 IP,流量走隧道</span>
|
||||
$ curl -o/dev/null -w '%{http_code}' https://github.com → <span class="g">200</span>
|
||||
box.log: router: match[0] port=53 => <span class="g">hijack-dns</span> → dns: exchanged A github.com 140.82.116.3</pre>
|
||||
|
||||
<h2>6. 给后人的排查 checklist(Developer ID 网络系统扩展)</h2>
|
||||
<ol>
|
||||
<li><b>先找同模型的能用实现对照</b>(Tailscale 独立版 / Mullvad),<u>在同一台机器</u>上验证环境没问题,把范围锁到自己的包。</li>
|
||||
@@ -178,8 +223,18 @@ $ systemextensionsctl list
|
||||
<code>systemextensionsctl developer on</code>,或在 15/14 机器上验证。</p>
|
||||
</div>
|
||||
|
||||
<h2>8. 运行时 checklist(libbox + NE 集成)</h2>
|
||||
<ol>
|
||||
<li>libbox <code>startOrReloadService(options:)</code> 传**非空** <code>LibboxOverrideOptions()</code>,别传 nil。</li>
|
||||
<li><code>startTunnel</code> 里的 libbox 启动放**后台队列**(<code>DispatchQueue.global().async</code>),别在 provider 队列同步跑(死锁)。</li>
|
||||
<li><code>startDefaultInterfaceMonitor</code> **阻塞到首个 path 更新再返回**。</li>
|
||||
<li>TUN 配置必须有 **DNS 劫持**(<code>action:hijack-dns</code>,按 port 53),排在 LAN/分流规则之前。</li>
|
||||
<li>**每次构建递增 <code>CFBundleVersion</code>**,否则 sysextd 不更新已激活的扩展。</li>
|
||||
<li>看 libbox 自身日志:配置 <code>log.output</code> 指向容器文件(<code>writeLogs</code> 回调启动期不触发);Go fatal 走 stderr,需把扩展 stderr 重定向到文件才看得到。</li>
|
||||
</ol>
|
||||
|
||||
<hr>
|
||||
<p class="small">改动文件:<code>client/macos/PacketTunnel/Info.plist</code> · <code>client/macos/PacketTunnel/PacketTunnel.entitlements</code> · <code>client/macos/Runner/Release.entitlements</code> · <code>client/macos/PacketTunnel/PacketTunnelProvider.swift</code> · <code>client/macos/Runner.xcodeproj/project.pbxproj</code>(<code>OTHER_LDFLAGS</code>/<code>PRODUCT_NAME</code>/移除 Embed Libbox/加 timestamp)。验证机:cara,干净 macOS 15.3.2 Intel。</p>
|
||||
<p class="small">改动文件:扩展 <code>Info.plist</code>/<code>entitlements</code>/<code>main.swift</code>/<code>PacketTunnelProvider.swift</code> · <code>Runner/Release.entitlements</code> · <code>Runner.xcodeproj/project.pbxproj</code>(<code>OTHER_LDFLAGS</code>/<code>PRODUCT_NAME</code>/移除 Embed Libbox/<code>--timestamp</code>/版本号) · <code>scripts/local_test.sh</code> · <code>client/macos/sign_libbox.sh</code> · 服务端 <code>server/internal/httpapi/clientconfig.go</code>(DNS 劫持)。验证机:cara,干净 macOS 15.3.2 Intel,出口 IP=节点、国外站可达。</p>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user