feat(M4/M5): URLTest 自动选线 + Kill-switch + 弹性重连 [tsk_xAQhC1xuCd8x]

M4 URLTest 自动选线:
- ClashApiClient.getGroupDelay() — GET /group/<name>/delay 触发按需测速
- ClashApiClient.extractUrltestResults() — 从 /proxies 响应解析 URLTest
  组成员最新延迟,填充 stats 帧的 urltestResults 字段
- DesktopKernelProcess._startStatsPoll() — 每秒并行拉取连接统计 + URLTest
  延迟(_clashApi.getProxies()),URLTest 失败不影响主统计
- DesktopVpnBridge.selectOutbound(tag) — Clash API PUT /proxies/proxy,
  tag 可传节点 tag 或 urltest 组 tag(如 "auto-select")恢复自动
- DesktopVpnBridge.getActiveOutbound() — GET /proxies 读 proxy.now
- app/kernel/poc/reality_client.config.json.tmpl — 增加 urltest("auto-select")
  + selector("proxy") 出口组,route.final 改为 "proxy"

M5 Kill-switch + 弹性重连:
- DesktopVpnBridge.setKillSwitch({required bool on}) — 将 _killSwitchEnabled
  写入 TUN inbound strict_route 字段(applyKillSwitchToConfig);内核运行中
  触发静默重载(kill → start(lastConfig))
- DesktopVpnBridge.applyKillSwitchToConfig() — static,killSwitch=true ↔
  TUN strict_route=true,killSwitch=false ↔ strict_route=false
- 自动退避重连:kernel 崩溃推 error → _scheduleReconnect() 退避序列
  1s/2s/4s/8s/16s/30s(上限),stop() 取消,重连成功后重置计数器
- DesktopKernelProcess 意外退出清理 _clashApi(避免下次 spawn 残留)
- app/pangolin/test/killswitch_checklist.md — 三端故障注入验收清单
  (Desktop ,iOS/Android 欠账说明)

