← 文档索引

KillSwitch 设计与跨平台能力矩阵(知识库)

Pangolin 客户端 · 断网保护设计依据与现状 · 2026-06-22(2026-07-01 更新:macOS killswitch 尝试后暂缓、回退 L0,详见 §6.5)

KillSwitch 不是「一个开关」,而是分层能力——理想态、平台天花板、当前实现是三件不同的事。本文沉淀其设计依据与现状。

1. 本质:fail-closed

KillSwitch 的本质是 fail-closed

隧道不处于活动状态时,禁止任何流量走明文/默认路径出去。

目标是堵住 VPN 掉线瞬间的 IP / DNS 泄漏。没有 KillSwitch 时,隧道一掉,流量静默回落到运营商默认路由,用户真实 IP 与 DNS 查询直接暴露——对科学上网场景是致命泄漏。

2. 理想设计的 5 条属性

  1. 默认关闭(fail-closed):隧道一旦不可用,流量是「被丢弃」而非「放行」。
  2. 扛得住进程死亡 ← 最难。app/内核进程崩溃或被系统杀掉后,封锁依然生效。这要求封锁由 OS 内核/框架强制,而不是靠 app 进程内的路由表。
  3. 覆盖所有「缺口窗口」:开机后还没连上的窗口、掉线重连的窗口、进程崩溃的窗口——三个都要堵。
  4. DNS 防泄漏:DNS 查询也强制走隧道或被一并阻断。
  5. 可控范围 + 诚实 UI:可选放行 LAN(局域网打印机等)、可选分应用;并如实告诉用户当前到底保护到了哪一层,不夸大。

3. 能力分级(统一标尺)

Level机制能堵什么堵不住什么
L0 无掉线即走明文全泄漏
L1 内核路由绑定(软)路由绑死隧道接口(如 sing-box strict_route进程活着、隧道未建/重连时不泄漏进程一死、接口被拆 → 路由恢复 → 泄漏
L2 OS 防火墙强制(硬)OS 级包过滤规则(NE 框架 / WFP / nftables)阻断非隧道流量进程死了也照堵(规则在 OS 内核,不在 app)开机到规则生效前的窗口
L3 常开 + 开机持久L2 + OS 从开机起自动拉起 VPN 并强制阻断开机窗口也堵,全程 fail-closed(基本无死角)

理想态 = L3。

关键认知:L1 与 L2 之间有一条质变线——L1 是「app 进程内的约束」,L2/L3 是「OS 框架的约束」。只有跨过这条线才算「真 KillSwitch」。目前 Pangolin 各平台用的 strict_route 都还停在 L1。

4. 各平台能力天花板

平台机制天花板说明
macOSNetworkExtension System ExtensionL3includeAllNetworks=true 全量入隧道 + NEOnDemandRule 常开 + enforceRoutes。NE 守护进程(neagent)在系统级强制,扛 app 崩溃。Apple 平台原生支持最完整。
iOSNetworkExtensionL3同 NE 原语,甚至更干净;on-demand「按需常开」。⚠️ 目前无 iOS 客户端
AndroidVpnServiceL3,但有条件app 内只能到 L1strict_route);真正的 L2/L3 = 系统设置「始终开启 VPN + 无 VPN 时阻止连接」,OS 级强制、扛崩溃、开机生效。但 app 不能编程开启,只能深链引导用户手动开(或 Device Owner/MDM 下发)。
Windows子进程 sing-box + wintunL2(需开发)现仅 strict_route(L1)。要到 L2 须让 app 装 WFP(Windows Filtering Platform)过滤器阻断非隧道流量;做成系统服务持久化才能扛崩溃。
Linux(非主目标)子进程 + tunL2(需开发)L1 现成;L2 靠 nftables/iptables killswitch 链。

一句话:Apple 两端原生能直达 L3;Android 能到 L3 但要用户手动配合;Windows/Linux 要自己写 OS 防火墙规则才能上 L2。

5. 各平台当前实际实现(代码事实)

平台当前 Level真实状态代码位置
WindowsL1✅ 改 strict_route + 子进程重载 + 退避重连。进程被硬杀仍泄漏。desktop_vpn_bridge.dart:305applyKillSwitchToConfig)、:178
LinuxL1同 Windows(共用 DesktopVpnBridge)。同上
macOSL0(暂缓)❌ stub:setKillSwitch 直接 result(nil)。曾尝试 enforceRoutes+on-demand,因性价比低 + 免费版配额重连 churn + includeAllNetworks 堵死整机网络,暂缓并回退 L0(#1 低优先级)。踩坑记录见 §6.5。VpnChannel.swift:89
AndroidL0❌ stub:setKillSwitch 只打日志(TODO 11G)。MainActivity.ktsetKillSwitch 分支)
iOS无客户端。

注:macOS 自 kUseNativeVpnMacOS=truevpn_bridge_provider.dart:19)起走原生 System Extension,不再走 DesktopVpnBridge。所以 macOS 的 strict_route(Windows 路线)对它不生效,必须走 NE 原生路线。

