fix(client/ios): iOS 接通原生隧道——切真桥 + save-before-start + DNS matchDomains

根因:iOS 端 vpn_bridge_provider 把 iOS 归入'其他平台 → VpnBridgeMock',
整个 App 用假桥,伪造已连接状态与写死的 URLTest 延迟(18/32/54ms),从不调用
B1/C1 打通的原生 NEPacketTunnelProvider。表现为 UI 显示已连接但流量全直连
(apple 通/google 被墙、RTT 20ms),设备日志无任何隧道/sing-box/节点痕迹。

三处修复:
1. vpn_bridge_provider.dart: iOS → VpnNativeBridge(真原生 MethodChannel)。
2. VpnManager.start(): 先 saveToPreferences+重载(首启触发系统授权、持久化配置)
   再 startVPNTunnel,否则对未持久化 manager 调用抛 NEVPNError。对齐 macOS。
3. PacketTunnelProvider.openTun: DNS 补 matchDomains=[""],强制所有 DNS 走隧道,
   iOS 不设会把隧道 DNS 当补充解析器、查询仍走系统直连 DNS 致污染打不开站点。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-23 00:28:53 +08:00
parent 9b365c7e1c
commit bd984feff9
3 changed files with 46 additions and 11 deletions
@@ -182,7 +182,12 @@ final class PangolinPlatformInterface: NSObject, LibboxPlatformInterfaceProtocol
settings.ipv6Settings = ipv6
}
if let dns = try? options.getDNSServerAddress(), !dns.value.isEmpty {
settings.dnsSettings = NEDNSSettings(servers: [dns.value])
let dnsSettings = NEDNSSettings(servers: [dns.value])
// iOS matchDomains=[""] DNS ;
// DNS , DNS(en0),
// DNS "" =
dnsSettings.matchDomains = [""]
settings.dnsSettings = dnsSettings
}
let sem = DispatchSemaphore(value: 0)
+35 -10
View File
@@ -116,20 +116,45 @@ class VpnManager: NSObject {
/// VPN NETunnelProviderSession
/// [configJson]: sing-box JSON options PacketTunnelProvider.startTunnel
func start(configJson: String, completion: @escaping (Error?) -> Void) {
guard let manager = tunnelManager else {
completion(makeError("VpnManager: tunnelManager not loaded. Call loadFromPreferences first."))
return
}
// App Group UserDefaults PacketTunnel extension
if let shared = UserDefaults(suiteName: appGroup) {
shared.set(configJson, forKey: "lastConfigJson")
}
do {
let options: [String: NSObject] = ["config": configJson as NSString]
try manager.connection.startVPNTunnel(options: options)
completion(nil)
} catch {
completion(error)
// iOS (saveToPreferences, VPN )
// connection manager startVPNTunnel NEVPNError
// macOS VpnChannel.loadOrCreateManager
NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, error in
guard let self = self else { return }
if let error = error { completion(error); return }
let manager = managers?.first(where: {
($0.protocolConfiguration as? NETunnelProviderProtocol)?
.providerBundleIdentifier == self.extensionBundleId
}) ?? NETunnelProviderManager()
let proto = (manager.protocolConfiguration as? NETunnelProviderProtocol)
?? NETunnelProviderProtocol()
proto.providerBundleIdentifier = self.extensionBundleId
proto.serverAddress = "Pangolin"
manager.protocolConfiguration = proto
// 线localizedDescription VPN/
manager.localizedDescription = "Pangolin 加速"
manager.isEnabled = true
manager.saveToPreferences { saveErr in
if let saveErr = saveErr { completion(saveErr); return }
// connection
manager.loadFromPreferences { loadErr in
if let loadErr = loadErr { completion(loadErr); return }
self.tunnelManager = manager
do {
let options: [String: NSObject] = ["config": configJson as NSString]
try manager.connection.startVPNTunnel(options: options)
completion(nil)
} catch {
completion(error)
}
}
}
}
}
@@ -24,6 +24,11 @@ final vpnBridgeProvider = Provider<VpnBridge>((ref) {
if (Platform.isMacOS && kUseNativeVpnMacOS) {
return VpnNativeBridge();
}
// iOS:NEPacketTunnelProvider 内嵌 libbox(B1/C1 已打通)。走原生 MethodChannel
// (pangolin/vpn → AppDelegate → VpnManager),不再用 mock。
if (Platform.isIOS) {
return VpnNativeBridge();
}
if (Platform.isMacOS || Platform.isLinux || Platform.isWindows) {
return DesktopVpnBridge();
}