From bd984feff92f0dd926b2e62045e34ae527d22f73 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Tue, 23 Jun 2026 00:28:53 +0800 Subject: [PATCH] =?UTF-8?q?fix(client/ios):=20iOS=20=E6=8E=A5=E9=80=9A?= =?UTF-8?q?=E5=8E=9F=E7=94=9F=E9=9A=A7=E9=81=93=E2=80=94=E2=80=94=E5=88=87?= =?UTF-8?q?=E7=9C=9F=E6=A1=A5=20+=20save-before-start=20+=20DNS=20matchDom?= =?UTF-8?q?ains?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: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 --- .../PacketTunnel/PacketTunnelProvider.swift | 7 ++- client/ios/Runner/VpnManager.swift | 45 ++++++++++++++----- client/lib/bridge/vpn_bridge_provider.dart | 5 +++ 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/client/ios/PacketTunnel/PacketTunnelProvider.swift b/client/ios/PacketTunnel/PacketTunnelProvider.swift index fa0cc06..1004cfa 100644 --- a/client/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/client/ios/PacketTunnel/PacketTunnelProvider.swift @@ -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) diff --git a/client/ios/Runner/VpnManager.swift b/client/ios/Runner/VpnManager.swift index 039930d..89f7314 100644 --- a/client/ios/Runner/VpnManager.swift +++ b/client/ios/Runner/VpnManager.swift @@ -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) + } + } + } } } diff --git a/client/lib/bridge/vpn_bridge_provider.dart b/client/lib/bridge/vpn_bridge_provider.dart index 9eebecb..ce6e692 100644 --- a/client/lib/bridge/vpn_bridge_provider.dart +++ b/client/lib/bridge/vpn_bridge_provider.dart @@ -24,6 +24,11 @@ final vpnBridgeProvider = Provider((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(); }