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:
@@ -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 }
|
||||
|
||||
@@ -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
@@ -50,7 +50,7 @@
|
||||
<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>
|
||||
<p class="sub">Pangolin 客户端 · 断网保护设计依据与现状 · 2026-06-22(2026-07-01 更新:macOS killswitch 落地 enforceRoutes+on-demand;includeAllNetworks 实测堵死整机网络已撤回)</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-demand(L1→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>
|
||||
|
||||
Reference in New Issue
Block a user