Files
pangolin/docs/android-client-design.html
T

171 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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/amd64tag 含 <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. KillSwitch11G</b></td><td><code>setKillSwitch(on)</code><b>L1<code>strict_route</code>+ 引导系统 Always-onL3</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 X200Android 16 / API 36AOSP 系,主力真机)</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>