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>
This commit is contained in:
wangjia
2026-07-01 06:09:04 +08:00
parent 97c0fdbbad
commit a32d87c9c0
3 changed files with 32 additions and 35 deletions
@@ -63,12 +63,7 @@ final class PacketTunnelProvider: NEPacketTunnelProvider {
LibboxSetup(setup, &setupErr)
if let setupErr { throw setupErr }
// KillSwitch: app startVPNTunnel options ;OS on-demand
// options nil true(on-demand killswitch )libbox
// includeAllNetworks() NE includeAllNetworks ,
let killSwitch = (options?["killSwitch"] as? NSNumber)?.boolValue ?? true
log.info("startTunnel: killSwitch=\(killSwitch, privacy: .public)")
let platform = PangolinPlatformInterface(provider: self, includeAllNetworks: killSwitch)
let platform = PangolinPlatformInterface(provider: self)
self.platform = platform
var newErr: NSError?
@@ -187,11 +182,9 @@ final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol
private var monitor: NWPathMonitor?
private var defaultInterfaceIndex: Int32 = -1
private let monitorQueue = DispatchQueue(label: "pangolin.tunnel.pathmonitor")
private let includeAllNetworksFlag: Bool
init(provider: NEPacketTunnelProvider, includeAllNetworks: Bool) {
init(provider: NEPacketTunnelProvider) {
self.provider = provider
self.includeAllNetworksFlag = includeAllNetworks
super.init()
}
@@ -276,7 +269,7 @@ final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol
// MARK: ()
func underNetworkExtension() -> Bool { true }
func useProcFS() -> Bool { false }
func includeAllNetworks() -> Bool { includeAllNetworksFlag }
func includeAllNetworks() -> Bool { false }
func clearDNSCache() {}
func readWIFIState() -> LibboxWIFIState? { nil }
func systemCertificates() -> (any LibboxStringIteratorProtocol)? { nil }
+10 -7
View File
@@ -131,7 +131,6 @@ final class VpnChannel: NSObject {
vpnLog("step③ startVPNTunnel(options: configContent) …")
try mgr.connection.startVPNTunnel(options: [
"configContent": configJson as NSString,
"killSwitch": NSNumber(value: killSwitchEnabled),
])
vpnLog("step③ startVPNTunnel 调用已返回(实际起停由 NEVPNStatus 流驱动)✓")
result(nil)
@@ -181,14 +180,18 @@ final class VpnChannel: NSObject {
// KillSwitch()NE
// killSwitchEnabled manager NE (,):
// L2 OS :includeAllNetworks=true + enforceRoutes=true ,
// neagent ( app/)
// L3 :NEOnDemandRule OS 线/, fail-closed
// excludeLocalNetworks=true LAN(/AirPlay ),
// enforceRoutes=true: includedRoutes(0.0.0.0/0 ),
//
// NEOnDemandRule :OS 线/,
// excludeLocalNetworks=true: LAN(/AirPlay ),
// includeAllNetworks:( sing-box
// app ) + on-demand
// + ()/
// ,, docs/killswitch-design.html §6.5
// enforceRoutes/excludeLocalNetworks macOS 11+ API,Runner 10.15 #available
private func configureKillSwitch(on mgr: NETunnelProviderManager) {
if let proto = mgr.protocolConfiguration as? NETunnelProviderProtocol {
proto.includeAllNetworks = killSwitchEnabled
proto.includeAllNetworks = false
if #available(macOS 11.0, *) {
proto.enforceRoutes = killSwitchEnabled
proto.excludeLocalNetworks = true
@@ -203,7 +206,7 @@ final class VpnChannel: NSObject {
mgr.onDemandRules = []
mgr.isOnDemandEnabled = false
}
vpnLog(" killSwitch 配置: includeAllNetworks=\(killSwitchEnabled) enforceRoutes=\(killSwitchEnabled) onDemand=\(killSwitchEnabled)")
vpnLog(" killSwitch 配置: enforceRoutes=\(killSwitchEnabled) onDemand=\(killSwitchEnabled) includeAllNetworks=false")
}
/// killswitch( + + ) setKillSwitch manager
+19 -18
View File
@@ -50,7 +50,7 @@
<a class="back" href="index.html">← 文档索引</a>
<h1>KillSwitch 设计与跨平台能力矩阵<span class="small">(知识库)</span></h1>
<p class="sub">Pangolin 客户端 · 断网保护设计依据与现状 · 2026-06-222026-06-30 更新:macOS L0→L3 已实现</p>
<p class="sub">Pangolin 客户端 · 断网保护设计依据与现状 · 2026-06-222026-07-01 更新:macOS killswitch 落地 enforceRoutes+on-demandincludeAllNetworks 实测堵死整机网络已撤回</p>
<div class="lead">
KillSwitch 不是「一个开关」,而是<b>分层能力</b>——理想态、平台天花板、当前实现是三件不同的事。本文沉淀其设计依据与现状。
@@ -104,7 +104,7 @@ KillSwitch 不是「一个开关」,而是<b>分层能力</b>——理想态
<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>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>
@@ -113,7 +113,7 @@ KillSwitch 不是「一个开关」,而是<b>分层能力</b>——理想态
<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>能力与实现曾严重不匹配</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>
@@ -124,40 +124,41 @@ KillSwitch 不是「一个开关」,而是<b>分层能力</b>——理想态
</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>
<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>目标 Level</th><th>NE 机制</th><th>效果</th></tr></thead>
<thead><tr><th>当前达成</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>
<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>loadOrCreateManager</code> 按标志设:<code>includeAllNetworks</code> / <code>enforceRoutes=true</code> / <code>excludeLocalNetworks=true</code>(放行 LAN 打印机等)/ <code>onDemandRules</code> + <code>isOnDemandEnabled</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>
<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>
<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>includeAllNetworks</code>/<code>enforceRoutes</code>/<code>excludeLocalNetworks</code>/on-demand 均 macOS 10.15/11+ 可用Runner 部署目标满足。</p>
<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> → 流量被阻断(无 IP/DNS 泄漏)= 跨过 L1→L2 质变线</li>
<li>on-demand:开机/掉线窗口隧道自动拉起;手动「断开」能真断、不反弹(验 <code>stop()</code> 的 on-demand 处理)</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>