Files
pangolin/app/pangolin/test/killswitch_checklist.md
T
wangjia 83dd5369ab 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>
2026-06-16 00:11:09 +08:00

6.9 KiB
Raw Blame History

Kill-switch 故障注入清单 — M5 验收

任务 IDtsk_xAQhC1xuCd8x
执行原则:每项注入期间持续循环 curl --max-time 2 ifconfig.me,断言要么超时要么走节点 IP,绝无真实 IP 泄露。


通用准备

# 注入测试辅助脚本(三端通用,在 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 泄露复验(浏览器内手动执行):

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

# 找到 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 关闭态下断网

# 先关闭 Kill-switchstrict_route=false
# 再断网
  • curl 超时(无网络时正常超时,但不应泄露路由绕过节点的 IP)
  • 注意区分:关闭 Kill-switch = 断网后流量不被隧道保护,可能走直连
  • 关闭 Kill-switch 时,断网后 curl 应超时或走直连(记录观察结果,不强制要求节点 IP)

4. Kill-switch ON ↔ OFF 切换验证

# 连接后调用 setKillSwitch(on: true)
# 查看 config 的 strict_route 字段
  • setKillSwitch(on: true) 触发内核重载,重载期间 curl 短暂超时
  • 重载完成后 curl 再次输出节点 IP
  • setKillSwitch(on: false) 同样触发重载,重载完成后 curl 输出节点 IP
  • getActiveOutbound() 在切换前后返回正确 tag

5. 节点端主动断连(服务器侧 reset 连接)

# 在服务端执行(模拟节点故障):
# iptables -I INPUT -p tcp --dport 443 -j REJECT
  • sing-box 内核检测到连接断开后推送 error
  • 自动重连定时器触发,尝试重连
  • 恢复后(服务端恢复策略),curl 再次输出节点 IP

已完成打勾记录

执行日期:___________
执行人:___________

  • 测试环境: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 仍为 stub(TODO 11F),以下清单为下阶段完成后验收。

实现路径(欠账):

  • AppDelegate.swift selectOutboundsendProviderMessagePacketTunnelProvider.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 仍为 stub(TODO 11G),以下清单为下阶段完成后验收。

实现路径(欠账):

  • MainActivity.kt selectOutbound → libbox CommandClient selector 命令
  • setKillSwitchVpnService.Builder.addDisallowedApplicationBlockVpnRoute 路由覆盖

1. PangolinVpnService 被 kill -9

# 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
  • 退出 Dozeadb 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 路由