6bcdbbe752
用户判定 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>
190 lines
17 KiB
HTML
190 lines
17 KiB
HTML
<!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-22(2026-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>WFP(Windows 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>已回退到 L0(stub)</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→L2(WFP 防火墙强制,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>
|