Files
pangolin/docs/connect-latency-urltest.html
wangjia e2440e44cc
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
docs: 补 CLAUDE.md 移动端/跨端统计章节 + 新增 urltest 排障文档
- 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>
2026-06-30 22:56:10 +08:00

91 lines
7.5 KiB
HTML

<!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/&lt;name&gt;/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/&lt;name&gt;/delay</code></td></tr>
</table>
<p class="note">取法细则(四端一致):显示源 = <code>/proxies</code> 里各 URLTest 组成员的 <code>history.last.delay</code>(稳定、缓存住,单次探测失败不丢);<code>/group/&lt;name&gt;/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>