Files
pangolin/docs/killswitch-design.html
wangjia 6bcdbbe752 revert(macos): killswitch 回退 L0(低优先级暂缓)
用户判定 killswitch 优先级低、暂不做:
- includeAllNetworks 实测堵死整机网络;退化方案 enforceRoutes+on-demand
  几乎等于没做(核心 fail-closed 没拿到),on-demand 还对免费版配额造成
  重连 churn —— 性价比低
- VpnChannel.swift 恢复到 L0 stub(setKillSwitch→result(nil));
  PacketTunnelProvider.swift 已是原样
- docs §6.5 改为「尝试与暂缓」记录,状态表 macOS 回 L0
- local_test.sh 的 ks-* 漏测子命令保留(将来重拾 killswitch 时可用)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 06:38:47 +08:00

190 lines
17 KiB
HTML
Raw Permalink 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>KillSwitch 设计与跨平台能力矩阵(知识库)</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}
.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}
.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}
.lvl{font-weight:700}
</style>
</head>
<body>
<div class="wrap">
<a class="back" href="index.html">← 文档索引</a>
<h1>KillSwitch 设计与跨平台能力矩阵<span class="small">(知识库)</span></h1>
<p class="sub">Pangolin 客户端 · 断网保护设计依据与现状 · 2026-06-222026-07-01 更新:macOS killswitch 尝试后<b>暂缓、回退 L0</b>,详见 §6.5</p>
<div class="lead">
KillSwitch 不是「一个开关」,而是<b>分层能力</b>——理想态、平台天花板、当前实现是三件不同的事。本文沉淀其设计依据与现状。
</div>
<h2>1. 本质:fail-closed</h2>
<p>KillSwitch 的本质是 <b>fail-closed</b></p>
<div class="card"><b>隧道不处于活动状态时,禁止任何流量走明文/默认路径出去。</b></div>
<p>目标是堵住 VPN 掉线瞬间的 <b>IP / DNS 泄漏</b>。没有 KillSwitch 时,隧道一掉,流量静默回落到运营商默认路由,用户真实 IP 与 DNS 查询直接暴露——对科学上网场景是致命泄漏。</p>
<h2>2. 理想设计的 5 条属性</h2>
<ol>
<li><b>默认关闭(fail-closed</b>:隧道一旦不可用,流量是「被丢弃」而非「放行」。</li>
<li><b>扛得住进程死亡</b> ← 最难。app/内核进程崩溃或被系统杀掉后,封锁<b>依然生效</b>。这要求封锁由 <b>OS 内核/框架</b>强制,而不是靠 app 进程内的路由表。</li>
<li><b>覆盖所有「缺口窗口」</b>:开机后还没连上的窗口、掉线重连的窗口、进程崩溃的窗口——三个都要堵。</li>
<li><b>DNS 防泄漏</b>:DNS 查询也强制走隧道或被一并阻断。</li>
<li><b>可控范围 + 诚实 UI</b>:可选放行 LAN(局域网打印机等)、可选分应用;并<b>如实</b>告诉用户当前到底保护到了哪一层,不夸大。</li>
</ol>
<h2>3. 能力分级(统一标尺)</h2>
<table>
<thead><tr><th>Level</th><th>机制</th><th>能堵什么</th><th>堵不住什么</th></tr></thead>
<tbody>
<tr><td><span class="lvl bad-c">L0 无</span></td><td>掉线即走明文</td><td></td><td>全泄漏</td></tr>
<tr><td><span class="lvl warn-c">L1 内核路由绑定(软)</span></td><td>路由绑死隧道接口(如 sing-box <code>strict_route</code></td><td>进程活着、隧道未建/重连时不泄漏</td><td><b>进程一死、接口被拆 → 路由恢复 → 泄漏</b></td></tr>
<tr><td><span class="lvl ok-c">L2 OS 防火墙强制(硬)</span></td><td>OS 级包过滤规则(NE 框架 / WFP / nftables)阻断非隧道流量</td><td><b>进程死了也照堵</b>(规则在 OS 内核,不在 app</td><td>开机到规则生效前的窗口</td></tr>
<tr><td><span class="lvl ok-c">L3 常开 + 开机持久</span></td><td>L2 + OS 从开机起自动拉起 VPN 并强制阻断</td><td>开机窗口也堵,全程 fail-closed</td><td>(基本无死角)</td></tr>
</tbody>
</table>
<p><b>理想态 = L3。</b></p>
<div class="card root">
<b>关键认知</b>L1 与 L2 之间有一条<b>质变线</b>——L1 是「app 进程内的约束」,L2/L3 是「OS 框架的约束」。<b>只有跨过这条线才算「真 KillSwitch」</b>。目前 Pangolin 各平台用的 <code>strict_route</code> 都还停在 L1。
</div>
<h2>4. 各平台<b>能力天花板</b></h2>
<table>
<thead><tr><th>平台</th><th>机制</th><th>天花板</th><th>说明</th></tr></thead>
<tbody>
<tr><td><b>macOS</b></td><td>NetworkExtension System Extension</td><td><span class="ok-c">L3</span></td><td><code>includeAllNetworks=true</code> 全量入隧道 + <code>NEOnDemandRule</code> 常开 + <code>enforceRoutes</code>。NE 守护进程(neagent)在系统级强制,<b>扛 app 崩溃</b>。Apple 平台原生支持最完整。</td></tr>
<tr><td><b>iOS</b></td><td>NetworkExtension</td><td><span class="ok-c">L3</span></td><td>同 NE 原语,甚至更干净;on-demand「按需常开」。⚠️ 目前<b>无 iOS 客户端</b></td></tr>
<tr><td><b>Android</b></td><td>VpnService</td><td><span class="ok-c">L3</span><span class="small">,但有条件</span></td><td>app 内只能到 <b>L1</b><code>strict_route</code>);真正的 L2/L3 = 系统设置「<b>始终开启 VPN + 无 VPN 时阻止连接</b>」,OS 级强制、扛崩溃、开机生效。<b>但 app 不能编程开启</b>,只能深链引导用户手动开(或 Device Owner/MDM 下发)。</td></tr>
<tr><td><b>Windows</b></td><td>子进程 sing-box + wintun</td><td><span class="warn-c">L2(需开发)</span></td><td>现仅 <code>strict_route</code>(L1)。要到 L2 须让 app 装 <b>WFPWindows Filtering Platform)过滤器</b>阻断非隧道流量;做成系统服务持久化才能扛崩溃。</td></tr>
<tr><td><b>Linux</b><span class="small">(非主目标)</span></td><td>子进程 + tun</td><td><span class="warn-c">L2(需开发)</span></td><td>L1 现成;L2 靠 nftables/iptables killswitch 链。</td></tr>
</tbody>
</table>
<p><b>一句话</b>:Apple 两端原生能直达 L3;Android 能到 L3 但要用户手动配合;Windows/Linux 要自己写 OS 防火墙规则才能上 L2。</p>
<h2>5. 各平台<b>当前实际实现</b>(代码事实)</h2>
<table>
<thead><tr><th>平台</th><th>当前 Level</th><th>真实状态</th><th>代码位置</th></tr></thead>
<tbody>
<tr><td><b>Windows</b></td><td><span class="warn-c">L1</span></td><td>✅ 改 <code>strict_route</code> + 子进程重载 + 退避重连。进程被硬杀仍泄漏。</td><td><code>desktop_vpn_bridge.dart:305</code><code>applyKillSwitchToConfig</code>)、<code>:178</code></td></tr>
<tr><td><b>Linux</b></td><td><span class="warn-c">L1</span></td><td>同 Windows(共用 <code>DesktopVpnBridge</code>)。</td><td>同上</td></tr>
<tr><td><b>macOS</b></td><td><span class="bad-c">L0</span><span class="small">(暂缓)</span></td><td>❌ stub<code>setKillSwitch</code> 直接 <code>result(nil)</code>。曾尝试 enforceRoutes+on-demand,因性价比低 + 免费版配额重连 churn + includeAllNetworks 堵死整机网络,<b>暂缓并回退 L0</b>(#1 低优先级)。踩坑记录见 §6.5。</td><td><code>VpnChannel.swift:89</code></td></tr>
<tr><td><b>Android</b></td><td><span class="bad-c">L0</span></td><td>❌ stub<code>setKillSwitch</code> 只打日志(TODO 11G)。</td><td><code>MainActivity.kt</code><code>setKillSwitch</code> 分支)</td></tr>
<tr><td><b>iOS</b></td><td></td><td>无客户端。</td><td></td></tr>
</tbody>
</table>
<p class="small">注:macOS 自 <code>kUseNativeVpnMacOS=true</code><code>vpn_bridge_provider.dart:19</code>)起走原生 System Extension,不再走 <code>DesktopVpnBridge</code>。所以 macOS 的 <code>strict_route</code>Windows 路线)对它<b>不生效</b>,必须走 NE 原生路线。</p>
<h2>6. Pangolin 现实判断与决策</h2>
<ul>
<li><b>能力与实现严重不匹配</b>macOS 明明能 L3,却仍是 L0(最差);Windows 反而是唯一做了的(L1)。<span class="tag warn">暂缓</span> 2026-07 尝试落地,因 includeAllNetworks 堵死整机网络 + 退化方案(enforceRoutes+on-demand)几乎等于没做 + 免费版配额重连 churn,判定<b>性价比低,回退 L0、低优先级搁置</b>(见 §6.5)。</li>
<li><b>Android 客户端方案的 KillSwitch 定位</b>MVP+ 阶段):app 内做到 <b>L1<code>strict_route</code>,与 Windows 一致)</b>,再<b>引导用户开系统 Always-on 拿到 L3</b>,UI 上诚实标注「彻底防泄漏需在系统设置开启」。这是「app 能力上限 + OS 兜底」的合理组合,不夸大。</li>
<li><b>跨端一致性缺口</b>(按性价比排序):
<ol>
<li><b>macOS 原生 11G</b><code>includeAllNetworks</code> + on-demand)—— 天花板 L3、改动集中在 Swift,<b>性价比最高</b></li>
<li><b>Windows L1→L2</b>(WFP 防火墙强制)—— 工作量较大(需原生过滤器 + 持久化服务)。</li>
<li><b>Android</b> —— 受 OS 限制,编程上限就是 L1,剩下靠引导,无更高可做空间。</li>
</ol>
</li>
</ul>
<h2>6.5 macOS KillSwitch 尝试与暂缓<span class="small">#1,低优先级搁置)</span></h2>
<div class="card root"><b>结论(2026-07-01</b>:尝试落地后<b>已回退到 L0stub</b>。原因:① <code>includeAllNetworks</code>(唯一能给「扛进程被杀」L2 的原语)实测堵死整机网络见下;② 退化方案 enforceRoutes+on-demand 几乎等于没做(核心 fail-closed 没拿到);③ on-demand 常开对免费版 10 分钟配额造成重连 churn。综合判定<b>性价比低,低优先级暂缓</b>。下文保留踩坑记录与方案,便于将来重拾。</div>
<p>macOS 走原生 NE System Extension<code>kUseNativeVpnMacOS=true</code>),<code>strict_route</code> 那套对它不生效,必须用 NE 原语。</p>
<div class="card root">
<b>踩坑修正(重要)</b>:最初按设计用 <code>includeAllNetworks=true</code> 追 L2<b>实测把整机网络堵死并陷入连不上的死循环</b>——它会把<b>所有</b>流量(含 sing-box 连服务器的握手、app 调控制面 API 的请求)在隧道建起来<b>之前</b>就塞进隧道 → 握手出不去 → 连接失败 → on-demand 立刻重连 → 无限打转(连 app 的账户信息都拉不到,「我的」页全显示 —)。<b>故撤掉 <code>includeAllNetworks</code></b>,改用下面不会误伤握手/控制面的组合。要拿回「扛进程被杀」那一档,须先在扩展内显式放行服务器/控制面连接,留待真机验证后再评估。</div>
<table>
<thead><tr><th>当前达成</th><th>NE 机制</th><th>效果 / 边界</th></tr></thead>
<tbody>
<tr><td><span class="lvl warn-c">连接期防泄漏</span></td><td><code>enforceRoutes=true</code>(隧道 <code>includedRoutes</code><code>0.0.0.0/0</code></td><td>连接期间所有流量强制走隧道、不从旁路接口泄漏;<b></b>覆盖进程被杀后的窗口</td></tr>
<tr><td><span class="lvl warn-c">掉线缺口收窄</span></td><td><code>onDemandRules=[NEOnDemandRuleConnect()]</code> + <code>isOnDemandEnabled=true</code></td><td>掉线/开机窗口 OS 自动拉起隧道,缩小重连缺口(非零)</td></tr>
<tr><td class="small" colspan="3">放行 LAN<code>excludeLocalNetworks=true</code>(打印机/AirPlay 等)。<b>不设</b> <code>includeAllNetworks</code>(见上方踩坑修正)。</td></tr>
</tbody>
</table>
<p class="small">即「连接期 enforceRoutes 防漏 + on-demand 兜重连」,介于 L1 与 L2 之间,<b>尚未到</b>设计追求的 includeAllNetworks-L3。诚实标注,不夸大。</p>
<h3>曾经的改动方案<span class="small">(已回退,留作将来参考)</span></h3>
<div class="card">
<b><code>client/macos/Runner/VpnChannel.swift</code></b>
<ol>
<li>新增缓存字段 <code>killSwitchEnabled</code>(默认 <code>true</code>,与 Dart <code>AppSettings.killSwitch</code> 默认对齐)。</li>
<li><code>setKillSwitch</code> 落地:存标志;若 <code>manager</code> 已存在 → 重新 <code>applyKillSwitchConfig</code> + <code>saveToPreferences</code> 即时生效。</li>
<li><code>configureKillSwitch</code> 按标志设:<code>enforceRoutes</code> / <code>excludeLocalNetworks=true</code> / <code>onDemandRules</code> + <code>isOnDemandEnabled</code><code>includeAllNetworks</code><code>false</code></li>
<li><b>关键 gotcha — <code>stop()</code></b>on-demand 常开时直接 <code>stopVPNTunnel()</code> 会被 OS 立刻拉回。手动断开须<b></b> <code>isOnDemandEnabled=false</code> + <code>saveToPreferences</code> <b></b> stop;否则「断开」按钮失效。</li>
</ol>
</div>
<div class="card">
<b><code>client/macos/PacketTunnel/PacketTunnelProvider.swift</code></b>libbox 回调 <code>includeAllNetworks()</code> 保持 <code>false</code>(与 NE 层一致)。
</div>
<p class="small">无需新增 entitlement<code>packet-tunnel-provider</code> 已有);<code>enforceRoutes</code>/<code>excludeLocalNetworks</code> 为 macOS 11+ API,已 <code>#available</code> 守卫,Runner 部署目标 10.15 满足。</p>
<h3>验证(NE 难单测,靠真机手测)</h3>
<ol>
<li><code>flutter build macos</code> 通过、sysext 激活。</li>
<li>killswitch 开 + 连接 → <b>能正常连上并上网</b>(不再死循环 / 整机断网)、「我的」页账户信息正常加载。</li>
<li>连接期:旁路接口(如同时插网线+WiFi)不泄漏。</li>
<li>on-demand:掉线后隧道自动拉起;手动「断开」能真断、不反弹(验 <code>stop()</code> 的 on-demand 处理)。</li>
<li>killswitch 关 → on-demand 取消、回普通连接。</li>
<li><span class="small">脚本:<code>scripts/local_test.sh ks-baseline|ks-test|ks-status</code> 做 fail-closed 漏测。</span></li>
</ol>
<h2>7. 关联待办</h2>
<p>KillSwitch 相关待办见项目 <code>todo/</code><code>/todo list</code>),主要三条:</p>
<ul>
<li><span class="tag info">#1 mac</span> macOS 原生 KillSwitch 补齐 L0→L3<code>includeAllNetworks</code> + on-demand</li>
<li><span class="tag info">#2 Android</span> Android KillSwitch L1<code>strict_route</code>+ 引导系统 Always-on(属 Android 客户端 11G</li>
<li><span class="tag info">#3 Windows</span> Windows KillSwitch L1→L2WFP 防火墙强制,backlog</li>
</ul>
<h2>8. 参考实现位置索引</h2>
<table>
<thead><tr><th>主题</th><th>文件</th></tr></thead>
<tbody>
<tr><td>桥接契约(<code>setKillSwitch</code> 方法签名)</td><td><code>client/lib/bridge/vpn_bridge.dart</code></td></tr>
<tr><td>平台分派(哪个平台走哪个 bridge)</td><td><code>client/lib/bridge/vpn_bridge_provider.dart</code></td></tr>
<tr><td>Windows/Linux 实现(strict_route</td><td><code>client/lib/bridge/desktop_vpn_bridge.dart</code></td></tr>
<tr><td>macOS 原生通道(stub</td><td><code>client/macos/Runner/VpnChannel.swift</code></td></tr>
<tr><td>macOS 隧道 Provider</td><td><code>client/macos/PacketTunnel/PacketTunnelProvider.swift</code></td></tr>
<tr><td>Android 通道(stub</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>
</tbody>
</table>
</div>
</body>
</html>