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:
wangjia
2026-06-22 08:58:10 +08:00
parent cebc9a1c4f
commit 2d2baf887c
4 changed files with 343 additions and 1 deletions
+56 -1
View File
@@ -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>
+124
View File
@@ -0,0 +1,124 @@
# VPN 测试调研(测什么 / 怎么测 / Pangolin 测试工具设计)
> 目的:在动手写测试工具前,先搞清楚 VPN 该测哪些维度、行业与中国/GFW 场景怎么测,
> 再据此设计 Pangolin 自己的测试工具。日期 2026-06-22。
---
## TL;DR
VPN 测试 = **连得上 + 连得对 + 连得快 + 连得稳 + 不泄漏 + (中国场景)穿得过且不被封**
六大类:**连通性 / 正确性(出口·DNS·分流)/ 泄漏 / 性能(延迟·吞吐·丢包)/ 稳定性(掉线·重连·killswitch)/ 抗封锁(GFW)**。
Pangolin 是面向大陆、REALITY+sing-box、macOS 自研客户端,**抗封锁 + 国内分流正确性 + 晚高峰吞吐**是和通用评测最不同、最该重点测的。
---
## 1. 行业通用 VPN 评测怎么测
主流评测站(Security.org、Cloudwards、PCMag 等)的共识方法论:
| 维度 | 测什么 | 怎么测 / 工具 | 通过标准 |
|---|---|---|---|
| **速度/吞吐** | 下载、上传、相对基线的衰减 | Ookla/iperf,**多服务器距离**(本地/美/英/亚)、**多时段**(早/晚高峰)、先测裸连基线再测开 VPN | 近端衰减 <1020% 算优;远端单独看 |
| **延迟** | RTT(ping)、首包延迟 | ping / curl `time_connect` | 越低越好;近端 +几十 ms 可接受 |
| **DNS 正确性** | 域名能否解析、解析延迟 | `dig`/`nslookup`、curl `time_namelookup` | 能解析、不超时 |
| **IP 泄漏** | 真实 IP 是否暴露(IPv4/IPv6) | 访问 ipify/ifconfig.me/ipleak.net,对比开关 VPN 出口 | 出口必须是节点 IP,**不能是本机真实 IP** |
| **DNS 泄漏** | DNS 查询是否走了本地 ISP | dnsleaktest.com、Wireshark 抓 53 端口去向 | 查询只经隧道/指定 DNS |
| **WebRTC 泄漏** | 浏览器 WebRTC 暴露真实 IP | browserleaks.com/webrtc | 不暴露 |
| **IPv6 泄漏** | IPv6 绕过隧道 | test-ipv6.com | 无 v6 泄漏(或 v6 也走隧道) |
| **Kill Switch** | 隧道意外断开时是否阻断流量 | 强杀扩展/断网,看是否还能裸连出网 | 断开瞬间流量被掐 |
| **流媒体/可达性** | 能否访问目标站点 | 实访 Netflix/YouTube/Google 等 | 能正常打开/播放 |
| **稳定性** | 长时连接掉线率、重连 | 长跑数小时,周期性请求 | 不频繁掉线;掉了能自动重连 |
| **抓包审计** | 是否有明文/异常外联 | Wireshark/tcpdump | 无明文、无意外外联 |
要点:**先测基线(不开 VPN)再测开 VPN 做对比**;**多次/多时段**取分布而非单点。
---
## 2. 中国 / GFW 场景特有的测法(Pangolin 重点)
通用评测不覆盖、但对面向大陆的 VPN 是生死线:
- **抗 GFW 封锁(核心)**
- GFW 一旦判定某连接是代理,会对 **(client IP, server IP, server port) 三元组封 120180 秒**(期间丢所有包)。测试:连一段时间后看是否突然全断、过几分钟又能连——这是被三元组封的特征。
- **主动探测(active probing)**:GFW 会主动连你的端口探测。REALITY 的防御是把探测流量转发给真实伪装站(如 apple.com)。测试:用普通 TLS 客户端(curl/openssl)连节点的 REALITY 端口,应表现得**像真实伪装站**(返回真站证书/内容),而不是暴露代理特征。
- **全加密流量检测**:GFW 对"高熵全加密"流量有启发式封锁。REALITY/VLESS 借真证书规避。测试:长期存活率(连续几天能否一直连)。
- **晚高峰吞吐**:国内出海带宽晚高峰(20:00–24:00)严重劣化。**必须按时段测吞吐**(白天 vs 晚高峰),单测白天没意义。
- **MTU / 分片**:运营商(尤其移动 5G)对大包/分片敏感,会借此识别。业界经验 **MTU clamp 到 1350(IPv6 1280)** 降低被识别和卡顿。测试:大包传输是否卡顿、丢包率。⚠️ 我们 TUN 配的是 `mtu 9000`(sing-box 默认),需评估对 REALITY-over-TCP 的实际影响。
- **国内分流正确性(#5)**:`geoip-cn`/`geosite-cn` 命中应**直连**(不走隧道),国外才走隧道。测试:访问国内站(如 baidu)出口应是**本地 IP**,国外站出口是**节点 IP**——分流错了要么国内变慢、要么国外漏走直连。
- **协议可达性矩阵**:REALITY(TCP 443)、Hysteria2(UDP 443)分别在不同网络(电信/联通/移动、家宽/5G)下的可达性。
参考:GFW 对全加密/QUIC 的检测与封锁机制见 gfw.report 的 USENIX 论文。
---
## 3. Pangolin 该测什么(结合架构分层)
架构:macOS 客户端(NEPacketTunnelProvider 系统扩展 + 内嵌 libbox/sing-box)→ REALITY(TCP)/Hy2(UDP)→ 节点 → 出海。控制面 :8080 下发配置。
### A. 连通性(能不能起来)
- 系统扩展 `activated enabled`(`systemextensionsctl list`)。
- 隧道接口起来(utun + `172.19.0.1`)、NEVPNStatus = connected。
- 控制面可达(:8080)、REALITY 数据口可达(节点 endpoint 端口)。
- **首次连接耗时**(含批准)、**重连耗时**。
### B. 正确性(连得对)
- **出口 IP = 节点 IP**(`curl ipify`),证明真走隧道。
- **DNS 解析正常**(域名能开;`hijack-dns` 生效——这次的坑)。
- **国内分流**:国内站出口=本地、国外站出口=节点(开 smartRoute 时)。
-**IP/DNS/IPv6 泄漏**
### C. 性能(连得快)
- 延迟:到节点 RTT、隧道内到常见站 RTT。
- 吞吐:下载/上传速度,**白天 vs 晚高峰**,对比裸连基线。
- 丢包率、DNS 解析延迟。
### D. 稳定性(连得稳)
- 长跑掉线率、自动重连。
- Kill switch(`strict_route` 已开):强杀扩展后流量是否被掐。
- 节点 agent 热重载(SIGHUP)后客户端是否平滑。
### E. 抗封锁(穿得过)
- REALITY 端口被动探测表现(伪装站特征)。
- 长期存活率(多日)。
- 三元组封锁特征监测。
---
## 4. 测试工具设计建议
分三层,**先做能自动化、回报最高的"客户端侧黑盒探测脚本"**:
### 工具一:`scripts/vpn_test.sh`(客户端侧黑盒,优先做)
在已连接的机器(本机/cara)上跑,一条命令出报告。覆盖 B+C 大部分:
- **出口 IP**:`curl ipify`,判定 = 节点 / = 本地 / 超时。
- **DNS**:解析 github/google + `time_namelookup`;开/关分流分别测国内外站出口。
- **可达性矩阵**:对一组目标(google/youtube/github/baidu…)测 HTTP 码 + 耗时。
- **延迟**:`curl time_connect` 到节点和若干站。
- **吞吐**:下载固定大小文件测 `speed_download`(用 cloudflare speed / 自建测速点)。
- **泄漏**:IPv6 出口、DNS 出口对比。
- 输出:表格 + 时间戳,可重复跑做时段对比。
- 复用本会话已验证的 `ssh cara '...'` 远程跑法。
### 工具二:节点侧探针(可选)
- REALITY 端口被动探测模拟(openssl 连 443 看是否像伪装站)。
- 三元组封锁监测(从国内 vantage 持续发包看存活)。
### 工具三:客户端内置自检(产品化,后续)
- app 内"诊断"按钮:跑连通/出口/DNS 自检,给用户一键报告(也利于支持排障)。
### 输出与判定
- 每项给 **PASS/WARN/FAIL** + 实测值 + 阈值。
- 支持 **基线对比**(先 `--baseline` 裸连存一份,再开 VPN 对比衰减)。
- 支持 **多时段**(cron 跑,产出趋势)。
---
## 5. 参考来源
- Security.org《Best VPN Services of 2026: 50+ VPNs Tested》— 速度/泄漏/流媒体方法论:https://www.security.org/vpn/best/
- Cloudwards《VPN Test: DNS Leaks, IP Address Leaks, Speed Issues》:https://www.cloudwards.net/vpn-test-guide/
- DoVPN《IP Leak Test Guide》:https://dovpn.com/ip-leak-test-guide/
- gfw.report — GFW 对全加密流量的检测(USENIX'23):https://gfw.report/publications/usenixsecurity23/en/
- gfw.report — GFW 对 QUIC/SNI 的封锁(USENIX'25):https://gfw.report/publications/usenixsecurity25/en/
- VLESS-REALITY 部署/绕过实践:https://greatfirewallguide.com/lab/vless-reality-vision