revert(macos): killswitch 回退 L0(低优先级暂缓)

用户判定 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>
This commit is contained in:
wangjia
2026-07-01 06:38:47 +08:00
parent a32d87c9c0
commit 6bcdbbe752
2 changed files with 8 additions and 81 deletions
+2 -76
View File
@@ -33,10 +33,6 @@ final class VpnChannel: NSObject {
private let statsClient = StatsClient()
private var manager: NETunnelProviderManager?
private var sysextDelegate: SysExtActivationDelegate?
// KillSwitch(), true Dart AppSettings.killSwitch
// NE fail-closed:includeAllNetworks + enforceRoutes(L2 OS )
// + NEOnDemandRule (L3 /线) docs/killswitch-design.html §6.5
private var killSwitchEnabled = true
static func register(with registrar: FlutterPluginRegistrar) {
let instance = VpnChannel()
@@ -91,26 +87,8 @@ final class VpnChannel: NSObject {
case "getActiveOutbound":
result("auto")
case "setKillSwitch":
let on = (call.arguments as? Bool) ?? true
killSwitchEnabled = on
vpnLog("setKillSwitch=\(on)")
// manager NE (includeAllNetworks/enforceRoutes/on-demand);
// manager , loadOrCreateManager
if let mgr = manager {
Task {
do {
try await self.applyKillSwitchConfig(to: mgr)
vpnLog("setKillSwitch: NE 配置已更新并保存 ✓")
result(nil)
} catch {
let ns = error as NSError
vpnLog("setKillSwitch: 保存 NE 配置失败 ✗ code=\(ns.code) desc=\(ns.localizedDescription)")
result(FlutterError(code: "killswitch_failed", message: error.localizedDescription, details: nil))
}
}
} else {
result(nil)
}
// includeAllNetworks / on-demand kill switch()
result(nil)
default:
result(FlutterMethodNotImplemented)
}
@@ -142,19 +120,6 @@ final class VpnChannel: NSObject {
}
private func stop(_ result: @escaping FlutterResult) async {
// on-demand stopVPNTunnel OS on-demand
// start() loadOrCreateManager killSwitchEnabled on-demand
if let mgr = manager, mgr.isOnDemandEnabled {
mgr.isOnDemandEnabled = false
do {
try await mgr.saveToPreferences()
try await mgr.loadFromPreferences()
vpnLog("stop: 已临时关闭 on-demand(避免手动断开被自动拉回)")
} catch {
let ns = error as NSError
vpnLog("stop: 关闭 on-demand 失败(仍继续断开) code=\(ns.code) desc=\(ns.localizedDescription)")
}
}
manager?.connection.stopVPNTunnel()
result(nil)
}
@@ -170,7 +135,6 @@ final class VpnChannel: NSObject {
mgr.protocolConfiguration = proto
mgr.localizedDescription = "Pangolin"
mgr.isEnabled = true
configureKillSwitch(on: mgr) // killSwitchEnabled NE fail-closed
vpnLog(" saveToPreferences(providerBundleId=\(Self.tunnelBundleId)) …")
try await mgr.saveToPreferences()
try await mgr.loadFromPreferences() // , connection
@@ -178,44 +142,6 @@ final class VpnChannel: NSObject {
return mgr
}
// KillSwitch()NE
// killSwitchEnabled manager NE (,):
// 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 = false
if #available(macOS 11.0, *) {
proto.enforceRoutes = killSwitchEnabled
proto.excludeLocalNetworks = true
}
}
if killSwitchEnabled {
let rule = NEOnDemandRuleConnect()
rule.interfaceTypeMatch = .any
mgr.onDemandRules = [rule]
mgr.isOnDemandEnabled = true
} else {
mgr.onDemandRules = []
mgr.isOnDemandEnabled = false
}
vpnLog(" killSwitch 配置: enforceRoutes=\(killSwitchEnabled) onDemand=\(killSwitchEnabled) includeAllNetworks=false")
}
/// killswitch( + + ) setKillSwitch manager
private func applyKillSwitchConfig(to mgr: NETunnelProviderManager) async throws {
configureKillSwitch(on: mgr)
try await mgr.saveToPreferences()
try await mgr.loadFromPreferences()
}
// / PacketTunnel System Extension
// ; didFinishWithResult
private func activateSystemExtensionIfNeeded() async throws {
+6 -5
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-07-01 更新:macOS killswitch 落地 enforceRoutes+on-demandincludeAllNetworks 实测堵死整机网络已撤回</p>
<p class="sub">Pangolin 客户端 · 断网保护设计依据与现状 · 2026-06-222026-07-01 更新:macOS killswitch 尝试后<b>暂缓、回退 L0</b>,详见 §6.5</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="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>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>
@@ -113,7 +113,7 @@ KillSwitch 不是「一个开关」,而是<b>分层能力</b>——理想态
<h2>6. Pangolin 现实判断与决策</h2>
<ul>
<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>能力与实现严重不匹配</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>
@@ -124,7 +124,8 @@ KillSwitch 不是「一个开关」,而是<b>分层能力</b>——理想态
</li>
</ul>
<h2>6.5 macOS KillSwitch 实现方案<span class="small">本轮 #1</span></h2>
<h2>6.5 macOS KillSwitch 尝试与暂缓<span class="small">#1,低优先级搁置</span></h2>
<div class="card root"><b>结论(2026-07-01</b>:尝试落地后<b>已回退到 L0stub</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>
@@ -137,7 +138,7 @@ KillSwitch 不是「一个开关」,而是<b>分层能力</b>——理想态
</tbody>
</table>
<p class="small">即「连接期 enforceRoutes 防漏 + on-demand 兜重连」,介于 L1 与 L2 之间,<b>尚未到</b>设计追求的 includeAllNetworks-L3。诚实标注,不夸大。</p>
<h3>改动(2 个原生文件,不动 Dart/服务端)</h3>
<h3>曾经的改动方案<span class="small">(已回退,留作将来参考)</span></h3>
<div class="card">
<b><code>client/macos/Runner/VpnChannel.swift</code></b>
<ol>