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(); }