From 9be13ea465b147c824643cddfefb214c0630354d Mon Sep 17 00:00:00 2001 From: Boris Verbitskii <79607875547@yandex.ru> Date: Fri, 17 May 2024 18:17:08 +0700 Subject: [PATCH] PacketTunnelProvider refactoring - removing unnecessary dispatchQueue - removing lazy initiation for wg and ovpn - fix memory leaks --- client/platforms/ios/NELogController.swift | 6 +- .../ios/PacketTunnelProvider+OpenVPN.swift | 367 ++++++++--------- .../ios/PacketTunnelProvider+WireGuard.swift | 372 ++++++++---------- .../platforms/ios/PacketTunnelProvider.swift | 264 ++++++------- 4 files changed, 481 insertions(+), 528 deletions(-) diff --git a/client/platforms/ios/NELogController.swift b/client/platforms/ios/NELogController.swift index 876a8678..e1d71e60 100644 --- a/client/platforms/ios/NELogController.swift +++ b/client/platforms/ios/NELogController.swift @@ -2,15 +2,15 @@ import Foundation import os.log public func wg_log(_ type: OSLogType, title: String = "", staticMessage: StaticString) { - neLog(type, title: "WG: \(title)", message: "\(staticMessage)") + neLog(type, title: "WG: \(title)", message: "\(staticMessage)") } public func wg_log(_ type: OSLogType, title: String = "", message: String) { - neLog(type, title: "WG: \(title)", message: message) + neLog(type, title: "WG: \(title)", message: message) } public func ovpnLog(_ type: OSLogType, title: String = "", message: String) { - neLog(type, title: "OVPN: \(title)", message: message) + neLog(type, title: "OVPN: \(title)", message: message) } public func neLog(_ type: OSLogType, title: String = "", message: String) { diff --git a/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift b/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift index 489c12c8..3e0a4a07 100644 --- a/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift +++ b/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift @@ -3,223 +3,232 @@ import NetworkExtension import OpenVPNAdapter struct OpenVPNConfig: Decodable { - let config: String - let splitTunnelType: Int - let splitTunnelSites: [String] + let config: String + let splitTunnelType: Int + let splitTunnelSites: [String] - var str: String { - "splitTunnelType: \(splitTunnelType) splitTunnelSites: \(splitTunnelSites) config: \(config)" - } + var str: String { + "splitTunnelType: \(splitTunnelType) splitTunnelSites: \(splitTunnelSites) config: \(config)" + } } extension PacketTunnelProvider { - func startOpenVPN(completionHandler: @escaping (Error?) -> Void) { - guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol, - let providerConfiguration = protocolConfiguration.providerConfiguration, - let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else { - ovpnLog(.error, message: "Can't start") - return + func startOpenVPN(completionHandler: @escaping (Error?) -> Void) { + guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol, + let providerConfiguration = protocolConfiguration.providerConfiguration, + let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else { + ovpnLog(.error, message: "Can't start") + return + } + + do { + let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData) + ovpnLog(.info, title: "config: ", message: openVPNConfig.str) + let ovpnConfiguration = Data(openVPNConfig.config.utf8) + setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler) + } catch { + ovpnLog(.error, message: "Can't parse config: \(error.localizedDescription)") + + if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError { + ovpnLog(.error, message: "Can't parse config: \(underlyingError.localizedDescription)") + } + + return + } } - do { - // ovpnLog(.info, message: "providerConfiguration: \(String(decoding: openVPNConfigData, as: UTF8.self))") + private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data, + withShadowSocks viaSS: Bool = false, + completionHandler: @escaping (Error?) -> Void) { + ovpnLog(.info, message: "Setup and launch") - let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData) - ovpnLog(.info, title: "config: ", message: openVPNConfig.str) - let ovpnConfiguration = Data(openVPNConfig.config.utf8) - setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler) - } catch { - ovpnLog(.error, message: "Can't parse config: \(error.localizedDescription)") + let str = String(decoding: ovpnConfiguration, as: UTF8.self) - if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError { - ovpnLog(.error, message: "Can't parse config: \(underlyingError.localizedDescription)") - } + let configuration = OpenVPNConfiguration() + configuration.fileContent = ovpnConfiguration + if str.contains("cloak") { + configuration.setPTCloak() + } - return + let evaluation: OpenVPNConfigurationEvaluation? + do { + ovpnAdapter = OpenVPNAdapter() + ovpnAdapter?.delegate = self + evaluation = try ovpnAdapter?.apply(configuration: configuration) + + } catch { + completionHandler(error) + return + } + + if evaluation?.autologin == false { + ovpnLog(.info, message: "Implement login with user credentials") + } + + vpnReachability.startTracking { [weak self] status in + guard status == .reachableViaWiFi else { return } + self?.ovpnAdapter?.reconnect(afterTimeInterval: 5) + } + + startHandler = completionHandler + ovpnAdapter?.connect(using: packetFlow) } - } + + func handleOpenVPNStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) { + guard let completionHandler = completionHandler else { return } + let bytesin = ovpnAdapter?.transportStatistics.bytesIn + let bytesout = ovpnAdapter?.transportStatistics.bytesOut - private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data, - withShadowSocks viaSS: Bool = false, - completionHandler: @escaping (Error?) -> Void) { - ovpnLog(.info, message: "Setup and launch") + guard let bytesin, let bytesout else { + completionHandler(nil) + return + } - let str = String(decoding: ovpnConfiguration, as: UTF8.self) + let response: [String: Any] = [ + "rx_bytes": bytesin, + "tx_bytes": bytesout + ] - let configuration = OpenVPNConfiguration() - configuration.fileContent = ovpnConfiguration - if str.contains("cloak") { - configuration.setPTCloak() + completionHandler(try? JSONSerialization.data(withJSONObject: response, options: [])) } - let evaluation: OpenVPNConfigurationEvaluation - do { - evaluation = try ovpnAdapter.apply(configuration: configuration) + func stopOpenVPN(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.description)") - } catch { - completionHandler(error) - return + stopHandler = completionHandler + if vpnReachability.isTracking { + vpnReachability.stopTracking() + } + ovpnAdapter?.disconnect() } - - if !evaluation.autologin { - ovpnLog(.info, message: "Implement login with user credentials") - } - - vpnReachability.startTracking { [weak self] status in - guard status == .reachableViaWiFi else { return } - self?.ovpnAdapter.reconnect(afterTimeInterval: 5) - } - - startHandler = completionHandler - ovpnAdapter.connect(using: packetFlow) - - // let ifaces = Interface.allInterfaces() - // .filter { $0.family == .ipv4 } - // .map { iface in iface.name } - - // ovpn_log(.error, message: "Available TUN Interfaces: \(ifaces)") - } - - func handleOpenVPNStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) { - guard let completionHandler = completionHandler else { return } - let bytesin = ovpnAdapter.transportStatistics.bytesIn - let bytesout = ovpnAdapter.transportStatistics.bytesOut - - let response: [String: Any] = [ - "rx_bytes": bytesin, - "tx_bytes": bytesout - ] - - completionHandler(try? JSONSerialization.data(withJSONObject: response, options: [])) - } - - func stopOpenVPN(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { - ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.description)") - - stopHandler = completionHandler - if vpnReachability.isTracking { - vpnReachability.stopTracking() - } - ovpnAdapter.disconnect() - } } extension PacketTunnelProvider: OpenVPNAdapterDelegate { - // OpenVPNAdapter calls this delegate method to configure a VPN tunnel. - // `completionHandler` callback requires an object conforming to `OpenVPNAdapterPacketFlow` - // protocol if the tunnel is configured without errors. Otherwise send nil. - // `OpenVPNAdapterPacketFlow` method signatures are similar to `NEPacketTunnelFlow` so - // you can just extend that class to adopt `OpenVPNAdapterPacketFlow` protocol and - // send `self.packetFlow` to `completionHandler` callback. - func openVPNAdapter( - _ openVPNAdapter: OpenVPNAdapter, - configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?, - completionHandler: @escaping (Error?) -> Void - ) { - // In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers - // send empty string to NEDNSSettings.matchDomains - networkSettings?.dnsSettings?.matchDomains = [""] + // OpenVPNAdapter calls this delegate method to configure a VPN tunnel. + // `completionHandler` callback requires an object conforming to `OpenVPNAdapterPacketFlow` + // protocol if the tunnel is configured without errors. Otherwise send nil. + // `OpenVPNAdapterPacketFlow` method signatures are similar to `NEPacketTunnelFlow` so + // you can just extend that class to adopt `OpenVPNAdapterPacketFlow` protocol and + // send `self.packetFlow` to `completionHandler` callback. + func openVPNAdapter( + _ openVPNAdapter: OpenVPNAdapter, + configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?, + completionHandler: @escaping (Error?) -> Void + ) { + // In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers + // send empty string to NEDNSSettings.matchDomains + networkSettings?.dnsSettings?.matchDomains = [""] - if splitTunnelType == 1 { - var ipv4IncludedRoutes = [NEIPv4Route]() + if splitTunnelType == 1 { + var ipv4IncludedRoutes = [NEIPv4Route]() - for allowedIPString in splitTunnelSites { - if let allowedIP = IPAddressRange(from: allowedIPString) { - ipv4IncludedRoutes.append(NEIPv4Route( - destinationAddress: "\(allowedIP.address)", - subnetMask: "\(allowedIP.subnetMask())")) - } - } + guard let splitTunnelSites else { + completionHandler(NSError(domain: "Split tunnel sited not setted up", code: 0)) + return + } - networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes - } else { - if splitTunnelType == 2 { - var ipv4ExcludedRoutes = [NEIPv4Route]() - var ipv4IncludedRoutes = [NEIPv4Route]() - var ipv6IncludedRoutes = [NEIPv6Route]() + for allowedIPString in splitTunnelSites { + if let allowedIP = IPAddressRange(from: allowedIPString) { + ipv4IncludedRoutes.append(NEIPv4Route( + destinationAddress: "\(allowedIP.address)", + subnetMask: "\(allowedIP.subnetMask())")) + } + } - for excludeIPString in splitTunnelSites { - if let excludeIP = IPAddressRange(from: excludeIPString) { - ipv4ExcludedRoutes.append(NEIPv4Route( - destinationAddress: "\(excludeIP.address)", - subnetMask: "\(excludeIP.subnetMask())")) - } + networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes + } else { + if splitTunnelType == 2 { + var ipv4ExcludedRoutes = [NEIPv4Route]() + var ipv4IncludedRoutes = [NEIPv4Route]() + var ipv6IncludedRoutes = [NEIPv6Route]() + + guard let splitTunnelSites else { + completionHandler(NSError(domain: "Split tunnel sited not setted up", code: 0)) + return + } + + for excludeIPString in splitTunnelSites { + if let excludeIP = IPAddressRange(from: excludeIPString) { + ipv4ExcludedRoutes.append(NEIPv4Route( + destinationAddress: "\(excludeIP.address)", + subnetMask: "\(excludeIP.subnetMask())")) + } + } + + if let allIPv4 = IPAddressRange(from: "0.0.0.0/0") { + ipv4IncludedRoutes.append(NEIPv4Route( + destinationAddress: "\(allIPv4.address)", + subnetMask: "\(allIPv4.subnetMask())")) + } + if let allIPv6 = IPAddressRange(from: "::/0") { + ipv6IncludedRoutes.append(NEIPv6Route( + destinationAddress: "\(allIPv6.address)", + networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength))) + } + networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes + networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes + networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes + } } - if let allIPv4 = IPAddressRange(from: "0.0.0.0/0") { - ipv4IncludedRoutes.append(NEIPv4Route( - destinationAddress: "\(allIPv4.address)", - subnetMask: "\(allIPv4.subnetMask())")) - } - if let allIPv6 = IPAddressRange(from: "::/0") { - ipv6IncludedRoutes.append(NEIPv6Route( - destinationAddress: "\(allIPv6.address)", - networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength))) - } - networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes - networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes - networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes - } + // Set the network settings for the current tunneling session. + setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler) } - // Set the network settings for the current tunneling session. - setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler) - } + // Process events returned by the OpenVPN library + func openVPNAdapter( + _ openVPNAdapter: OpenVPNAdapter, + handleEvent event: OpenVPNAdapterEvent, + message: String?) { + switch event { + case .connected: + if reasserting { + reasserting = false + } - // Process events returned by the OpenVPN library - func openVPNAdapter( - _ openVPNAdapter: OpenVPNAdapter, - handleEvent event: OpenVPNAdapterEvent, - message: String?) { - switch event { - case .connected: - if reasserting { - reasserting = false + guard let startHandler = startHandler else { return } + + startHandler(nil) + self.startHandler = nil + case .disconnected: + guard let stopHandler = stopHandler else { return } + + if vpnReachability.isTracking { + vpnReachability.stopTracking() + } + + stopHandler() + self.stopHandler = nil + case .reconnecting: + reasserting = true + default: + break + } } - guard let startHandler = startHandler else { return } - - startHandler(nil) - self.startHandler = nil - case .disconnected: - guard let stopHandler = stopHandler else { return } + // Handle errors thrown by the OpenVPN library + func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) { + // Handle only fatal errors + guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool, + fatal == true else { return } if vpnReachability.isTracking { - vpnReachability.stopTracking() + vpnReachability.stopTracking() } - stopHandler() - self.stopHandler = nil - case .reconnecting: - reasserting = true - default: - break - } + if let startHandler { + startHandler(error) + self.startHandler = nil + } else { + cancelTunnelWithError(error) + } } - // Handle errors thrown by the OpenVPN library - func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) { - // Handle only fatal errors - guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool, - fatal == true else { return } - - if vpnReachability.isTracking { - vpnReachability.stopTracking() + // Use this method to process any log message returned by OpenVPN library. + func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) { + // Handle log messages + ovpnLog(.info, message: logMessage) } - - if let startHandler { - startHandler(error) - self.startHandler = nil - } else { - cancelTunnelWithError(error) - } - } - - // Use this method to process any log message returned by OpenVPN library. - func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) { - // Handle log messages - ovpnLog(.info, message: logMessage) - } } extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {} diff --git a/client/platforms/ios/PacketTunnelProvider+WireGuard.swift b/client/platforms/ios/PacketTunnelProvider+WireGuard.swift index 31f37aea..18200c7f 100644 --- a/client/platforms/ios/PacketTunnelProvider+WireGuard.swift +++ b/client/platforms/ios/PacketTunnelProvider+WireGuard.swift @@ -2,220 +2,186 @@ import Foundation import NetworkExtension extension PacketTunnelProvider { - func startWireguard(activationAttemptId: String?, - errorNotifier: ErrorNotifier, - completionHandler: @escaping (Error?) -> Void) { - guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol, - let providerConfiguration = protocolConfiguration.providerConfiguration, - let wgConfigData: Data = providerConfiguration[Constants.wireGuardConfigKey] as? Data else { - wg_log(.error, message: "Can't start, config missing") - completionHandler(nil) - return - } - - do { - let wgConfig = try JSONDecoder().decode(WGConfig.self, from: wgConfigData) - let wgConfigStr = wgConfig.str - wg_log(.info, title: "config: ", message: wgConfig.redux) - - let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: wgConfigStr) - - if tunnelConfiguration.peers.first!.allowedIPs - .map({ $0.stringRepresentation }) - .joined(separator: ", ") == "0.0.0.0/0, ::/0" { - if wgConfig.splitTunnelType == 1 { - for index in tunnelConfiguration.peers.indices { - tunnelConfiguration.peers[index].allowedIPs.removeAll() - var allowedIPs = [IPAddressRange]() - - for allowedIPString in wgConfig.splitTunnelSites { - if let allowedIP = IPAddressRange(from: allowedIPString) { - allowedIPs.append(allowedIP) - } - } - - tunnelConfiguration.peers[index].allowedIPs = allowedIPs - } - } else if wgConfig.splitTunnelType == 2 { - for index in tunnelConfiguration.peers.indices { - var excludeIPs = [IPAddressRange]() - - for excludeIPString in wgConfig.splitTunnelSites { - if let excludeIP = IPAddressRange(from: excludeIPString) { - excludeIPs.append(excludeIP) - } - } - - tunnelConfiguration.peers[index].excludeIPs = excludeIPs - } - } - } - - wg_log(.info, message: "Starting tunnel from the " + - (activationAttemptId == nil ? "OS directly, rather than the app" : "app")) - - // Start the tunnel - wgAdapter.start(tunnelConfiguration: tunnelConfiguration) { adapterError in - guard let adapterError else { - let interfaceName = self.wgAdapter.interfaceName ?? "unknown" - wg_log(.info, message: "Tunnel interface is \(interfaceName)") - completionHandler(nil) - return - } - - switch adapterError { - case .cannotLocateTunnelFileDescriptor: - wg_log(.error, staticMessage: "Starting tunnel failed: could not determine file descriptor") - errorNotifier.notify(PacketTunnelProviderError.couldNotDetermineFileDescriptor) - completionHandler(PacketTunnelProviderError.couldNotDetermineFileDescriptor) - case .dnsResolution(let dnsErrors): - let hostnamesWithDnsResolutionFailure = dnsErrors.map { $0.address } - .joined(separator: ", ") - wg_log(.error, message: - "DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure)") - errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure) - completionHandler(PacketTunnelProviderError.dnsResolutionFailure) - case .setNetworkSettings(let error): - wg_log(.error, message: - "Starting tunnel failed with setTunnelNetworkSettings returning \(error.localizedDescription)") - errorNotifier.notify(PacketTunnelProviderError.couldNotSetNetworkSettings) - completionHandler(PacketTunnelProviderError.couldNotSetNetworkSettings) - case .startWireGuardBackend(let errorCode): - wg_log(.error, message: "Starting tunnel failed with wgTurnOn returning \(errorCode)") - errorNotifier.notify(PacketTunnelProviderError.couldNotStartBackend) - completionHandler(PacketTunnelProviderError.couldNotStartBackend) - case .invalidState: - fatalError() - } - } - } catch { - wg_log(.error, message: "Can't parse WG config: \(error.localizedDescription)") - completionHandler(nil) - return - } - } - - func handleWireguardStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) { - guard let completionHandler = completionHandler else { return } - wgAdapter.getRuntimeConfiguration { settings in - var data: Data? - if let settings { - data = settings.data(using: .utf8)! - } - - let components = settings!.components(separatedBy: "\n") - - var settingsDictionary: [String: String] = [:] - for component in components { - let pair = component.components(separatedBy: "=") - if pair.count == 2 { - settingsDictionary[pair[0]] = pair[1] - } - } - - let response: [String: Any] = [ - "rx_bytes": settingsDictionary["rx_bytes"] ?? "0", - "tx_bytes": settingsDictionary["tx_bytes"] ?? "0" - ] - - completionHandler(try? JSONSerialization.data(withJSONObject: response, options: [])) - } - } - - private func handleWireguardAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) { - guard let completionHandler = completionHandler else { return } - if messageData.count == 1 && messageData[0] == 0 { - wgAdapter.getRuntimeConfiguration { settings in - var data: Data? - if let settings { - data = settings.data(using: .utf8)! - } - completionHandler(data) - } - } else if messageData.count >= 1 { - // Updates the tunnel configuration and responds with the active configuration - wg_log(.info, message: "Switching tunnel configuration") - guard let configString = String(data: messageData, encoding: .utf8) - else { - completionHandler(nil) - return - } - - do { - let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: configString) - wgAdapter.update(tunnelConfiguration: tunnelConfiguration) { error in - if let error { - wg_log(.error, message: "Failed to switch tunnel configuration: \(error.localizedDescription)") + func startWireguard(activationAttemptId: String?, + errorNotifier: ErrorNotifier, + completionHandler: @escaping (Error?) -> Void) { + guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol, + let providerConfiguration = protocolConfiguration.providerConfiguration, + let wgConfigData: Data = providerConfiguration[Constants.wireGuardConfigKey] as? Data else { + wg_log(.error, message: "Can't start, config missing") + completionHandler(nil) + return + } + + do { + let wgConfig = try JSONDecoder().decode(WGConfig.self, from: wgConfigData) + let wgConfigStr = wgConfig.str + wg_log(.info, title: "config: ", message: wgConfig.redux) + + let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: wgConfigStr) + + if tunnelConfiguration.peers.first!.allowedIPs + .map({ $0.stringRepresentation }) + .joined(separator: ", ") == "0.0.0.0/0, ::/0" { + if wgConfig.splitTunnelType == 1 { + for index in tunnelConfiguration.peers.indices { + tunnelConfiguration.peers[index].allowedIPs.removeAll() + var allowedIPs = [IPAddressRange]() + + for allowedIPString in wgConfig.splitTunnelSites { + if let allowedIP = IPAddressRange(from: allowedIPString) { + allowedIPs.append(allowedIP) + } + } + + tunnelConfiguration.peers[index].allowedIPs = allowedIPs + } + } else if wgConfig.splitTunnelType == 2 { + for index in tunnelConfiguration.peers.indices { + var excludeIPs = [IPAddressRange]() + + for excludeIPString in wgConfig.splitTunnelSites { + if let excludeIP = IPAddressRange(from: excludeIPString) { + excludeIPs.append(excludeIP) + } + } + + tunnelConfiguration.peers[index].excludeIPs = excludeIPs + } + } + } + + wg_log(.info, message: "Starting tunnel from the " + + (activationAttemptId == nil ? "OS directly, rather than the app" : "app")) + + // Start the tunnel + wgAdapter = WireGuardAdapter(with: self) { logLevel, message in + wg_log(logLevel.osLogLevel, message: message) + } + + wgAdapter?.start(tunnelConfiguration: tunnelConfiguration) { [weak self] adapterError in + guard let adapterError else { + let interfaceName = self?.wgAdapter?.interfaceName ?? "unknown" + wg_log(.info, message: "Tunnel interface is \(interfaceName)") + completionHandler(nil) + return + } + + switch adapterError { + case .cannotLocateTunnelFileDescriptor: + wg_log(.error, staticMessage: "Starting tunnel failed: could not determine file descriptor") + errorNotifier.notify(PacketTunnelProviderError.couldNotDetermineFileDescriptor) + completionHandler(PacketTunnelProviderError.couldNotDetermineFileDescriptor) + case .dnsResolution(let dnsErrors): + let hostnamesWithDnsResolutionFailure = dnsErrors.map { $0.address } + .joined(separator: ", ") + wg_log(.error, message: + "DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure)") + errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure) + completionHandler(PacketTunnelProviderError.dnsResolutionFailure) + case .setNetworkSettings(let error): + wg_log(.error, message: + "Starting tunnel failed with setTunnelNetworkSettings returning \(error.localizedDescription)") + errorNotifier.notify(PacketTunnelProviderError.couldNotSetNetworkSettings) + completionHandler(PacketTunnelProviderError.couldNotSetNetworkSettings) + case .startWireGuardBackend(let errorCode): + wg_log(.error, message: "Starting tunnel failed with wgTurnOn returning \(errorCode)") + errorNotifier.notify(PacketTunnelProviderError.couldNotStartBackend) + completionHandler(PacketTunnelProviderError.couldNotStartBackend) + case .invalidState: + fatalError() + } + } + } catch { + wg_log(.error, message: "Can't parse WG config: \(error.localizedDescription)") completionHandler(nil) return - } - - self.wgAdapter.getRuntimeConfiguration { settings in - var data: Data? - if let settings { - data = settings.data(using: .utf8)! - } - completionHandler(data) - } } - } catch { - completionHandler(nil) - } - } else { - completionHandler(nil) } - } - // private func startEmptyTunnel(completionHandler: @escaping (Error?) -> Void) { - // dispatchPrecondition(condition: .onQueue(dispatchQueue)) - // - // let emptyTunnelConfiguration = TunnelConfiguration( - // name: nil, - // interface: InterfaceConfiguration(privateKey: PrivateKey()), - // peers: [] - // ) - // - // wgAdapter.start(tunnelConfiguration: emptyTunnelConfiguration) { error in - // self.dispatchQueue.async { - // if let error { - // wg_log(.error, message: "Failed to start an empty tunnel") - // completionHandler(error) - // } else { - // wg_log(.info, message: "Started an empty tunnel") - // self.tunnelAdapterDidStart() - // } - // } - // } - // - // let settings = NETunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1") - // - // self.setTunnelNetworkSettings(settings) { error in - // completionHandler(error) - // } - // } + func handleWireguardStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) { + guard let completionHandler = completionHandler else { return } + wgAdapter?.getRuntimeConfiguration { settings in + let components = settings!.components(separatedBy: "\n") - // private func tunnelAdapterDidStart() { - // dispatchPrecondition(condition: .onQueue(dispatchQueue)) - // // ... - // } + var settingsDictionary: [String: String] = [:] + for component in components { + let pair = component.components(separatedBy: "=") + if pair.count == 2 { + settingsDictionary[pair[0]] = pair[1] + } + } - func stopWireguard(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { - wg_log(.info, message: "Stopping tunnel: reason: \(reason.description)") + let response: [String: Any] = [ + "rx_bytes": settingsDictionary["rx_bytes"] ?? "0", + "tx_bytes": settingsDictionary["tx_bytes"] ?? "0" + ] - wgAdapter.stop { error in - ErrorNotifier.removeLastErrorFile() + completionHandler(try? JSONSerialization.data(withJSONObject: response, options: [])) + } + } - if let error { - wg_log(.error, message: "Failed to stop WireGuard adapter: \(error.localizedDescription)") - } - completionHandler() + private func handleWireguardAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) { + guard let completionHandler = completionHandler else { return } + if messageData.count == 1 && messageData[0] == 0 { + wgAdapter?.getRuntimeConfiguration { settings in + var data: Data? + if let settings { + data = settings.data(using: .utf8)! + } + completionHandler(data) + } + } else if messageData.count >= 1 { + // Updates the tunnel configuration and responds with the active configuration + wg_log(.info, message: "Switching tunnel configuration") + guard let configString = String(data: messageData, encoding: .utf8) + else { + completionHandler(nil) + return + } + + do { + let tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: configString) + wgAdapter?.update(tunnelConfiguration: tunnelConfiguration) { [weak self] error in + if let error { + wg_log(.error, message: "Failed to switch tunnel configuration: \(error.localizedDescription)") + completionHandler(nil) + return + } + + self?.wgAdapter?.getRuntimeConfiguration { settings in + var data: Data? + if let settings { + data = settings.data(using: .utf8)! + } + completionHandler(data) + } + } + } catch { + completionHandler(nil) + } + } else { + completionHandler(nil) + } + } + + func stopWireguard(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + wg_log(.info, message: "Stopping tunnel: reason: \(reason.description)") + + wgAdapter?.stop { error in + ErrorNotifier.removeLastErrorFile() + + if let error { + wg_log(.error, message: "Failed to stop WireGuard adapter: \(error.localizedDescription)") + } + completionHandler() #if os(macOS) - // HACK: This is a filthy hack to work around Apple bug 32073323 (dup'd by us as 47526107). - // Remove it when they finally fix this upstream and the fix has been rolled out to - // sufficient quantities of users. - exit(0) + // HACK: This is a filthy hack to work around Apple bug 32073323 (dup'd by us as 47526107). + // Remove it when they finally fix this upstream and the fix has been rolled out to + // sufficient quantities of users. + exit(0) #endif + } } - } } diff --git a/client/platforms/ios/PacketTunnelProvider.swift b/client/platforms/ios/PacketTunnelProvider.swift index cac5f35b..f78db345 100644 --- a/client/platforms/ios/PacketTunnelProvider.swift +++ b/client/platforms/ios/PacketTunnelProvider.swift @@ -5,7 +5,7 @@ import Darwin import OpenVPNAdapter enum TunnelProtoType: String { - case wireguard, openvpn, shadowsocks, none + case wireguard, openvpn } struct Constants { @@ -34,160 +34,138 @@ struct Constants { } class PacketTunnelProvider: NEPacketTunnelProvider { - lazy var wgAdapter = { - WireGuardAdapter(with: self) { logLevel, message in - wg_log(logLevel.osLogLevel, message: message) - } - }() - - lazy var ovpnAdapter: OpenVPNAdapter = { - let adapter = OpenVPNAdapter() - adapter.delegate = self - return adapter - }() - - /// Internal queue. - private let dispatchQueue = DispatchQueue(label: "PacketTunnel", qos: .utility) - - var splitTunnelType: Int! - var splitTunnelSites: [String]! - - let vpnReachability = OpenVPNReachability() - - var startHandler: ((Error?) -> Void)? - var stopHandler: (() -> Void)? - var protoType: TunnelProtoType = .none - + var wgAdapter: WireGuardAdapter? + var ovpnAdapter: OpenVPNAdapter? + + var splitTunnelType: Int? + var splitTunnelSites: [String]? + + let vpnReachability = OpenVPNReachability() + + var startHandler: ((Error?) -> Void)? + var stopHandler: (() -> Void)? + var protoType: TunnelProtoType? + override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) { - guard let message = String(data: messageData, encoding: .utf8) else { - if let completionHandler { - completionHandler(nil) + guard let message = String(data: messageData, encoding: .utf8) else { + if let completionHandler { + completionHandler(nil) + } + return + } + + neLog(.info, title: "App said: ", message: message) + + guard let message = try? JSONSerialization.jsonObject(with: messageData, options: []) as? [String: Any] else { + neLog(.error, message: "Failed to serialize message from app") + return + } + + guard let completionHandler else { + neLog(.error, message: "Missing message completion handler") + return + } + + guard let action = message[Constants.kMessageKeyAction] as? String else { + neLog(.error, message: "Missing action key in app message") + completionHandler(nil) + return + } + + if action == Constants.kActionStatus { + handleStatusAppMessage(messageData, + completionHandler: completionHandler) } - return - } - - neLog(.info, title: "App said: ", message: message) - - guard let message = try? JSONSerialization.jsonObject(with: messageData, options: []) as? [String: Any] else { - neLog(.error, message: "Failed to serialize message from app") - return - } - - guard let completionHandler else { - neLog(.error, message: "Missing message completion handler") - return - } - - guard let action = message[Constants.kMessageKeyAction] as? String else { - neLog(.error, message: "Missing action key in app message") - completionHandler(nil) - return - } - - if action == Constants.kActionStatus { - handleStatusAppMessage(messageData, completionHandler: completionHandler) - } } - - override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) { - dispatchQueue.async { - let activationAttemptId = options?[Constants.kActivationAttemptId] as? String - let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId) - - neLog(.info, message: "Start tunnel") - - if let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol { - let providerConfiguration = protocolConfiguration.providerConfiguration - if (providerConfiguration?[Constants.ovpnConfigKey] as? Data) != nil { - self.protoType = .openvpn - } else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil { - self.protoType = .wireguard + + override func startTunnel(options: [String : NSObject]? = nil, + completionHandler: @escaping ((any Error)?) -> Void) { + let activationAttemptId = options?[Constants.kActivationAttemptId] as? String + let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId) + + neLog(.info, message: "Start tunnel") + + if let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol { + let providerConfiguration = protocolConfiguration.providerConfiguration + if (providerConfiguration?[Constants.ovpnConfigKey] as? Data) != nil { + protoType = .openvpn + } else if (providerConfiguration?[Constants.wireGuardConfigKey] as? Data) != nil { + protoType = .wireguard + } + } + + guard let protoType else { + let error = NSError(domain: "Protocol is not selected", code: 0) + completionHandler(error) + return + } + + switch protoType { + case .wireguard: + startWireguard(activationAttemptId: activationAttemptId, + errorNotifier: errorNotifier, + completionHandler: completionHandler) + case .openvpn: + startOpenVPN(completionHandler: completionHandler) } - } else { - self.protoType = .none - } - - switch self.protoType { - case .wireguard: - self.startWireguard(activationAttemptId: activationAttemptId, - errorNotifier: errorNotifier, - completionHandler: completionHandler) - case .openvpn: - self.startOpenVPN(completionHandler: completionHandler) - case .shadowsocks: - break - // startShadowSocks(completionHandler: completionHandler) - case .none: - break - } } - } + - override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { - dispatchQueue.async { - switch self.protoType { - case .wireguard: - self.stopWireguard(with: reason, completionHandler: completionHandler) - case .openvpn: - self.stopOpenVPN(with: reason, completionHandler: completionHandler) - case .shadowsocks: - break - // stopShadowSocks(with: reason, completionHandler: completionHandler) - case .none: - break - } + override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + guard let protoType else { + completionHandler() + return + } + + switch protoType { + case .wireguard: + stopWireguard(with: reason, + completionHandler: completionHandler) + case .openvpn: + stopOpenVPN(with: reason, + completionHandler: completionHandler) + } } - } - func handleStatusAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) { - switch protoType { - case .wireguard: - handleWireguardStatusMessage(messageData, completionHandler: completionHandler) - case .openvpn: - handleOpenVPNStatusMessage(messageData, completionHandler: completionHandler) - case .shadowsocks: - break - // handleShadowSocksAppMessage(messageData, completionHandler: completionHandler) - case .none: - break + func handleStatusAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) { + guard let protoType else { + completionHandler?(nil) + return + } + + switch protoType { + case .wireguard: + handleWireguardStatusMessage(messageData, completionHandler: completionHandler) + case .openvpn: + handleOpenVPNStatusMessage(messageData, completionHandler: completionHandler) + } } - } - // MARK: Network observing methods - - private func startListeningForNetworkChanges() { - stopListeningForNetworkChanges() - addObserver(self, forKeyPath: Constants.kDefaultPathKey, options: .old, context: nil) - } - - private func stopListeningForNetworkChanges() { - removeObserver(self, forKeyPath: Constants.kDefaultPathKey) - } - - override func observeValue(forKeyPath keyPath: String?, - of object: Any?, - change: [NSKeyValueChangeKey: Any]?, - context: UnsafeMutableRawPointer?) { - guard Constants.kDefaultPathKey != keyPath else { return } - // Since iOS 11, we have observed that this KVO event fires repeatedly when connecting over Wifi, - // even though the underlying network has not changed (i.e. `isEqualToPath` returns false), - // leading to "wakeup crashes" due to excessive network activity. Guard against false positives by - // comparing the paths' string description, which includes properties not exposed by the class - guard let lastPath: NWPath = change?[.oldKey] as? NWPath, - let defPath = defaultPath, - lastPath != defPath || lastPath.description != defPath.description else { - return + // MARK: Network observing methods + override func observeValue(forKeyPath keyPath: String?, + of object: Any?, + change: [NSKeyValueChangeKey: Any]?, + context: UnsafeMutableRawPointer?) { + guard Constants.kDefaultPathKey != keyPath else { return } + // Since iOS 11, we have observed that this KVO event fires repeatedly when connecting over Wifi, + // even though the underlying network has not changed (i.e. `isEqualToPath` returns false), + // leading to "wakeup crashes" due to excessive network activity. Guard against false positives by + // comparing the paths' string description, which includes properties not exposed by the class + guard let lastPath: NWPath = change?[.oldKey] as? NWPath, + let defPath = defaultPath, + lastPath != defPath || lastPath.description != defPath.description else { + return + } + DispatchQueue.main.async { [weak self] in + guard let self, self.defaultPath != nil else { return } + self.handle(networkChange: self.defaultPath!) { _ in } + } } - DispatchQueue.main.async { [weak self] in - guard let self, self.defaultPath != nil else { return } - self.handle(networkChange: self.defaultPath!) { _ in } - } - } - private func handle(networkChange changePath: NWPath, completion: @escaping (Error?) -> Void) { - wg_log(.info, message: "Tunnel restarted.") - startTunnel(options: nil, completionHandler: completion) - } + private func handle(networkChange changePath: NWPath, completion: @escaping (Error?) -> Void) { + wg_log(.info, message: "Tunnel restarted.") + startTunnel(options: nil, completionHandler: completion) + } } extension WireGuardLogLevel {