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:
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user