Files
pangolin/docs/killswitch-design.html
T
wangjia a32d87c9c0 fix(macos): killswitch 撤掉 includeAllNetworks(堵死整机网络) — 改 enforceRoutes+on-demand
实测 includeAllNetworks=true 把所有流量(含 sing-box 连服务器握手、app 调控制面
请求)在隧道建起前就塞进隧道 → 握手出不去 → 连接失败 + on-demand 死循环 + 整机
断网(连「我的」页账户信息都拉不到、显示 —)。

改为不会误伤握手/控制面的组合:
- configureKillSwitch:includeAllNetworks 恒 false;保留 enforceRoutes(连接期防漏)
  + excludeLocalNetworks + NEOnDemandRule 常开(兜重连)
- 撤回 start() 的 killSwitch option 及扩展侧 includeAllNetworks 透传(回 false)
- 诚实标注:当前 L1→L2 之间,未到 includeAllNetworks-L3;要拿「扛进程被杀」须先
  在扩展内放行服务器/控制面连接,留待真机验证

docs/killswitch-design.html: §6.5 加踩坑修正、状态表 macOS 改「L1→L2 之间」

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

189 lines
16 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>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 落地 enforceRoutes+on-demandincludeAllNetworks 实测堵死整机网络已撤回)</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="warn-c">L1→L2 之间</span></td><td>✅ 本轮 #1<code>enforceRoutes</code> 连接期防漏 + <code>NEOnDemandRule</code> 常开兜重连;随 killswitch 开关即时切换,手动断开先关 on-demand 防反弹。⚠️ <code>includeAllNetworks</code> 实测堵死整机网络已撤回,故<b>暂未到</b> includeAllNetworks-L3。详见 §6.5。</td><td><code>VpnChannel.swift</code><code>configureKillSwitch</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> macOS 已落地 enforceRoutes + on-demandL1→L2 之间);includeAllNetworks-L3 因实测堵死整机网络暂缓,待真机调通服务器绕行再上(见 §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>
<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>改动(2 个原生文件,不动 Dart/服务端)</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>