6. Pangolin 现实判断与决策

6.5 macOS KillSwitch 尝试与暂缓(#1,低优先级搁置)

结论(2026-07-01):尝试落地后已回退到 L0(stub)。原因:① includeAllNetworks(唯一能给「扛进程被杀」L2 的原语)实测堵死整机网络见下;② 退化方案 enforceRoutes+on-demand 几乎等于没做(核心 fail-closed 没拿到);③ on-demand 常开对免费版 10 分钟配额造成重连 churn。综合判定性价比低,低优先级暂缓。下文保留踩坑记录与方案,便于将来重拾。

macOS 走原生 NE System Extension(kUseNativeVpnMacOS=true),strict_route 那套对它不生效,必须用 NE 原语。

踩坑修正(重要):最初按设计用 includeAllNetworks=true 追 L2,实测把整机网络堵死并陷入连不上的死循环——它会把所有流量(含 sing-box 连服务器的握手、app 调控制面 API 的请求)在隧道建起来之前就塞进隧道 → 握手出不去 → 连接失败 → on-demand 立刻重连 → 无限打转(连 app 的账户信息都拉不到,「我的」页全显示 —)。故撤掉 includeAllNetworks,改用下面不会误伤握手/控制面的组合。要拿回「扛进程被杀」那一档,须先在扩展内显式放行服务器/控制面连接,留待真机验证后再评估。
当前达成NE 机制效果 / 边界
连接期防泄漏enforceRoutes=true(隧道 includedRoutes0.0.0.0/0连接期间所有流量强制走隧道、不从旁路接口泄漏;覆盖进程被杀后的窗口
掉线缺口收窄onDemandRules=[NEOnDemandRuleConnect()] + isOnDemandEnabled=true掉线/开机窗口 OS 自动拉起隧道,缩小重连缺口(非零)
放行 LAN:excludeLocalNetworks=true(打印机/AirPlay 等)。不设 includeAllNetworks(见上方踩坑修正)。

即「连接期 enforceRoutes 防漏 + on-demand 兜重连」,介于 L1 与 L2 之间,尚未到设计追求的 includeAllNetworks-L3。诚实标注,不夸大。

曾经的改动方案(已回退,留作将来参考)

client/macos/Runner/VpnChannel.swift
  1. 新增缓存字段 killSwitchEnabled(默认 true,与 Dart AppSettings.killSwitch 默认对齐)。
  2. setKillSwitch 落地:存标志;若 manager 已存在 → 重新 applyKillSwitchConfig + saveToPreferences 即时生效。
  3. configureKillSwitch 按标志设:enforceRoutes / excludeLocalNetworks=true / onDemandRules + isOnDemandEnabledincludeAllNetworksfalse
  4. 关键 gotcha — stop():on-demand 常开时直接 stopVPNTunnel() 会被 OS 立刻拉回。手动断开须 isOnDemandEnabled=false + saveToPreferences stop;否则「断开」按钮失效。
client/macos/PacketTunnel/PacketTunnelProvider.swift:libbox 回调 includeAllNetworks() 保持 false(与 NE 层一致)。

无需新增 entitlement(packet-tunnel-provider 已有);enforceRoutes/excludeLocalNetworks 为 macOS 11+ API,已 #available 守卫,Runner 部署目标 10.15 满足。

验证(NE 难单测,靠真机手测)

  1. flutter build macos 通过、sysext 激活。
  2. killswitch 开 + 连接 → 能正常连上并上网(不再死循环 / 整机断网)、「我的」页账户信息正常加载。
  3. 连接期:旁路接口(如同时插网线+WiFi)不泄漏。
  4. on-demand:掉线后隧道自动拉起;手动「断开」能真断、不反弹(验 stop() 的 on-demand 处理)。
  5. killswitch 关 → on-demand 取消、回普通连接。
  6. 脚本:scripts/local_test.sh ks-baseline|ks-test|ks-status 做 fail-closed 漏测。

7. 关联待办

KillSwitch 相关待办见项目 todo//todo list),主要三条:

8. 参考实现位置索引

主题文件
桥接契约(setKillSwitch 方法签名)client/lib/bridge/vpn_bridge.dart
平台分派(哪个平台走哪个 bridge)client/lib/bridge/vpn_bridge_provider.dart
Windows/Linux 实现(strict_route)client/lib/bridge/desktop_vpn_bridge.dart
macOS 原生通道(stub)client/macos/Runner/VpnChannel.swift
macOS 隧道 Providerclient/macos/PacketTunnel/PacketTunnelProvider.swift
Android 通道(stub)client/android/app/src/main/kotlin/com/pangolin/pangolin_vpn/MainActivity.kt
Android VPN 服务client/android/app/src/main/kotlin/com/pangolin/pangolin_vpn/PangolinVpnService.kt