Files
pangolin/client/macos/Runner/VpnChannel.swift
T
wangjia 6bcdbbe752 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>
2026-07-01 06:38:47 +08:00

288 lines
15 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// VpnChannel.swift app VPN (B / P1 )
//
// Flutter pangolin/vpn(MethodChannel)+ pangolin/vpn/statuspangolin/vpn/stats
// (EventChannel) NETunnelProviderManager:/ PacketTunnel System Extension
// client/lib/bridge/vpn_bridge.dart VpnNativeBridge
//
// : MainFlutterWindow.awakeFromNib `VpnChannel.register(with: flutterViewController)`
//
// ** System Extension**(OSSystemExtensionRequest,
// activateSystemExtensionIfNeeded + 4),
// ; NETunnelProviderManager provider
import FlutterMacOS
import NetworkExtension
import SystemExtensions
import os.log
// os_log + %{public} Console.app / `log show` (NSLog %@
// redact <private>,)os_log(C API) macOS 10.12 ,
// Runner 10.15 :`log show --predicate 'subsystem == "com.pangolin.pangolin"'`
private let vpnLogObj = OSLog(subsystem: "com.pangolin.pangolin", category: "vpn")
private func vpnLog(_ message: String) {
os_log("%{public}@", log: vpnLogObj, type: .default, message)
NSLog("[pangolin/vpn] %@", message) // stderr,flutter run
}
final class VpnChannel: NSObject {
private static let tunnelBundleId = "com.pangolin.pangolin.PacketTunnel"
private var statusSink: FlutterEventSink?
private var statsSink: FlutterEventSink?
private var statusObserver: NSObjectProtocol?
private let statsClient = StatsClient()
private var manager: NETunnelProviderManager?
private var sysextDelegate: SysExtActivationDelegate?
static func register(with registrar: FlutterPluginRegistrar) {
let instance = VpnChannel()
let method = FlutterMethodChannel(name: "pangolin/vpn",
binaryMessenger: registrar.messenger)
method.setMethodCallHandler(instance.handle)
FlutterEventChannel(name: "pangolin/vpn/status", binaryMessenger: registrar.messenger)
.setStreamHandler(StatusStreamHandler(owner: instance))
FlutterEventChannel(name: "pangolin/vpn/stats", binaryMessenger: registrar.messenger)
.setStreamHandler(StatsStreamHandler(owner: instance))
instance.observeStatus()
// System Extension: sysext(on-demand/)
// , app primeExistingConnection start()
// sysext (,command.sock ,
// ) register : replace,
// bundle
instance.activateOnLaunch()
}
/// / sysext(): bundle
private func activateOnLaunch() {
Task {
do {
vpnLog("launch: 主动激活 System Extension(确保运行的是 bundle 内最新版)…")
try await activateSystemExtensionIfNeeded()
vpnLog("launch: System Extension 激活/更新完成 ✓")
} catch {
let ns = error as NSError
vpnLog("launch: System Extension 激活失败 ✗ code=\(ns.code) desc=\(ns.localizedDescription)")
}
}
}
// MethodChannel
private func handle(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
switch call.method {
case "start":
guard let configJson = call.arguments as? String else {
result(FlutterError(code: "bad_args", message: "expected config json", details: nil))
return
}
Task { await self.start(configJson, result) }
case "stop":
Task { await self.stop(result) }
case "getStatus":
result(Self.statusString(manager?.connection.status ?? .invalid))
case "selectOutbound":
// TODO: sendProviderMessage / libbox CommandClient outbound
result(nil)
case "getActiveOutbound":
result("auto")
case "setKillSwitch":
// includeAllNetworks / on-demand kill switch()
result(nil)
default:
result(FlutterMethodNotImplemented)
}
}
private func start(_ configJson: String, _ result: @escaping FlutterResult) async {
vpnLog("start() 收到调用, config 长度=\(configJson.count) bytes")
do {
vpnLog("step① 激活 System Extension …")
try await activateSystemExtensionIfNeeded()
vpnLog("step① System Extension 激活完成 ✓")
vpnLog("step② 装配 NETunnelProviderManager …")
let mgr = try await loadOrCreateManager()
self.manager = mgr
vpnLog("step② manager 就绪, 当前隧道状态=\(Self.statusString(mgr.connection.status))")
vpnLog("step③ startVPNTunnel(options: configContent) …")
try mgr.connection.startVPNTunnel(options: [
"configContent": configJson as NSString,
])
vpnLog("step③ startVPNTunnel 调用已返回(实际起停由 NEVPNStatus 流驱动)✓")
result(nil)
} catch {
let ns = error as NSError
vpnLog("start FAILED ✗ domain=\(ns.domain) code=\(ns.code) desc=\(ns.localizedDescription) userInfo=\(ns.userInfo)")
result(FlutterError(code: "start_failed", message: error.localizedDescription, details: nil))
}
}
private func stop(_ result: @escaping FlutterResult) async {
manager?.connection.stopVPNTunnel()
result(nil)
}
// NETunnelProviderManager
private func loadOrCreateManager() async throws -> NETunnelProviderManager {
let all = try await NETunnelProviderManager.loadAllFromPreferences()
vpnLog(" loadAllFromPreferences: 已有 \(all.count) 个 VPN 配置")
let mgr = all.first ?? NETunnelProviderManager()
let proto = (mgr.protocolConfiguration as? NETunnelProviderProtocol) ?? NETunnelProviderProtocol()
proto.providerBundleIdentifier = Self.tunnelBundleId
proto.serverAddress = "Pangolin" //
mgr.protocolConfiguration = proto
mgr.localizedDescription = "Pangolin"
mgr.isEnabled = true
vpnLog(" saveToPreferences(providerBundleId=\(Self.tunnelBundleId)) …")
try await mgr.saveToPreferences()
try await mgr.loadFromPreferences() // , connection
vpnLog(" manager 保存+重载完成 ✓")
return mgr
}
// / PacketTunnel System Extension
// ; didFinishWithResult
private func activateSystemExtensionIfNeeded() async throws {
vpnLog(" 提交 OSSystemExtensionRequest.activationRequest(id=\(Self.tunnelBundleId)) …")
vpnLog(" 主 bundle=\(Bundle.main.bundlePath)")
let sysextDir = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/SystemExtensions").path
vpnLog(" SystemExtensions 目录=\(sysextDir) 内容=\((try? FileManager.default.contentsOfDirectory(atPath: sysextDir)) ?? ["<读取失败>"])")
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
let req = OSSystemExtensionRequest.activationRequest(
forExtensionWithIdentifier: Self.tunnelBundleId, queue: .main)
let delegate = SysExtActivationDelegate(continuation: cont)
self.sysextDelegate = delegate //
req.delegate = delegate
OSSystemExtensionManager.shared.submitRequest(req)
vpnLog(" submitRequest 已提交, 等待 sysextd 回调(didFinish / didFail / needsApproval)…")
}
}
// /
private func observeStatus() {
// StatsClient statsSink
statsClient.onStats = { [weak self] stats in self?.statsSink?(stats) }
statusObserver = NotificationCenter.default.addObserver(
forName: .NEVPNStatusDidChange, object: nil, queue: .main
) { [weak self] note in
guard let self, let conn = note.object as? NEVPNConnection else { return }
let s = Self.statusString(conn.status)
vpnLog("NEVPNStatus 变化 → \(s) (raw=\(conn.status.rawValue))")
self.statusSink?(s)
// sendProviderMessage
switch conn.status {
case .connected:
if let s = conn as? NETunnelProviderSession {
self.statsClient.start(session: s)
}
case .disconnected, .invalid:
self.statsClient.stop()
default:
break
}
}
// (sysext)app connected .connected
// observer statsClient start
// manager( onStatsListen )
Task { await self.primeExistingConnection() }
}
/// manager (StatsClient.start )
private func primeExistingConnection() async {
guard let all = try? await NETunnelProviderManager.loadAllFromPreferences(),
let mgr = all.first else { return }
await MainActor.run {
self.manager = mgr
if mgr.connection.status == .connected, let s = mgr.connection as? NETunnelProviderSession {
vpnLog("启动时检测到隧道已连接(raw=\(mgr.connection.status.rawValue)),主动启动实时统计")
self.statusSink?(Self.statusString(mgr.connection.status))
self.statsClient.start(session: s)
}
}
}
fileprivate func onStatusListen(_ sink: @escaping FlutterEventSink) {
statusSink = sink
sink(Self.statusString(manager?.connection.status ?? .invalid))
}
fileprivate func onStatusCancel() { statusSink = nil }
fileprivate func onStatsListen(_ sink: @escaping FlutterEventSink) {
statsSink = sink
// StatsClient( sendProviderMessage ) connected
// onStats ; sink
if manager?.connection.status == .connected,
let s = manager?.connection as? NETunnelProviderSession {
statsClient.start(session: s)
}
}
fileprivate func onStatsCancel() { statsSink = nil }
private static func statusString(_ s: NEVPNStatus) -> String {
switch s {
case .connected: return "on"
case .connecting, .reasserting: return "connecting"
case .disconnecting: return "disconnecting"
case .disconnected, .invalid: return "off"
@unknown default: return "error"
}
}
}
// EventChannel
private final class StatusStreamHandler: NSObject, FlutterStreamHandler {
weak var owner: VpnChannel?
init(owner: VpnChannel) { self.owner = owner }
func onListen(withArguments _: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
owner?.onStatusListen(events); return nil
}
func onCancel(withArguments _: Any?) -> FlutterError? { owner?.onStatusCancel(); return nil }
}
private final class StatsStreamHandler: NSObject, FlutterStreamHandler {
weak var owner: VpnChannel?
init(owner: VpnChannel) { self.owner = owner }
func onListen(withArguments _: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
owner?.onStatsListen(events); return nil
}
func onCancel(withArguments _: Any?) -> FlutterError? { owner?.onStatsCancel(); return nil }
}
// System Extension ,
// didFinishWithResult( await ,
// )
private final class SysExtActivationDelegate: NSObject, OSSystemExtensionRequestDelegate {
private let continuation: CheckedContinuation<Void, Error>
private var resumed = false
init(continuation: CheckedContinuation<Void, Error>) { self.continuation = continuation }
func request(_ request: OSSystemExtensionRequest,
didFinishWithResult result: OSSystemExtensionRequest.Result) {
vpnLog("sysext didFinishWithResult ✓ result=\(result.rawValue) (0=completed, 1=willCompleteAfterReboot)")
guard !resumed else { return }
resumed = true
continuation.resume()
}
func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) {
let ns = error as NSError
// OSSystemExtensionErrorDomain code :1 unknown,2 missingEntitlement,
// 3 unsupportedParentBundleLocation,4 extensionNotFound,8 codeSignatureInvalid,
// 9 validationFailed,10 forbiddenBySystemPolicy,13 authorizationRequired
vpnLog("sysext didFailWithError ✗ domain=\(ns.domain) code=\(ns.code) desc=\(ns.localizedDescription)")
guard !resumed else { return }
resumed = true
continuation.resume(throwing: error)
}
func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) {
vpnLog("sysext requestNeedsUserApproval —— 需在 系统设置 → 隐私与安全性 点「允许」(等待中…)")
}
func request(_ request: OSSystemExtensionRequest,
actionForReplacingExtension existing: OSSystemExtensionProperties,
withExtension ext: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction {
vpnLog("sysext 替换扩展: 已装 v\(existing.bundleVersion)/\(existing.bundleShortVersion) → 新 v\(ext.bundleVersion)/\(ext.bundleShortVersion), 选择 replace")
return .replace
}
}