docs: 补 CLAUDE.md 移动端/跨端统计章节 + 新增 urltest 排障文档
ci-pangolin / Lint — shellcheck (push) Has been cancelled
ci-pangolin / OpenAPI Sync Check (push) Has been cancelled
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Has been cancelled
ci-pangolin / Flutter — analyze + test (push) Has been cancelled
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Has been cancelled
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Has been cancelled
ci-pangolin / Go — build + test (push) Has been cancelled
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Has been cancelled
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Has been cancelled
ci-pangolin / Golden — 视觉回归 (components + auth) (push) Has been cancelled
ci-pangolin / Lint — shellcheck (push) Has been cancelled
ci-pangolin / OpenAPI Sync Check (push) Has been cancelled
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Has been cancelled
ci-pangolin / Flutter — analyze + test (push) Has been cancelled
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Has been cancelled
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Has been cancelled
ci-pangolin / Go — build + test (push) Has been cancelled
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Has been cancelled
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Has been cancelled
ci-pangolin / Golden — 视觉回归 (components + auth) (push) Has been cancelled
- CLAUDE.md:加「移动端(iOS/iPad+Android)原生隧道」「跨端实时统计+urltest 延迟」两节—— libbox 构建前置(JDK 17/包名/gitignore)、clash_api 注入正解、libbox 单 client Group 坑、 Dart 共享广播流、连接态跳直连实测;附「最后在线取 last_seen」一句。 - 新增 docs/connect-latency-urltest.html(排障/Runbook):四端 urltest 实现 + 根因链 + 打点法, 登记进 docs/index.html。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -117,3 +117,37 @@ cd web/website && npm run gen:tokens
|
||||
**已知坑**
|
||||
- 开发机若是 **macOS 26 (Tahoe)**:`sysextd` 报 `no policy, cannot allow apps outside /Applications`(app 在 /Applications 也报)是 **Apple 回归**,本机调试需关 SIP 后 `systemextensionsctl developer on`;真实用户(macOS 14/15)不受影响。
|
||||
- #5 国内分流:客户端 `smartRoute` → `?split_cn=1` 下发远程 rule-set;**TODO 改本地 `.srs` 预取**,避免启动期下载。
|
||||
|
||||
## client/ 移动端(iOS/iPad + Android)原生隧道
|
||||
|
||||
同样内嵌 libbox,与 macOS 同「CommandServer 模型」,但形态/打包不同:
|
||||
|
||||
- **iOS/iPad**:`NEPacketTunnelProvider` 扩展(`client/ios/PacketTunnel/`),经 `NETunnelProviderManager` 管理(非 sysext);App Group 用 **iOS 的 `group.` 前缀**(`group.com.pangolin.pangolinVpn`)。`ctl_info`/`sockaddr_ctl` iOS SDK 不发,靠 `PacketTunnel-Bridging-Header.h` 手写声明。
|
||||
- **Android**:`PangolinVpnService`(`client/android/.../PangolinVpnService.kt`),VpnService + libbox **同进程**;明文联调 API 需 manifest `usesCleartextTraffic`(Android 9+ 默认禁 http)。
|
||||
- iOS/Android 与 macOS 同走 Dart `VpnNativeBridge`,所以 Dart 侧修复(状态/统计流)四端共享。
|
||||
|
||||
**libbox 构建(产物 gitignore,需手动生成)**——`scripts/build-libbox.sh`:
|
||||
- Apple:`bash scripts/build-libbox.sh apple macos`(只 macOS,快)/ `apple ios`(iOS+sim)→ `Libbox.xcframework` 放 `client/{macos,ios}/Frameworks/`。
|
||||
- Android:`bash scripts/build-libbox.sh android` → `Libbox.aar`,**重命名小写** `libbox.aar` 放 `app/kernel/dist/android/`。⚠️ gomobile 编 Android **强制 JDK 17**(JDK 21 直接拒);`export JAVA_HOME=/opt/homebrew/opt/openjdk@17/...` 再编。
|
||||
- ⚠️ **Android libbox Java 包名是 `io.nekohasekai.libbox`**(不是 `libbox`),Kotlin import 用前者。
|
||||
|
||||
## 跨端实时统计 + urltest 延迟(连接页"延迟"的唯一正解)
|
||||
|
||||
连接页"延迟"= 内核 urltest(经 REALITY 真实出站测 RTT);**坑很深,改前必读**
|
||||
`docs/connect-latency-urltest.html`。铁律:
|
||||
|
||||
- **服务端下发的 sing-box 配置没有 `clash_api`** → 内核不暴露出站组/urltest 历史 → libbox 的
|
||||
Group 命令 / `/proxies` 全空。**每个原生端必须在起内核前给配置注入 `experimental.clash_api`
|
||||
(127.0.0.1 本地监听)+ `cache_file`**,再经本地 HTTP 查 `/proxies`(读 history)+
|
||||
`/group/<name>/delay`(刷新)取真实延迟(与 Windows desktop bridge 同法)。
|
||||
- **libbox CommandClient 一连接只订一种命令**:`addCommand(Status)+addCommand(Group)` 同一 client
|
||||
只第一个生效(Group 永不回调)。速率走 Status,urltest 走上面的 clash HTTP,别指望单 client 的 Group。
|
||||
- **macOS 扩展是 root、容器与无 root 主 app 不同路径** → 主 app 连不上扩展的 command.sock;macOS/iOS
|
||||
统计走 `NETunnelProviderSession.sendProviderMessage`(扩展内 StatsCollector 采集、app 拉取)。Android
|
||||
同进程,直接 socket。
|
||||
- **Dart 侧**:`VpnNativeBridge.statsStream/statusStream` 必须缓存成**共享广播流**(`asBroadcastStream`)——
|
||||
否则多订阅者(连接页速度 + ConnectionController 回写延迟)抢 EventChannel 的单一原生 sink,后订阅者赢、
|
||||
前者变哑(表现:速度正常但延迟一直 —)。`_onStats` 在自动连到已运行隧道(`_connectedNode==null`)时回退
|
||||
`effectiveNode`;连接态 `_measure` 跳过直连实测(全局 TUN 会本地接住、返回假的几 ms)。
|
||||
|
||||
> 设备列表"最后在线"取 `last_seen`(连接/用量/~15s 会话轮询刷新),不是 `last_login`(仅登录那刻)。
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pangolin 连接页延迟(urltest)· 跨端实现 + 排障</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0f1117; --panel:#171a22; --panel2:#1d2129; --fg:#e6e8ee; --fg2:#a8afbd;
|
||||
--accent:#e0884f; --accent2:#5fb0c9; --ok:#5ec27a; --bad:#e06a6a; --warn:#e0b84f;
|
||||
--border:#272c36; --mono:"SF Mono",ui-monospace,Menlo,Consolas,monospace;
|
||||
--sans:-apple-system,"PingFang SC","Helvetica Neue",Arial,sans-serif;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);line-height:1.7;font-size:15px}
|
||||
.wrap{max-width:920px;margin:0 auto;padding:48px 24px 96px}
|
||||
h1{font-size:30px;line-height:1.3;margin:0 0 8px;letter-spacing:-.01em}
|
||||
.sub{color:var(--fg2);font-size:15px;margin:0 0 28px}
|
||||
h2{font-size:21px;margin:44px 0 14px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
||||
h3{font-size:16.5px;margin:24px 0 8px;color:var(--accent)}
|
||||
p{margin:10px 0}
|
||||
code{font-family:var(--mono);font-size:.88em;background:var(--panel2);padding:1px 6px;border-radius:5px;color:#f0d9c4}
|
||||
a{color:var(--accent2);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
ul{margin:8px 0;padding-left:22px}
|
||||
li{margin:5px 0}
|
||||
.lead{background:linear-gradient(180deg,rgba(224,136,79,.10),transparent);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin:0 0 8px}
|
||||
.small{color:var(--fg2);font-size:13px}
|
||||
.note{color:var(--fg2);font-size:13px;border-left:3px solid var(--border);padding-left:12px;margin:10px 0}
|
||||
table{width:100%;border-collapse:collapse;margin:14px 0;font-size:13.5px}
|
||||
th,td{border:1px solid var(--border);padding:8px 10px;text-align:left;vertical-align:top}
|
||||
th{background:var(--panel2);color:var(--fg);font-weight:600}
|
||||
td{color:var(--fg2)}
|
||||
td b,td strong{color:var(--fg)}
|
||||
.bad{color:var(--bad);font-weight:700}
|
||||
.ok{color:var(--ok);font-weight:700}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<h1>连接页延迟(urltest)· 跨端实现 + 排障</h1>
|
||||
<p class="sub">macOS / iOS / iPad / Android / Windows 四端一致——内核 urltest 经 REALITY 真实出站测 RTT</p>
|
||||
|
||||
<div class="lead">
|
||||
<strong>一句话:</strong>连接页"延迟"<b>不是</b> App 直连节点测的(全局 TUN 下直连握手会被本地接住,返回假的几 ms),
|
||||
而是 <b>sing-box 内核的 urltest</b>(经 REALITY 真实出站 GET <code>generate_204</code> 测 RTT)。
|
||||
但<b>服务端下发的配置没有 <code>clash_api</code></b> → 内核不暴露出站组/urltest 历史 → 默认取不到,连接页显示 <code>—</code>。
|
||||
正解:<b>每个原生端在起内核前给配置注入 <code>clash_api</code></b>,再经本地 HTTP 取。这条链四端踩了一整天,逐一记此。
|
||||
</div>
|
||||
|
||||
<h2>根因链(为什么默认全空)</h2>
|
||||
<table>
|
||||
<tr><th>层</th><th>坑</th><th>解</th></tr>
|
||||
<tr><td><b>① 内核不暴露</b></td><td>服务端 <code>BuildClientConfig</code> 渲染的配置无 <code>experimental.clash_api</code> → sing-box 不开出站组/urltest 历史接口</td><td>原生端起内核前注入 <code>clash_api</code>(127.0.0.1 本地监听)+ <code>cache_file</code></td></tr>
|
||||
<tr><td><b>② libbox 取法错</b></td><td>libbox <code>CommandClient</code> <b>一连接只订一种命令</b>:同一 client <code>addCommand(Status)+addCommand(Group)</code> 只第一个生效,Group 永不回调 → urltest 恒空</td><td>速率走 Status;urltest 改走 <b>clash HTTP</b>(<code>/proxies</code> 读 history + <code>/group/<name>/delay</code> 刷新),与 Windows desktop bridge 同法</td></tr>
|
||||
<tr><td><b>③ Dart 抢 sink</b></td><td><code>VpnNativeBridge.statsStream</code> 每次访问都 <code>receiveBroadcastStream()</code>;多订阅者(连接页速度 + ConnectionController 回写延迟)抢 EventChannel 的<b>单一原生 sink</b>,后订阅者赢、前者变哑 → 速度正常但延迟一直 <code>—</code></td><td>缓存成<b>共享广播流</b> <code>asBroadcastStream()</code>,所有订阅者复用一条原生订阅</td></tr>
|
||||
<tr><td><b>④ 直连实测覆盖</b></td><td>连接态下 <code>nodes_provider._measure</code> 直连 TCP 测节点被全局 TUN <b>本地接住</b>,几 ms 返回假值,覆盖 urltest 真实 RTT(显示 3ms)</td><td>连接态跳过直连实测,延迟只认 urltest;<code>_onStats</code> 在自动连到已运行隧道(<code>_connectedNode==null</code>)时回退 <code>effectiveNode</code></td></tr>
|
||||
</table>
|
||||
|
||||
<h2>各端怎么取(架构差异)</h2>
|
||||
<table>
|
||||
<tr><th>端</th><th>注入 clash_api</th><th>采集 + app↔内核通道</th></tr>
|
||||
<tr><td><b>macOS</b></td><td>扩展 <code>PacketTunnelProvider.injectClashApi</code></td><td>扩展是 <b>root</b>,容器与无 root 主 app 不同路径 → 主 app 连不上 command.sock。扩展内 <code>StatsCollector</code> 采集(libbox Status 取速率 + clash HTTP 取 urltest),主 app 经 <code>NETunnelProviderSession.sendProviderMessage("stats")</code> 拉</td></tr>
|
||||
<tr><td><b>iOS/iPad</b></td><td>同 macOS(扩展内注入)</td><td>同 macOS:扩展 <code>StatsCollector</code> + <code>sendProviderMessage</code>。(iOS 扩展/app 同用户、容器其实共享,但为与 macOS 一致仍走此路)</td></tr>
|
||||
<tr><td><b>Android</b></td><td><code>PangolinVpnService.injectClashApi</code></td><td>VpnService 与 libbox <b>同进程</b>:服务内直接连 command.sock 取速率 + <code>pollClash</code> 取 urltest,经 <code>VpnEventBus</code> 推 Flutter</td></tr>
|
||||
<tr><td><b>Windows</b></td><td>desktop bridge 注入(<code>_ensureClashApi</code>)</td><td>sing-box 子进程 + clash HTTP:<code>kernel_process.dart</code> 读 <code>/proxies</code> history + 每 12 拍 <code>/group/<name>/delay</code></td></tr>
|
||||
</table>
|
||||
<p class="note">取法细则(四端一致):显示源 = <code>/proxies</code> 里各 URLTest 组成员的 <code>history.last.delay</code>(稳定、缓存住,单次探测失败不丢);<code>/group/<name>/delay</code> 只为让内核重测、写进 history;跳过 <code>GLOBAL</code> 选择器组。</p>
|
||||
|
||||
<h2>排障打点法</h2>
|
||||
<p>延迟为 <code>—</code> 时,层层往内定位(macOS/iOS 可读主 app NSLog,Android 用 logcat):</p>
|
||||
<ul>
|
||||
<li><code>[pangolin/stats] urltest[0]= diag=...</code> —— app 收到的 urltest 是否空 + clash 原始响应;</li>
|
||||
<li>内核侧:注入 <code>log.level=debug</code> + 抓 <code>urltest/gstatic</code> 行,看内核 <code>outbound reality-out available: NNNms</code> 有没有出(分清"内核没算出"vs"外面没拿到");</li>
|
||||
<li>若 <code>/group/delay</code> 返回 <code>{}</code> 而 <code>dl=0</code>:多半是链路本身太差(探测往返跑不完),非 bug。</li>
|
||||
</ul>
|
||||
|
||||
<h2>libbox 构建前置(否则编译都过不了)</h2>
|
||||
<ul>
|
||||
<li>产物 gitignore,需手动 <code>scripts/build-libbox.sh</code> 生成:<code>apple macos|ios</code> → <code>Libbox.xcframework</code>;<code>android</code> → <code>libbox.aar</code>(改小写放 <code>app/kernel/dist/android/</code>)。</li>
|
||||
<li>⚠️ gomobile 编 <b>Android 强制 JDK 17</b>(JDK 21 直接拒);Android libbox Java 包名是 <code>io.nekohasekai.libbox</code>(非 <code>libbox</code>)。</li>
|
||||
<li>iOS pbxproj 曾有 <b>UUID 冲突</b>(两源文件复用同 ID)→ 文件错配 target、全模块 cannot-find,补齐 framework 后才暴露。</li>
|
||||
</ul>
|
||||
|
||||
<p class="note">相关:<a href="traffic-accounting-scopes.html">流量记账口径</a> · <a href="macos-sysext-realize-troubleshooting.html">macOS sysext realize 排障</a> · 代码见各端 <code>StatsCollector</code> / <code>PangolinVpnService</code> / <code>kernel_process.dart</code>。</p>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -140,6 +140,11 @@
|
||||
</a>
|
||||
|
||||
<h2>排障 / Runbook</h2>
|
||||
<a class="doc" href="connect-latency-urltest.html">
|
||||
<div class="t">连接页延迟(urltest)· 跨端实现 + 排障 <span class="tag html">HTML</span></div>
|
||||
<div class="d">连接页"延迟"= 内核 urltest(非直连实测)。根因链:服务端配置无 clash_api、libbox 单 client Group 不回调、Dart 广播流抢 EventChannel sink、连接态直连实测假值。各端(macOS/iOS/Android/Windows)注入 clash_api + 本地 HTTP 取法 + 排障打点 + libbox 构建前置(Android 要 JDK 17、包名 io.nekohasekai.libbox)。</div>
|
||||
<div class="path">docs/connect-latency-urltest.html</div>
|
||||
</a>
|
||||
<a class="doc" href="ci-runner.md">
|
||||
<div class="t">CI Runner 设置记录 <span class="tag md">MD</span></div>
|
||||
<div class="d">gitea Actions self-hosted runner 记录:决定用 1 个(mac-pangolin-2,项目级·mac host·label nas·复用 jiu relay)、已删 orphan 的清理命令、launchd 持久化 + 单点韧性(runner 离线 Telegram 告警)。CI 首跑 run #298。</div>
|
||||
|
||||
Reference in New Issue
Block a user