680f1af205
macOS 走原生 NE,strict_route 不生效。补齐断网保护到天花板 L3: VpnChannel.swift - 缓存 killSwitchEnabled(默认 true,与 Dart AppSettings.killSwitch 对齐) - setKillSwitch 落地:存标志,manager 已装配则即时重写 NE 配置+保存 - configureKillSwitch:includeAllNetworks+enforceRoutes(L2 OS 强制、扛崩溃) + NEOnDemandRule 常开(L3)+ excludeLocalNetworks 放行 LAN (enforceRoutes/excludeLocalNetworks 为 macOS 11+ API,#available 守卫) - stop() gotcha:on-demand 常开时先关 isOnDemandEnabled+save 再 stop, 否则手动断开被 OS 立刻拉回 - start() options 带 killSwitch 传扩展 PacketTunnelProvider.swift - startTunnel 读 killSwitch 选项(OS on-demand 自启时 nil→默认 true) - libbox 回调 includeAllNetworks() 返回该值,与 NE 层对齐 docs/killswitch-design.html: 新增 §6.5 实现方案,状态表 macOS L0→L3 验证:xcodebuild CODE_SIGNING_ALLOWED=NO 两 target SwiftCompile 通过; 端到端 fail-closed/on-demand 需真机手测(NE 难单测)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
188 lines
15 KiB
HTML
188 lines
15 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-06-30 更新:macOS L0→L3 已实现)</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="ok-c">L3</span></td><td>✅ NE fail-closed(本轮 #1):<code>includeAllNetworks</code>+<code>enforceRoutes</code>(L2 OS 强制、扛崩溃)+ <code>NEOnDemandRule</code> 常开(L3)。随 killswitch 开关即时切换;手动断开先关 on-demand 防反弹。详见 §6.5。</td><td><code>VpnChannel.swift</code>(<code>configureKillSwitch</code>)、<code>PacketTunnelProvider.swift</code>(<code>includeAllNetworks()</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 ok">已修复</span> macOS 已补齐到 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 L0→L3 实现方案<span class="small">(本轮 #1)</span></h2>
|
||
<p>macOS 走原生 NE System Extension(<code>kUseNativeVpnMacOS=true</code>),<code>strict_route</code> 那套对它不生效,必须用 NE 原语。L0→L3 的达成路径:</p>
|
||
<table>
|
||
<thead><tr><th>目标 Level</th><th>NE 机制</th><th>效果</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><span class="lvl ok-c">L2 OS 强制</span></td><td><code>protocolConfiguration.includeAllNetworks=true</code> + <code>enforceRoutes=true</code></td><td>全量流量入隧道、由 <code>neagent</code> 系统级强制,<b>扛 app/内核崩溃</b>(规则在 OS 不在 app 进程)</td></tr>
|
||
<tr><td><span class="lvl ok-c">L3 常开+持久</span></td><td>L2 + <code>onDemandRules=[NEOnDemandRuleConnect()]</code> + <code>isOnDemandEnabled=true</code></td><td>掉线/开机窗口也堵,OS 自动拉起,全程 fail-closed</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<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>loadOrCreateManager</code> 按标志设:<code>includeAllNetworks</code> / <code>enforceRoutes=true</code> / <code>excludeLocalNetworks=true</code>(放行 LAN 打印机等)/ <code>onDemandRules</code> + <code>isOnDemandEnabled</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>
|
||
<li><code>start()</code> 的 <code>startVPNTunnel(options:)</code> 附带 <code>killSwitch</code> 标志传给扩展。</li>
|
||
</ol>
|
||
</div>
|
||
<div class="card">
|
||
<b><code>client/macos/PacketTunnel/PacketTunnelProvider.swift</code></b>
|
||
<ol>
|
||
<li><code>startTunnel(options:)</code> 读取 <code>killSwitch</code> 选项并缓存。</li>
|
||
<li>libbox 回调 <code>includeAllNetworks()</code> 返回该缓存值(与 NE 层对齐,让 sing-box 不装绕行路由)。</li>
|
||
</ol>
|
||
</div>
|
||
<p class="small">无需新增 entitlement(<code>packet-tunnel-provider</code> 已有);<code>includeAllNetworks</code>/<code>enforceRoutes</code>/<code>excludeLocalNetworks</code>/on-demand 均 macOS 10.15/11+ 可用,Runner 部署目标满足。</p>
|
||
<h3>验证(NE 难单测,靠真机手测)</h3>
|
||
<ol>
|
||
<li><code>flutter build macos</code> 通过、sysext 激活。</li>
|
||
<li>killswitch 开 + 连接 → 正常上网;<b>杀扩展进程或拔隧道</b> → 流量被阻断(无 IP/DNS 泄漏)= 跨过 L1→L2 质变线。</li>
|
||
<li>on-demand:开机/掉线窗口隧道自动拉起;手动「断开」能真断、不反弹(验 <code>stop()</code> 的 on-demand 处理)。</li>
|
||
<li>killswitch 关 → on-demand 取消、回普通连接。</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>
|