c1fddcdf99
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
171 lines
14 KiB
HTML
171 lines
14 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 Android 客户端设计方案</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 32px}
|
||
h2{font-size:21px;margin:48px 0 14px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
||
h3{font-size:16px;margin:28px 0 8px;color:var(--accent2)}
|
||
p{margin:10px 0}
|
||
code{font-family:var(--mono);font-size:.88em;background:var(--panel2);padding:1px 6px;border-radius:5px;color:#f0d9c4}
|
||
pre{background:#0a0c11;border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow-x:auto;font-family:var(--mono);font-size:13px;line-height:1.55;color:#cdd3df}
|
||
pre .c{color:#6b7385}
|
||
pre .r{color:var(--bad)}
|
||
pre .g{color:var(--ok)}
|
||
pre .y{color:var(--warn)}
|
||
.tag{display:inline-block;font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px;vertical-align:middle}
|
||
.tag.bad{background:rgba(224,106,106,.16);color:var(--bad)}
|
||
.tag.ok{background:rgba(94,194,122,.16);color:var(--ok)}
|
||
.tag.warn{background:rgba(224,184,79,.16);color:var(--warn)}
|
||
.tag.info{background:rgba(95,176,201,.16);color:var(--accent2)}
|
||
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin:16px 0}
|
||
.card.root{border-left:3px solid var(--accent)}
|
||
.card h3{margin-top:0}
|
||
table{width:100%;border-collapse:collapse;margin:16px 0;font-size:14px}
|
||
th,td{text-align:left;padding:9px 12px;border-bottom:1px solid var(--border);vertical-align:top}
|
||
th{color:var(--fg2);font-weight:600;font-size:13px}
|
||
td code{font-size:.85em}
|
||
.ok-c{color:var(--ok)} .bad-c{color:var(--bad)} .warn-c{color:var(--warn)}
|
||
ul,ol{padding-left:22px;margin:10px 0}
|
||
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}
|
||
.kbd{font-family:var(--mono);font-size:.85em;color:var(--accent)}
|
||
.small{color:var(--fg2);font-size:13px}
|
||
hr{border:none;border-top:1px solid var(--border);margin:40px 0}
|
||
a{color:var(--accent2)}
|
||
.back{display:inline-block;margin-bottom:24px;font-size:13px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
|
||
<a class="back" href="index.html">← 文档索引</a>
|
||
|
||
<h1>Pangolin Android 客户端设计方案</h1>
|
||
<p class="sub">目标终点:<b>MVP 端到端连通 + 切节点 + KillSwitch(清掉 TODO 11G)</b> · 分支 <code>feature/android</code> · 2026-06-22</p>
|
||
|
||
<div class="lead">
|
||
<strong>一句话定性:</strong>这<b>不是从零开发</b>,而是把已有的 <b>M2 纸面 PoC 推到真机能连真实节点出网</b>,并补齐切节点 + KillSwitch(清掉 TODO 11G)。Flutter UI 全平台共享、本次零改动。
|
||
</div>
|
||
|
||
<h2>1. 现状盘点</h2>
|
||
<p>之前的任务(commit <code>83c23f9</code> "feat(android): 实现 M2 Android VPN 隧道")已写好相当完整的原生层,但<b>从未真正编译运行过</b>。</p>
|
||
|
||
<div class="card">
|
||
<h3>已具备 <span class="tag ok">现成</span></h3>
|
||
<ul>
|
||
<li>Flutter UI 全平台共享(连接键三态 / 节点 / 统计 / 账户都现成,Android 直接复用)。</li>
|
||
<li>Channel 契约已冻结(<code>pangolin/vpn</code> MethodChannel + <code>pangolin/vpn/status</code>、<code>pangolin/vpn/stats</code> EventChannel),见 <code>client/lib/bridge/vpn_bridge.dart</code>。</li>
|
||
<li><code>MainActivity.kt</code> —— 通道注册 + VPN 授权流程(<code>VpnService.prepare</code>)+ 电池优化豁免引导。</li>
|
||
<li><code>PangolinVpnService.kt</code> —— <code>VpnService</code> + libbox <code>BoxService</code> 集成、<code>openTun</code>、统计(CommandClient + TrafficStats 兜底)、前台通知。</li>
|
||
<li><code>VpnEventBus.kt</code> —— Service↔Activity 状态总线(主线程回调)。</li>
|
||
<li><code>AndroidManifest.xml</code> —— VPN 权限、前台服务 <code>specialUse</code>(适配 Android 14)。</li>
|
||
<li><code>build-android.sh</code> —— gomobile 编译 sing-box → <code>libbox.aar</code>(sing-box <code>v1.13.12</code> / Go <code>1.24.3</code> / gomobile pin <code>v0.0.0-20240604…</code>,tag <code>with_quic,with_utls,with_clash_api,with_gvisor</code>)。</li>
|
||
<li><code>poc_config.json</code> —— REALITY 配置模板。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>缺口(让它真正能跑要补的)<span class="tag bad">待补</span></h3>
|
||
<ol>
|
||
<li><b><code>libbox.aar</code> 没构建过</b> —— <code>app/kernel/dist/android/</code> 为空。需要 Android NDK + gomobile(硬前置)。</li>
|
||
<li><b>原生 libbox API 名是「猜」的</b> —— 代码里多处注释「若编译失败请对照实际 libbox API 微调」(<code>Libbox.newBoxService</code>、<code>TunOptions.inet4Address()</code> 等从没被真实 aar 校验过)。</li>
|
||
<li><b>Dart 侧没接线</b> —— <code>vpn_bridge_provider.dart</code> 目前把 Android 落到 <code>VpnBridgeMock</code>(只处理了 macOS/桌面)。</li>
|
||
<li><b>三个 stub</b> —— <code>selectOutbound</code> / <code>getActiveOutbound</code> / <code>setKillSwitch</code> 标了 TODO 11G。</li>
|
||
<li><b>从没在真机/模拟器上端到端连过。</b></li>
|
||
</ol>
|
||
</div>
|
||
|
||
<h2>2. 整体策略:自底向上「先构建,再对着真实 API 修」</h2>
|
||
<p>最大未知是 <b>libbox.aar 能不能构建出来、原生代码里那些「猜」的 API 名对不对</b>。所以第一步就把这个雷踩掉,而不是最后才发现编译不过。</p>
|
||
<p><b>正确性锚点</b>:官方 <code>SagerNet/sing-box-for-android</code>(SFA)的 Kotlin 源 + 构建出的 aar 里 <code>classes.jar</code> 的真实方法签名。现有 <code>PangolinVpnService.kt</code> 结构是照 SFA 写的,只是 API 名没校验过——拿真 aar 一比对即可定。</p>
|
||
|
||
<h2>3. 里程碑(垂直切片,每片真机可验)</h2>
|
||
<table>
|
||
<thead><tr><th>里程碑</th><th>目标</th><th>验收</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><b>A. 构建内核</b></td><td>跑通 <code>build-android.sh</code> 产出 <code>libbox.aar</code>(arm64/arm/amd64,tag 含 <code>with_clash_api</code>)</td><td>aar 存在、<code>unzip -l</code> 三 ABI 的 <code>.so</code> 齐</td></tr>
|
||
<tr><td><b>B. 编译链接</b></td><td>Dart provider 接 Android→<code>VpnNativeBridge</code>;原生代码对着真 aar 修到 <code>flutter build apk</code> 通过</td><td>APK 构建成功、<code>flutter analyze</code> 零警告</td></tr>
|
||
<tr><td><b>C. 端到端连通</b></td><td>真机一键连 RackNerd 节点,TUN 起、DNS 不劫持失败、能打开被墙站点</td><td>真机实测科学上网成功;UI 三态正确(连接键 off→connecting→on <b>严格由内核回调驱动,禁止乐观显示</b>)</td></tr>
|
||
<tr><td><b>D. 统计走字</b></td><td>stats EventChannel 每秒推上/下行字节 + 速率</td><td>统计页数字跳动,与系统流量大致吻合</td></tr>
|
||
<tr><td><b>E. 切节点(11G)</b></td><td><code>selectOutbound(tag)</code> 经 libbox CommandClient 做 group 选择(复刻桌面 <code>selectProxy</code>);<code>getActiveOutbound</code> 查询当前出口</td><td>节点页切换出口不断连、当前出口高亮正确</td></tr>
|
||
<tr><td><b>F. KillSwitch(11G)</b></td><td><code>setKillSwitch(on)</code> 做 <b>L1(<code>strict_route</code>)+ 引导系统 Always-on(L3)</b></td><td>开关后内核停止即断网/恢复符合预期;UI 诚实标注「彻底防泄漏需到系统设置开启」</td></tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<h2>4. 关键技术决策(与桌面/契约对齐,不自创)</h2>
|
||
<ul>
|
||
<li><b>配置来源不变</b>:Dart 侧 <code>connect_api.fetchConfig</code> 从服务端 <code>POST /v1/nodes/:id/connect</code> 取<b>完整 sing-box config</b>,原样传 <code>bridge.start(configJson)</code>。<b>客户端绝不拼配置。</b></li>
|
||
<li><b>TUN 由 <code>PlatformInterface.openTun</code> 建</b>:libbox 回调里用 <code>VpnService.Builder</code> 配地址/MTU/路由/DNS → <code>establish()</code> 拿 fd。<code>autoDetectInterfaceControl</code> 走 <code>VpnService.protect()</code> 防环路。现有代码已写好,重点是<b>校验 <code>TunOptions</code> 的真实 getter 名</b>(已用 try-catch 兜底)。</li>
|
||
<li><b>DNS 劫持铁律</b>:和 macOS 一样,服务端 config 的 <code>route.rules</code> 首条必须是 <code>{"action":"hijack-dns","port":[53]}</code>——否则隧道连上也打不开网站。这是服务端职责,Android 侧只需确认下发的 config 带这条。</li>
|
||
<li><b>统计</b>:优先 libbox <code>CommandClient</code>(command=STATUS, 1s);连不上退回 <code>TrafficStats</code>(按 UID)。现有代码已实现双路。</li>
|
||
<li><b>切节点 = Clash group 选择</b>:复刻桌面 <code>clashApiClient.selectProxy(group, tag)</code> 的语义,Android 用 libbox <code>CommandClient</code> 的 group 选择 API。组名对着服务端渲染的 config 确认(config 含 <code>auto</code> urltest 组 + <code>reality-out</code>/<code>hy2-out</code> 出口)。</li>
|
||
<li><b>KillSwitch = <code>strict_route</code> + 引导 Always-on</b>:详见 §5 第 1 条与 <a href="killswitch-design.html">KillSwitch 设计知识库</a>。</li>
|
||
</ul>
|
||
|
||
<h2>5. 三处定调(用户已确认)</h2>
|
||
<div class="card root">
|
||
<h3>① KillSwitch(里程碑 F)</h3>
|
||
<p>做「L1 + 引导 L3」的组合——<code>setKillSwitch(on)</code> 落 <code>strict_route</code>(与 Windows 桌面统一)+ 在 UI 诚实标注「彻底防泄漏需到系统设置开 Always-on」,并提供跳转引导。<b>不假装 app 内能做到真 KillSwitch。</b> 背景与分级见 <a href="killswitch-design.html">KillSwitch 设计知识库</a>。</p>
|
||
</div>
|
||
<div class="card root">
|
||
<h3>② 工具链(构建后修正)</h3>
|
||
<p>原定「降 Go 保 pin」——但实测 pin 失效,真实情况:用 <code>golang.org/dl/go1.24.3</code> 当启动器(go.mod 为 <code>go 1.24.7</code>,自动切 1.24.7 编译);gomobile 改用<b>官方 SagerNet fork <code>github.com/sagernet/gomobile@v0.1.12</code></b>(非上游,对照 sing-box go.mod 确认);bind 加 <code>-checklinkname=0</code> + tags <code>badlinkname,tfogo_checklinkname0</code>;JDK 用 Android Studio JBR;国内 <code>GOPROXY=goproxy.cn</code>。Android 栈升 <b>Gradle 8.7 + AGP 8.6 + Kotlin 2.2.0 + 声明式插件</b>。详见实现计划。</p>
|
||
</div>
|
||
<div class="card root">
|
||
<h3>③ 测试基线(里程碑 C)</h3>
|
||
<p><b>先 x86_64 模拟器跑通</b>(环境最干净、最贴官方验证、排除真机变量)→ <b>Vivo X200(Android 16 / API 36,AOSP 系,主力真机)</b> → <b>华为 HarmonyOS 4.2(兼容 Android 版,次要兼容性抽查)</b>。华为后台保活激进、电池优化引导 Intent 需单独适配。</p>
|
||
</div>
|
||
|
||
<h2>6. 已知风险 / 坑</h2>
|
||
<ol>
|
||
<li><b>Go 1.26 vs gomobile 旧 pin</b>:若 <code>gomobile bind</code> 失败,按定调降 Go 1.24.3(保 pin)。</li>
|
||
<li><b>当前没有设备连着</b>(<code>adb devices</code> 为空)——里程碑 C 起需插真机/开模拟器。模拟器为 x86_64,aar 必须含 amd64(脚本已含)。</li>
|
||
<li><b>libbox API 漂移</b>:现有 <code>.kt</code> 方法名是猜的,里程碑 B 逐一对平(TunOptions getter 已 try-catch 兜底)。</li>
|
||
<li><b>Android 14/16 前台服务 specialUse</b>:manifest 已声明;targetSdk 拉到 36 时复测 FGS 启动是否被限。国内分发免审;未来上 Play 需补用途说明。</li>
|
||
<li><b>电池优化 / 厂商保活</b>:现有代码会弹豁免引导;华为/Vivo 后台管理激进,需引导用户允许后台 + 自启动,且厂商设置页深链可能落不准。</li>
|
||
</ol>
|
||
|
||
<h2>7. 不在本次范围(YAGNI)</h2>
|
||
<ul>
|
||
<li>国内分流 <code>.srs</code> 本地预取(现走远程 rule-set)。</li>
|
||
<li>应用图标 / 启动页打磨。</li>
|
||
<li>签名打包 / 应用市场上架 / 侧载分发流程。</li>
|
||
<li>per-app 分应用代理。</li>
|
||
<li>macOS 原生侧的 11G(<code>selectOutbound</code>/<code>getActiveOutbound</code>/<code>setKillSwitch</code>)补齐——平行未完成,见 <a href="killswitch-design.html">KillSwitch 知识库</a> 与 todo #1,不在本次范围。</li>
|
||
</ul>
|
||
|
||
<h2>8. 参考位置索引</h2>
|
||
<table>
|
||
<thead><tr><th>主题</th><th>文件</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>桥接契约</td><td><code>client/lib/bridge/vpn_bridge.dart</code></td></tr>
|
||
<tr><td>平台分派(需加 Android→Native)</td><td><code>client/lib/bridge/vpn_bridge_provider.dart</code></td></tr>
|
||
<tr><td>连接状态机(调 bridge.start)</td><td><code>client/lib/state/connection_provider.dart</code></td></tr>
|
||
<tr><td>取 config</td><td><code>client/lib/services/connect_api.dart</code></td></tr>
|
||
<tr><td>Android 通道注册</td><td><code>client/android/app/src/main/kotlin/com/pangolin/pangolin_vpn/MainActivity.kt</code></td></tr>
|
||
<tr><td>Android VPN 服务</td><td><code>client/android/app/src/main/kotlin/com/pangolin/pangolin_vpn/PangolinVpnService.kt</code></td></tr>
|
||
<tr><td>内核构建脚本</td><td><code>app/kernel/build-android.sh</code></td></tr>
|
||
<tr><td>内核版本锚点</td><td><code>app/kernel/VERSION</code></td></tr>
|
||
<tr><td>桌面参考实现(selectOutbound/killSwitch)</td><td><code>client/lib/bridge/desktop_vpn_bridge.dart</code></td></tr>
|
||
<tr><td>服务端 config 渲染</td><td><code>server/internal/httpapi/clientconfig.go</code></td></tr>
|
||
<tr><td>KillSwitch 设计知识库</td><td><a href="killswitch-design.html">docs/killswitch-design.html</a></td></tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
</div>
|
||
</body>
|
||
</html>
|