新增测试(client/test/bridge/desktop_vpn_bridge_m4m5_test.dart):
- M4: extractUrltestResults 解析/空/无 history 边界
- M4: getGroupDelay HTTP 请求格式验证
- M4: selectOutbound / getActiveOutbound Clash API 调用断言
- M5: applyKillSwitchToConfig 字段覆盖 + 无 TUN 边界
- M5: setKillSwitch 状态注入 + 同值幂等
- M5: 崩溃→error 不崩 / 首次重连在退避延迟内触发 / stop 后不再重连

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-16 00:11:09 +08:00
parent cadd527680
commit 83dd5369ab
5 changed files with 1123 additions and 16 deletions
+20 -3
View File
@@ -1,6 +1,7 @@
{
"_comment": "sing-box 客户端配置模板 — VLESS+REALITY+TUNPoC tsk_SLCsjNgtmng3",
"_comment": "sing-box 客户端配置模板 — VLESS+REALITY+TUNPoC tsk_SLCsjNgtmng3 / M4/M5 tsk_xAQhC1xuCd8x",
"_usage": "渲染脚本: app/kernel/poc/gen-poc-config.sh;占位符一律 __UPPER_SNAKE__",
"_m4_note": "URLTest 自动选线:auto-select(urltest) → 成员节点延迟最低者。手动切换经 Clash API PUT /proxies/proxy",
"log": {
"level": "info",
@@ -13,7 +14,7 @@
"tag": "dns-remote",
"address": "tls://1.1.1.1",
"address_resolver": "dns-local",
"detour": "reality-out"
"detour": "proxy"
},
{
"tag": "dns-local",
@@ -50,6 +51,22 @@
],
"outbounds": [
{
"_comment": "M4: URLTest 自动选优出口组(成员:所有实际代理节点 tag)",
"type": "urltest",
"tag": "auto-select",
"outbounds": ["reality-out"],
"url": "http://www.gstatic.com/generate_204",
"interval": "3m",
"tolerance": 50
},
{
"_comment": "M4: Selector 出口组(Clash API 操作此组来手动覆盖 / 切回自动)",
"type": "selector",
"tag": "proxy",
"outbounds": ["auto-select", "reality-out"],
"default": "auto-select"
},
{
"type": "vless",
"tag": "reality-out",
@@ -93,7 +110,7 @@
{ "geoip": "cn", "outbound": "direct" },
{ "geoip": "private", "outbound": "direct" }
],
"final": "reality-out",
"final": "proxy",
"auto_detect_interface": true
},
+209
View File
@@ -0,0 +1,209 @@
# Kill-switch 故障注入清单 — M5 验收
任务 IDtsk_xAQhC1xuCd8x
执行原则:**每项注入期间持续循环 `curl --max-time 2 ifconfig.me`,断言要么超时要么走节点 IP,绝无真实 IP 泄露。**
---
## 通用准备
```bash
# 注入测试辅助脚本(三端通用,在 shell 里循环执行)
while true; do
result=$(curl --max-time 2 -s ifconfig.me 2>&1)
echo "[$(date +%T)] $result"
sleep 1
done
```
预期:
- 正常连接时:输出节点出口 IP(非本机真实 IP)
- Kill-switch 生效时:`curl: (28) Operation timed out` 或连接拒绝
- **绝不允许输出本机真实 IP**
WebRTC 泄露复验(浏览器内手动执行):
- 访问 https://browserleaks.com/webrtc 观察"Local IP"和"Public IP"字段
- Kill-switch 激活时,Public IP 应为空或节点 IP
DNS 泄露复验:
- 访问 https://dnsleaktest.com → Run Extended Test
- Kill-switch 激活时,所有 DNS 服务器应为节点侧 IP(非国内 ISP DNS)
---
## 平台一:桌面端 macOS(11D 已完成)
实现:sing-box TUN `strict_route: true` + `DesktopVpnBridge.setKillSwitch()`
### 1. 内核进程被 kill -9
```bash
# 找到 sing-box 进程 PID
pgrep -f sing-box
# 强制终止
kill -9 <PID>
```
- [ ] curl 循环输出变为超时(不泄露真实 IP)
- [ ] Flutter UI 显示 `error` 状态(不崩溃)
- [ ] 约 1s 后触发自动重连(状态切换为 `connecting`
- [ ] 退避序列正确:第1次1s,第2次2s,第3次4s…上限30s
- [ ] 重连成功后 UI 恢复 `on` 状态
- [ ] 重连后 curl 再次输出节点 IP
### 2. 物理断网(关闭 Wi-Fi / 拔以太网)
- [ ] curl 循环输出变为超时
- [ ] UI 状态切换为 `error` 或保持 `on`(取决于 sing-box keepalive 行为)
- [ ] 恢复网络后,自动重连成功
- [ ] 恢复后 curl 再次输出节点 IP,无真实 IP 泄露
### 3. Kill-switch 关闭态下断网
```bash
# 先关闭 Kill-switchstrict_route=false
# 再断网
```
- [ ] curl 超时(无网络时正常超时,但不应泄露路由绕过节点的 IP)
- [ ] 注意区分:关闭 Kill-switch = 断网后流量不被隧道保护,可能走直连
- [ ] 关闭 Kill-switch 时,断网后 curl 应超时或走直连(记录观察结果,不强制要求节点 IP)
### 4. Kill-switch ON ↔ OFF 切换验证
```bash
# 连接后调用 setKillSwitch(on: true)
# 查看 config 的 strict_route 字段
```
- [ ] `setKillSwitch(on: true)` 触发内核重载,重载期间 curl 短暂超时
- [ ] 重载完成后 curl 再次输出节点 IP
- [ ] `setKillSwitch(on: false)` 同样触发重载,重载完成后 curl 输出节点 IP
- [ ] `getActiveOutbound()` 在切换前后返回正确 tag
### 5. 节点端主动断连(服务器侧 reset 连接)
```bash
# 在服务端执行(模拟节点故障):
# iptables -I INPUT -p tcp --dport 443 -j REJECT
```
- [ ] sing-box 内核检测到连接断开后推送 `error`
- [ ] 自动重连定时器触发,尝试重连
- [ ] 恢复后(服务端恢复策略),curl 再次输出节点 IP
### 已完成打勾记录
执行日期:___________
执行人:___________
- [x] 测试环境:macOS Ventura 13.xApple Silicon M2
- [ ] 1. kill -9 内核进程
- [ ] 2. 物理断网
- [ ] 3. Kill-switch OFF + 断网
- [ ] 4. Kill-switch ON/OFF 切换
- [ ] 5. 节点端主动断连
- [ ] WebRTC 复验(browserleaks.com
- [ ] DNS 泄露复验(dnsleaktest.com
---
## 平台二:iOS11E M3 已完成;M4/M5 欠账)
**当前状态:`selectOutbound` / `getActiveOutbound` / `setKillSwitch` 仍为 stubTODO 11F),以下清单为下阶段完成后验收。**
实现路径(欠账):
- `AppDelegate.swift` `selectOutbound``sendProviderMessage``PacketTunnelProvider.handleAppMessage`
- `setKillSwitch` → 通过 `NEPacketTunnelNetworkSettings.includedRoutes` / `excludedRoutes` 控制泄露路由
### 1. PacketTunnel Extension 被系统终止
- [ ] iOS 杀后台,或 Xcode Debug > Simulate Memory Warning
- [ ] curl 循环(在终端 SSH 到 iOS 或通过 TestFlight 辅助 App 执行)结果为超时
- [ ] 主 App UI 显示 `error` 状态
- [ ] 自动重连(NETunnelProviderSession 重启)触发
- [ ] 恢复后 curl 输出节点 IP
### 2. Wi-Fi ↔ 蜂窝切换
- [ ] 切换期间 curl 超时(不泄露真实 IP)
- [ ] NEPathMonitor 触发重连
- [ ] 切换完成后 curl 输出节点 IP
### 3. Kill-switch ONincludedRoutes 覆盖所有流量)
- [ ] `setKillSwitch(on: true)` 触发 NetworkExtension 重配置
- [ ] 断网后 curl 超时(不走直连)
- [ ] 恢复后 curl 输出节点 IP
### 4. WebRTC / DNS 泄露复验
- [ ] Safari 访问 browserleaks.com/webrtc — Public IP 为节点 IP 或空
- [ ] dnsleaktest.com — 无国内 ISP DNS
### 已完成打勾记录
执行日期:___________
执行人:___________
**欠账说明:M4/M5 iOS stub 尚未实现(需先完成 11F),本清单留待后续补全。**
---
## 平台三:Android11F M2 已完成;M4/M5 欠账)
**当前状态:`selectOutbound` / `getActiveOutbound` / `setKillSwitch` 仍为 stubTODO 11G),以下清单为下阶段完成后验收。**
实现路径(欠账):
- `MainActivity.kt` `selectOutbound` → libbox CommandClient selector 命令
- `setKillSwitch``VpnService.Builder.addDisallowedApplication``BlockVpnRoute` 路由覆盖
### 1. PangolinVpnService 被 kill -9
```bash
# adb shell
adb shell am kill com.pangolin.pangolin_vpn
```
- [ ] curl 超时(Android 侧通过 adb shell curl 执行)
- [ ] Flutter UI 收到 VpnEventBus `error` 推送,UI 不崩
- [ ] 自动重连触发(PangolinVpnService 重启)
- [ ] 恢复后 curl 输出节点 IP
### 2. Wi-Fi ↔ 蜂窝切换
- [ ] 切换期间 curl 超时
- [ ] ConnectivityManager 触发重连
- [ ] 恢复后输出节点 IP
### 3. Kill-switch ONDISALLOW_APPLICATION 覆盖)
- [ ] `setKillSwitch(on: true)` 触发 VpnService 重配置
- [ ] 断开 Wi-Fi 后 curl 超时(不走直连)
- [ ] 恢复 Wi-Fi 后 curl 输出节点 IP
### 4. 电池优化 / Doze 下后台存活
- [ ] 强制进入 Doze 模式:`adb shell dumpsys deviceidle force-idle`
- [ ] 10 分钟内确认 VPN 服务仍在运行(`adb shell dumpsys vpn`
- [ ] 退出 Doze`adb shell dumpsys deviceidle unforce`
### 5. WebRTC / DNS 泄露复验
- [ ] Chrome 访问 browserleaks.com/webrtc
- [ ] dnsleaktest.com
### 已完成打勾记录
执行日期:___________
执行人:___________
**欠账说明:M4/M5 Android stub 尚未实现(需先完成 11G),本清单留待后续补全。**
---
## 欠账汇总(供后续任务参考)
| 平台 | Kill-switch 实现 | URLTest/selectOutbound | 说明 |
|------|-----------------|----------------------|------|
| 桌面 macOS | ✅ 已实现(strict_route + 重载) | ✅ Clash API | 本任务完成 |
| iOS | ❌ 欠账 | ❌ 欠账 | TODO(11F): sendProviderMessage + includedRoutes |
| Android | ❌ 欠账 | ❌ 欠账 | TODO(11G): libbox CommandClient + VpnService 路由 |