diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-02-08 13:41:25 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-03-17 09:46:29 +0100 |
| commit | 024715100b1170d2bd73a13c6ee6e03745f4d3da (patch) | |
| tree | f6611bdd1651af841adc1e73a9d76ced2dda419e | |
| parent | 078a0a8d436929ba0290f87e8d772668ff352add (diff) | |
| download | mullvadvpn-024715100b1170d2bd73a13c6ee6e03745f4d3da.tar.xz mullvadvpn-024715100b1170d2bd73a13c6ee6e03745f4d3da.zip | |
Add tunnel monitor
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 14 | ||||
| -rw-r--r-- | ios/MullvadVPN/Logging/Logging.swift | 8 | ||||
| -rw-r--r-- | ios/PacketTunnel/PacketTunnelProvider.swift | 484 | ||||
| -rw-r--r-- | ios/PacketTunnel/Pinger.swift | 257 | ||||
| -rw-r--r-- | ios/PacketTunnel/TunnelMonitor.swift | 312 | ||||
| -rw-r--r-- | ios/PacketTunnel/TunnelMonitorConfiguration.swift | 23 |
6 files changed, 925 insertions, 173 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 1b6b6d4243..49ce76af67 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -91,6 +91,7 @@ 582CFEE726945FC30072883A /* AppStoreSubscriptions.strings in Resources */ = {isa = PBXBuildFile; fileRef = 582CFEE526945FC30072883A /* AppStoreSubscriptions.strings */; }; 582CFEEA269463B80072883A /* Settings.strings in Resources */ = {isa = PBXBuildFile; fileRef = 582CFEE8269463B80072883A /* Settings.strings */; }; 5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5835B7CB233B76CB0096D79F /* TunnelManager.swift */; }; + 5838318B27C40A3900000571 /* Pinger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838318A27C40A3900000571 /* Pinger.swift */; }; 583DA21425FA4B5C00318683 /* LocationDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583DA21325FA4B5C00318683 /* LocationDataSource.swift */; }; 5840250122B1124600E4CFEC /* IPAddress+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250022B1124600E4CFEC /* IPAddress+Codable.swift */; }; 5840250222B1124600E4CFEC /* IPAddress+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250022B1124600E4CFEC /* IPAddress+Codable.swift */; }; @@ -160,6 +161,8 @@ 5860392A26DCE7AB00554C79 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; }; 5860392B26DCEE6300554C79 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; }; 5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */; }; + 58655DCE27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58655DCD27DA0A5D00911834 /* TunnelMonitorConfiguration.swift */; }; + 58655DCF27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58655DCD27DA0A5D00911834 /* TunnelMonitorConfiguration.swift */; }; 5868585524054096000B8131 /* AppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868585424054096000B8131 /* AppButton.swift */; }; 5868BD33261DCD2600E6027F /* CustomSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868BD32261DCD2600E6027F /* CustomSplitViewController.swift */; }; 586ADD4723FC13F400CE9E87 /* countries.geo.json in Resources */ = {isa = PBXBuildFile; fileRef = 586ADD4523FC13F400CE9E87 /* countries.geo.json */; }; @@ -320,6 +323,7 @@ 58FB865E26EA284E00F188BC /* LogFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865D26EA284E00F188BC /* LogFormatting.swift */; }; 58FB865F26EA2E6D00F188BC /* LogFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865D26EA284E00F188BC /* LogFormatting.swift */; }; 58FB866126EB678000F188BC /* TunnelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB866026EB677F00F188BC /* TunnelInfo.swift */; }; + 58FC040A27B3EE03001C21F0 /* TunnelMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FC040927B3EE03001C21F0 /* TunnelMonitor.swift */; }; 58FD5BE724192A2C00112C88 /* AppStoreReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BE624192A2B00112C88 /* AppStoreReceipt.swift */; }; 58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */; }; 58FD5BF22424F7D700112C88 /* UserInterfaceInteractionRestriction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BF12424F7D700112C88 /* UserInterfaceInteractionRestriction.swift */; }; @@ -420,6 +424,7 @@ 582CFEE626945FC30072883A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AppStoreSubscriptions.strings; sourceTree = "<group>"; }; 582CFEE9269463B80072883A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Settings.strings; sourceTree = "<group>"; }; 5835B7CB233B76CB0096D79F /* TunnelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManager.swift; sourceTree = "<group>"; }; + 5838318A27C40A3900000571 /* Pinger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pinger.swift; sourceTree = "<group>"; }; 583DA21325FA4B5C00318683 /* LocationDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDataSource.swift; sourceTree = "<group>"; }; 5840250022B1124600E4CFEC /* IPAddress+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IPAddress+Codable.swift"; sourceTree = "<group>"; }; 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadEndpoint.swift; sourceTree = "<group>"; }; @@ -460,6 +465,7 @@ 585DA8AE26B9492500B8C587 /* Promise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Promise.swift; sourceTree = "<group>"; }; 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromiseCompletion.swift; sourceTree = "<group>"; }; 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslucentButtonBlurView.swift; sourceTree = "<group>"; }; + 58655DCD27DA0A5D00911834 /* TunnelMonitorConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitorConfiguration.swift; sourceTree = "<group>"; }; 5866F39B2243B82D00168AE5 /* MullvadVPN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MullvadVPN.entitlements; sourceTree = "<group>"; }; 5868585424054096000B8131 /* AppButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppButton.swift; sourceTree = "<group>"; }; 5868BD32261DCD2600E6027F /* CustomSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSplitViewController.swift; sourceTree = "<group>"; }; @@ -601,6 +607,7 @@ 58FB865926EA214400F188BC /* RelayCacheObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheObserver.swift; sourceTree = "<group>"; }; 58FB865D26EA284E00F188BC /* LogFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogFormatting.swift; sourceTree = "<group>"; }; 58FB866026EB677F00F188BC /* TunnelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelInfo.swift; sourceTree = "<group>"; }; + 58FC040927B3EE03001C21F0 /* TunnelMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitor.swift; sourceTree = "<group>"; }; 58FD5BE624192A2B00112C88 /* AppStoreReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreReceipt.swift; sourceTree = "<group>"; }; 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProduct+Formatting.swift"; sourceTree = "<group>"; }; 58FD5BF12424F7D700112C88 /* UserInterfaceInteractionRestriction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInterfaceInteractionRestriction.swift; sourceTree = "<group>"; }; @@ -992,6 +999,9 @@ 58CE5E7D224146470008646E /* Info.plist */, 58CE5E7E224146470008646E /* PacketTunnel.entitlements */, 58CE5E7B224146470008646E /* PacketTunnelProvider.swift */, + 5838318A27C40A3900000571 /* Pinger.swift */, + 58FC040927B3EE03001C21F0 /* TunnelMonitor.swift */, + 58655DCD27DA0A5D00911834 /* TunnelMonitorConfiguration.swift */, ); path = PacketTunnel; sourceTree = "<group>"; @@ -1395,6 +1405,7 @@ 58FB865A26EA214400F188BC /* RelayCacheObserver.swift in Sources */, 5806766C27048E3E00C858CB /* AnyOptional.swift in Sources */, 58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */, + 58655DCE27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */, 5850367F25A481D800A43E93 /* IPAddressRange+Codable.swift in Sources */, 58F2E14C276A61C000A79513 /* ReplaceKeyOperation.swift in Sources */, 5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */, @@ -1562,6 +1573,8 @@ 58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */, 582AD44127BE6178002A6BFC /* CodingErrors+ChainedError.swift in Sources */, 5840250222B1124600E4CFEC /* IPAddress+Codable.swift in Sources */, + 58FC040A27B3EE03001C21F0 /* TunnelMonitor.swift in Sources */, + 5838318B27C40A3900000571 /* Pinger.swift in Sources */, 5820675C26E6576800655B05 /* RelayCache.swift in Sources */, 58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */, 585DA89A26B0329200B8C587 /* TunnelConnectionInfo.swift in Sources */, @@ -1578,6 +1591,7 @@ 5815039824D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */, 5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */, 58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */, + 58655DCF27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */, 5815039E24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */, 585DA87826B024A900B8C587 /* CachedRelays.swift in Sources */, 584E96BD240FD4DA00D3334F /* Location.swift in Sources */, diff --git a/ios/MullvadVPN/Logging/Logging.swift b/ios/MullvadVPN/Logging/Logging.swift index b96d9dc08d..fe5b65b433 100644 --- a/ios/MullvadVPN/Logging/Logging.swift +++ b/ios/MullvadVPN/Logging/Logging.swift @@ -9,7 +9,7 @@ import Foundation import Logging -func initLoggingSystem(bundleIdentifier: String) { +func initLoggingSystem(bundleIdentifier: String, metadata: Logger.Metadata? = nil) { let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier)! let logsDirectoryURL = containerURL.appendingPathComponent("Logs", isDirectory: true) let logFileName = "\(bundleIdentifier).log" @@ -44,7 +44,11 @@ func initLoggingSystem(bundleIdentifier: String) { if logHandlers.isEmpty { return SwiftLogNoOpLogHandler() } else { - return MultiplexLogHandler(logHandlers) + var multiplex = MultiplexLogHandler(logHandlers) + if let metadata = metadata { + multiplex.metadata = metadata + } + return multiplex } } diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index b9f645eb9d..9c67b084a9 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -12,146 +12,208 @@ import NetworkExtension import Logging import WireGuardKit -class PacketTunnelProvider: NEPacketTunnelProvider { +class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { - /// Tunnel provider logger + /// Tunnel provider logger. private let providerLogger: Logger - /// WireGuard adapter logger + /// WireGuard adapter logger. private let tunnelLogger: Logger - /// Internal queue + /// Internal queue. private let dispatchQueue = DispatchQueue(label: "PacketTunnel", qos: .utility) - /// WireGuard adapter - private lazy var adapter: WireGuardAdapter = { - return WireGuardAdapter(with: self, logHandler: { [weak self] (logLevel, message) in - self?.dispatchQueue.async { - self?.tunnelLogger.log(level: logLevel.loggerLevel, "\(message)") - } - }) - }() + /// WireGuard adapter. + private var adapter: WireGuardAdapter! + + /// Raised once tunnel establishes connection in the very first time, before calling the system + /// completion handler passed into `startTunnel`. + private var isConnected = false - /// Tunnel connection info + /// A system completion handler passed from startTunnel and saved for later use once the + /// connection is established. + private var startTunnelCompletionHandler: ((PacketTunnelProviderError?) -> Void)? + + /// A completion handler passed during reassertion and saved for later use once the connection + /// is reestablished. + private var reassertTunnelCompletionHandler: ((PacketTunnelProviderError?) -> Void)? + + /// Tunnel monitor. + private var tunnelMonitor: TunnelMonitor! + + /// Tunnel connection info. private var tunnelConnectionInfo: TunnelConnectionInfo? { didSet { if let tunnelConnectionInfo = tunnelConnectionInfo { - self.providerLogger.debug("Set tunnel relay to \(tunnelConnectionInfo.hostname)") + self.providerLogger.debug("Set tunnel relay to \(tunnelConnectionInfo.hostname).") } else { - self.providerLogger.debug("Unset tunnel relay") + self.providerLogger.debug("Unset tunnel relay.") } } } override init() { - initLoggingSystem(bundleIdentifier: Bundle.main.bundleIdentifier!) + let pid = ProcessInfo.processInfo.processIdentifier - var providerLogger = Logger(label: "PacketTunnelProvider") - var tunnelLogger = Logger(label: "WireGuard") + var metadata = Logger.Metadata() + metadata["pid"] = .string("\(pid)") - let pid = ProcessInfo.processInfo.processIdentifier - providerLogger[metadataKey: "pid"] = .stringConvertible(pid) - tunnelLogger[metadataKey: "pid"] = .stringConvertible(pid) + initLoggingSystem(bundleIdentifier: Bundle.main.bundleIdentifier!, metadata: metadata) + + providerLogger = Logger(label: "PacketTunnelProvider") + tunnelLogger = Logger(label: "WireGuard") + + super.init() + + adapter = WireGuardAdapter(with: self, shouldHandleReasserting: false, logHandler: { [weak self] (logLevel, message) in + self?.dispatchQueue.async { + self?.tunnelLogger.log(level: logLevel.loggerLevel, "\(message)") + } + }) - self.providerLogger = providerLogger - self.tunnelLogger = tunnelLogger + tunnelMonitor = TunnelMonitor(queue: dispatchQueue, adapter: adapter) + tunnelMonitor.delegate = self } override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { let tunnelOptions = PacketTunnelOptions(rawOptions: options ?? [:]) - let appSelectorResult: Result<RelaySelectorResult?, Error> = Result { try tunnelOptions.getSelectorResult() } + var appSelectorResult: RelaySelectorResult? - switch appSelectorResult { - case .success(.some(let selectorResult)): - providerLogger.debug("Start the tunnel via app, connect to \(selectorResult.tunnelConnectionInfo.hostname)") + // Parse relay selector from tunnel options. + do { + appSelectorResult = try tunnelOptions.getSelectorResult() - case .success(nil): - if tunnelOptions.isOnDemand() { - providerLogger.debug("Start the tunnel via on-demand rule") - } else { - providerLogger.debug("Start the tunnel via system") - } + switch appSelectorResult { + case .some(let selectorResult): + providerLogger.debug("Start the tunnel via app, connect to \(selectorResult.tunnelConnectionInfo.hostname).") - case .failure(let error): - providerLogger.debug("Start the tunnel via app") + case .none: + if tunnelOptions.isOnDemand() { + providerLogger.debug("Start the tunnel via on-demand rule.") + } else { + providerLogger.debug("Start the tunnel via system.") + } + } + } catch { + providerLogger.debug("Start the tunnel via app.") providerLogger.error( chainedError: AnyChainedError(error), message: "Failed to decode relay selector result passed from the app. Will continue by picking new relay." ) } - makeConfiguration(appSelectorResult.flattenValue) - .asPromise() - .receive(on: dispatchQueue) - .mapThen { tunnelConfiguration in - let tunnelConnectionInfo = tunnelConfiguration.selectorResult.tunnelConnectionInfo - self.tunnelConnectionInfo = tunnelConnectionInfo + // Read tunnel configuration. + let tunnelConfiguration: PacketTunnelConfiguration + switch makeConfiguration(appSelectorResult) { + case .success(let configuration): + tunnelConfiguration = configuration + + case .failure(let error): + providerLogger.error(chainedError: error, message: "Failed to start the tunnel.") + completionHandler(error) + return + } + + // Set tunnel connection info. + dispatchQueue.async { + self.tunnelConnectionInfo = tunnelConfiguration.selectorResult.tunnelConnectionInfo + } + + // Start tunnel. + adapter.start(tunnelConfiguration: tunnelConfiguration.wgTunnelConfig) { error in + self.dispatchQueue.async { + if let error = error { + let tunnelProviderError = PacketTunnelProviderError.startWireguardAdapter(error) + self.providerLogger.error(chainedError: tunnelProviderError, message: "Failed to start the tunnel.") - return self.adapter.start(tunnelConfiguration: tunnelConfiguration.wgTunnelConfig) - .mapError { error in - return PacketTunnelProviderError.startWireguardAdapter(error) + completionHandler(tunnelProviderError) + } else { + self.providerLogger.debug("Started the tunnel.") + + // Store completion handler and call it from TunnelMonitorDelegate once + // the connection is established. + self.startTunnelCompletionHandler = { [weak self] error in + // Mark the tunnel connected. + self?.isConnected = true + + // Call system completion handler. + completionHandler(error) } - .receive(on: self.dispatchQueue) - } - .onSuccess { - self.providerLogger.debug("Started the tunnel") - } - .onFailure { error in - self.providerLogger.error(chainedError: error, message: "Failed to start the tunnel") - } - .observe { completion in - completionHandler(completion.unwrappedValue?.error) + + // Start tunnel monitor. + let gatewayAddress = tunnelConfiguration.selectorResult.endpoint.ipv4Gateway + self.tunnelMonitor.start(address: gatewayAddress) + } } + } } override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { providerLogger.debug("Stop the tunnel: \(reason)") - adapter.stop() - .receive(on: self.dispatchQueue) - .mapError { error in - return PacketTunnelProviderError.stopWireguardAdapter(error) - } - .onFailure { error in - self.providerLogger.error(chainedError: error, message: "Failed to stop the tunnel gracefully") - } - .observe { _ in - self.providerLogger.debug("Stopped the tunnel") + dispatchQueue.async { + // Stop tunnel monitor. + self.tunnelMonitor.stop() + + // Unset the start tunnel completion handler. + self.startTunnelCompletionHandler = nil + } + + adapter.stop { error in + self.dispatchQueue.async { + let tunnelProviderError = error.map { PacketTunnelProviderError.stopWireguardAdapter($0) } + + if let tunnelProviderError = tunnelProviderError { + self.providerLogger.error(chainedError: tunnelProviderError, message: "Failed to stop the tunnel gracefully.") + } + + self.providerLogger.debug("Stopped the tunnel.") completionHandler() } + } } override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { - Result { try TunnelIPC.Coding.decodeRequest(messageData) } - .mapError { PacketTunnelProviderError.ipcHandler($0) } - .asPromise() - .onFailure { error in - self.providerLogger.error(chainedError: error, message: "Failed to decode the app message request") + dispatchQueue.async { + let request: TunnelIPC.Request + do { + request = try TunnelIPC.Coding.decodeRequest(messageData) + } catch { + self.providerLogger.error(chainedError: AnyChainedError(error), message: "Failed to decode the app message request.") + + completionHandler?(nil) + return } - .receive(on: dispatchQueue) - .mapThen { request -> Result<Data?, PacketTunnelProviderError>.Promise in - self.providerLogger.debug("handleAppMessage: \(request)") - switch request { - case .reloadTunnelSettings: - self.reloadTunnelSettings().observe { _ in } - return .success(nil) + self.providerLogger.debug("Received app message: \(request)") - case .tunnelConnectionInfo: - return Result { try TunnelIPC.Coding.encodeResponse(self.tunnelConnectionInfo) } - .mapError { PacketTunnelProviderError.ipcHandler($0) } - .map { data -> Data? in - return .some(data) - } - .flatMapError { error in - self.providerLogger.error(chainedError: error, message: "Failed to encode the app message response for \(request)") - return .success(nil) - } - .asPromise() + switch request { + case .reloadTunnelSettings: + self.providerLogger.debug("Reloading tunnel settings...") + + self.reloadTunnelSettings { [weak self] error in + guard let self = self else { return } + + if let error = error { + self.providerLogger.error(chainedError: error, message: "Failed to reload tunnel settings.") + } else { + self.providerLogger.debug("Reloaded tunnel settings.") + } } - }.observe { completion in - completionHandler?(completion.unwrappedValue?.value ?? nil) + + completionHandler?(nil) + + case .tunnelConnectionInfo: + var response: Data? + do { + response = try TunnelIPC.Coding.encodeResponse(self.tunnelConnectionInfo) + } catch { + self.providerLogger.error(chainedError: AnyChainedError(error), message: "Failed to encode the app message response for \(request)") + } + + completionHandler?(response) } + } } override func sleep(completionHandler: @escaping () -> Void) { @@ -163,53 +225,166 @@ class PacketTunnelProvider: NEPacketTunnelProvider { // Add code here to wake up. } + // MARK: - TunnelMonitor + + func tunnelMonitorDidDetermineConnectionEstablished(_ tunnelMonitor: TunnelMonitor) { + dispatchPrecondition(condition: .onQueue(dispatchQueue)) + + providerLogger.debug("Connection established.") + + startTunnelCompletionHandler?(nil) + startTunnelCompletionHandler = nil + + reassertTunnelCompletionHandler?(nil) + reassertTunnelCompletionHandler = nil + } + + func tunnelMonitorDelegateShouldHandleConnectionRecovery(_ tunnelMonitor: TunnelMonitor) { + dispatchPrecondition(condition: .onQueue(dispatchQueue)) + + providerLogger.debug("Recover connection. Picking next relay...") + + let handleRecoveryFailure = { (_ error: PacketTunnelProviderError) in + // Stop tunnel monitor. + tunnelMonitor.stop() + + // Call start tunnel completion handler with error. + self.startTunnelCompletionHandler?(error) + + // Reset start tunnel completion handler. + self.startTunnelCompletionHandler = nil + + // Call tunnel reassertion completion handler with error. + self.reassertTunnelCompletionHandler?(error) + + // Reset tunnel reassertion completion handler. + self.reassertTunnelCompletionHandler = nil + } + + // Read tunnel configuration. + let tunnelConfiguration: PacketTunnelConfiguration + switch makeConfiguration(nil) { + case .success(let configuration): + tunnelConfiguration = configuration + + case .failure(let error): + handleRecoveryFailure(error) + return + } + + // Set tunnel connection info. + self.tunnelConnectionInfo = tunnelConfiguration.selectorResult.tunnelConnectionInfo + + // Update WireGuard configuration. + adapter.update(tunnelConfiguration: tunnelConfiguration.wgTunnelConfig) { error in + self.dispatchQueue.async { + if let error = error { + handleRecoveryFailure(.updateWireguardConfiguration(error)) + } + } + } + } + // MARK: - Private private func makeConfiguration(_ appSelectorResult: RelaySelectorResult? = nil) -> Result<PacketTunnelConfiguration, PacketTunnelProviderError> { - return protocolConfiguration.passwordReference.map { data in - return Self.readTunnelSettings(keychainReference: data) - .flatMap { tunnelSettings in - return (appSelectorResult.map { .success($0) } ?? Self.selectRelayEndpoint(relayConstraints: tunnelSettings.relayConstraints)) - .map { (selectorResult) -> PacketTunnelConfiguration in - return PacketTunnelConfiguration( - tunnelSettings: tunnelSettings, - selectorResult: selectorResult - ) - } - } - } ?? .failure(.missingKeychainConfigurationReference) + guard let passwordRef = protocolConfiguration.passwordReference else { + return .failure(.missingKeychainConfigurationReference) + } + + let keychainEntry: TunnelSettingsManager.KeychainEntry + switch TunnelSettingsManager.load(searchTerm: .persistentReference(passwordRef)) { + case .success(let entry): + keychainEntry = entry + case .failure(let error): + return .failure(.cannotReadTunnelSettings(error)) + } + + let selectorResult: RelaySelectorResult + if let appSelectorResult = appSelectorResult { + selectorResult = appSelectorResult + } else { + let relayConstraints = keychainEntry.tunnelSettings.relayConstraints + switch Self.selectRelayEndpoint(relayConstraints: relayConstraints) { + case .success(let value): + selectorResult = value + case .failure(let error): + return .failure(error) + } + } + + let tunnelConfiguration = PacketTunnelConfiguration( + tunnelSettings: keychainEntry.tunnelSettings, + selectorResult: selectorResult + ) + + return .success(tunnelConfiguration) } - private func reloadTunnelSettings() -> Result<(), PacketTunnelProviderError>.Promise { - providerLogger.debug("Reload tunnel settings") + private func reloadTunnelSettings(completionHandler: @escaping (PacketTunnelProviderError?) -> Void) { + dispatchPrecondition(condition: .onQueue(dispatchQueue)) - return makeConfiguration() - .asPromise() - .mapThen { packetTunnelConfig in - let tunnelConnectionInfo = packetTunnelConfig.selectorResult.tunnelConnectionInfo - let oldTunnelConnectionInfo = self.tunnelConnectionInfo - self.tunnelConnectionInfo = tunnelConnectionInfo + // Read tunnel configuration. + let tunnelConfiguration: PacketTunnelConfiguration + switch makeConfiguration(nil) { + case .success(let configuration): + tunnelConfiguration = configuration - return self.adapter.update(tunnelConfiguration: packetTunnelConfig.wgTunnelConfig) - .receive(on: self.dispatchQueue) - .mapError { error in - return PacketTunnelProviderError.updateWireguardConfiguration(error) - } - .onSuccess { _ in - self.providerLogger.debug("Updated WireGuard configuration") + case .failure(let error): + completionHandler(error) + return + } + + // Set tunnel connection info. + let tunnelConnectionInfo = tunnelConfiguration.selectorResult.tunnelConnectionInfo + let oldTunnelConnectionInfo = self.tunnelConnectionInfo + self.tunnelConnectionInfo = tunnelConnectionInfo + + // Raise reasserting flag, but only if tunnel has already moved to connected state once. + // Otherwise keep the app in connecting state until it manages to establish the very first + // connection. + if isConnected { + reasserting = true + } + + // Update WireGuard configuration. + adapter.update(tunnelConfiguration: tunnelConfiguration.wgTunnelConfig) { error in + self.dispatchQueue.async { + // Reset previously stored completion handler. + self.reassertTunnelCompletionHandler = nil + + // Call completion handler immediately on error to update adapter configuration. + if let error = error { + // Revert to previously used tunnel connection info. + self.tunnelConnectionInfo = oldTunnelConnectionInfo + + // Lower the reasserting flag. + if self.isConnected { + self.reasserting = false } - .onFailure { error in - self.tunnelConnectionInfo = oldTunnelConnectionInfo - self.providerLogger.error(chainedError: error) + + // Call completion handler immediately. + completionHandler(.updateWireguardConfiguration(error)) + } else { + // Store completion handler and call it from TunnelMonitorDelegate once + // the connection is established. + self.reassertTunnelCompletionHandler = { [weak self] providerError in + guard let self = self else { return } + + // Lower the reasserting flag. + if self.isConnected { + self.reasserting = false + } + + completionHandler(providerError) } - } - } - /// Read tunnel settings from Keychain - private class func readTunnelSettings(keychainReference: Data) -> Result<TunnelSettings, PacketTunnelProviderError> { - return TunnelSettingsManager.load(searchTerm: .persistentReference(keychainReference)) - .mapError { PacketTunnelProviderError.cannotReadTunnelSettings($0) } - .map { $0.tunnelSettings } + // Restart tunnel monitor. + let gatewayAddress = tunnelConfiguration.selectorResult.endpoint.ipv4Gateway + self.tunnelMonitor.start(address: gatewayAddress) + } + } + } } /// Load relay cache with potential networking to refresh the cache and pick the relay for the @@ -219,10 +394,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider { let prebundledRelaysURL = RelayCache.IO.preBundledRelaysFileURL! return RelayCache.IO.readWithFallback(cacheFileURL: cacheFileURL, preBundledRelaysFileURL: prebundledRelaysURL) - .mapError { relayCacheError -> PacketTunnelProviderError in - return .readRelayCache(relayCacheError) + .mapError { error in + return PacketTunnelProviderError.readRelayCache(error) } - .flatMap { cachedRelayList -> Result<RelaySelectorResult, PacketTunnelProviderError> in + .flatMap { cachedRelayList in if let selectorResult = RelaySelector.evaluate(relays: cachedRelayList.relays, constraints: relayConstraints) { return .success(selectorResult) } else { @@ -254,34 +429,28 @@ enum PacketTunnelProviderError: ChainedError { /// Failure to update the Wireguard configuration case updateWireguardConfiguration(WireGuardAdapterError) - /// IPC handler failure - case ipcHandler(Error) - var errorDescription: String? { switch self { case .readRelayCache: - return "Failure to read the relay cache" + return "Failure to read the relay cache." case .noRelaySatisfyingConstraint: - return "No relay satisfying the given constraint" + return "No relay satisfying the given constraint." case .missingKeychainConfigurationReference: - return "Keychain configuration reference is not set on protocol configuration" + return "Keychain configuration reference is not set on protocol configuration." case .cannotReadTunnelSettings: - return "Failure to read tunnel settings" + return "Failure to read tunnel settings." case .startWireguardAdapter: - return "Failure to start the WireGuard adapter" + return "Failure to start the WireGuard adapter." case .stopWireguardAdapter: - return "Failure to stop the WireGuard adapter" + return "Failure to stop the WireGuard adapter." case .updateWireguardConfiguration: - return "Failure to update the Wireguard configuration" - - case .ipcHandler: - return "Failure to handle the IPC request" + return "Failure to update the Wireguard configuration." } } } @@ -347,32 +516,6 @@ extension WireGuardLogLevel { } } -extension WireGuardAdapter { - func start(tunnelConfiguration: TunnelConfiguration) -> Result<(), WireGuardAdapterError>.Promise { - return Result<(), WireGuardAdapterError>.Promise { resolver in - self.start(tunnelConfiguration: tunnelConfiguration) { error in - resolver.resolve(value: error.map { .failure($0) } ?? .success(())) - } - } - } - - func stop() -> Result<(), WireGuardAdapterError>.Promise { - return Result<(), WireGuardAdapterError>.Promise { resolver in - self.stop { error in - resolver.resolve(value: error.map { .failure($0) } ?? .success(())) - } - } - } - - func update(tunnelConfiguration: TunnelConfiguration) -> Result<(), WireGuardAdapterError>.Promise { - return Result<(), WireGuardAdapterError>.Promise { resolver in - self.update(tunnelConfiguration: tunnelConfiguration) { error in - resolver.resolve(value: error.map { .failure($0) } ?? .success(())) - } - } - } -} - extension WireGuardAdapterError: LocalizedError { public var errorDescription: String? { switch self { @@ -393,10 +536,10 @@ extension WireGuardAdapterError: LocalizedError { return "Failure to resolve endpoints:\n\(detailedErrorDescription)" case .setNetworkSettings: - return "Failure to set network settings" + return "Failure to set network settings." case .startWireGuardBackend(let code): - return "Failure to start WireGuard backend (error code: \(code))" + return "Failure to start WireGuard backend (error code: \(code))." } } } @@ -412,4 +555,3 @@ extension MullvadEndpoint { return Endpoint(host: .ipv6(ipv6Relay.ip), port: .init(integerLiteral: ipv6Relay.port)) } } - diff --git a/ios/PacketTunnel/Pinger.swift b/ios/PacketTunnel/Pinger.swift new file mode 100644 index 0000000000..ea143d0ca6 --- /dev/null +++ b/ios/PacketTunnel/Pinger.swift @@ -0,0 +1,257 @@ +// +// Pinger.swift +// PacketTunnel +// +// Created by pronebird on 21/02/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import struct Network.IPv4Address +import Logging + +final class Pinger { + // Sender identifier passed along with ICMP packet. + private let identifier: UInt16 = 757 + + private var sequenceNumber: UInt16 = 0 + private var socket: CFSocket? + + private let address: IPv4Address + private let interfaceName: String? + + private let logger = Logger(label: "Pinger") + private let stateLock = NSRecursiveLock() + private var timer: DispatchSourceTimer? + + init(address: IPv4Address, interfaceName: String?) { + self.address = address + self.interfaceName = interfaceName + } + + deinit { + stop() + } + + func start(delay: DispatchTimeInterval, repeating repeatInterval: DispatchTimeInterval) -> Result<(), Pinger.Error> { + stateLock.lock() + defer { stateLock.unlock() } + + stop() + + guard let newSocket = CFSocketCreate(kCFAllocatorDefault, AF_INET, SOCK_DGRAM, IPPROTO_ICMP, 0, nil, nil) else { + return .failure(.createSocket) + } + + let flags = CFSocketGetSocketFlags(newSocket) + if (flags & kCFSocketCloseOnInvalidate) == 0 { + CFSocketSetSocketFlags(newSocket, flags | kCFSocketCloseOnInvalidate) + } + + if case .failure(let error) = bindSocket(newSocket) { + return .failure(error) + } + + guard let runLoop = CFSocketCreateRunLoopSource(kCFAllocatorDefault, newSocket, 0) else { + return .failure(.createRunLoop) + } + + CFRunLoopAddSource(CFRunLoopGetMain(), runLoop, .defaultMode) + + let newTimer = DispatchSource.makeTimerSource() + newTimer.setEventHandler { [weak self] in + self?.send() + } + + socket = newSocket + timer = newTimer + + newTimer.schedule(wallDeadline: .now() + delay, repeating: repeatInterval) + newTimer.resume() + + return .success(()) + } + + func stop() { + stateLock.lock() + defer { stateLock.unlock() } + + if let socket = socket { + CFSocketInvalidate(socket) + } + + socket = nil + + timer?.cancel() + timer = nil + } + + private func send() { + stateLock.lock() + guard let socket = socket else { + stateLock.unlock() + return + } + stateLock.unlock() + + var sa = sockaddr_in() + sa.sin_len = UInt8(MemoryLayout.size(ofValue: sa)) + sa.sin_family = sa_family_t(AF_INET) + sa.sin_addr = address.rawValue.withUnsafeBytes { buffer in + return buffer.bindMemory(to: in_addr.self).baseAddress!.pointee + } + + let sequenceNumber = nextSequenceNumber() + let packetData = Self.createICMPPacket(identifier: identifier, sequenceNumber: sequenceNumber, payload: nil) + + let bytesSent = packetData.withUnsafeBytes { dataBuffer -> Int in + return withUnsafeBytes(of: &sa) { bufferPointer in + let sockaddrPointer = bufferPointer.bindMemory(to: sockaddr.self).baseAddress! + + return sendto( + CFSocketGetNative(socket), + dataBuffer.baseAddress!, + dataBuffer.count, + 0, + sockaddrPointer, + socklen_t(MemoryLayout<sockaddr_in>.size) + ) + } + } + + if bytesSent == -1 { + logger.debug("Failed to send echo (errno: \(errno)).") + } + } + + private func nextSequenceNumber() -> UInt16 { + stateLock.lock() + let (partialValue, isOverflow) = sequenceNumber.addingReportingOverflow(1) + let nextSequenceNumber = isOverflow ? 0 : partialValue + + sequenceNumber = nextSequenceNumber + stateLock.unlock() + + return nextSequenceNumber + } + + private func bindSocket(_ socket: CFSocket) -> Result<(), Pinger.Error> { + guard let interfaceName = interfaceName else { + logger.debug("Interface is not specified.") + return .success(()) + } + + var index = if_nametoindex(interfaceName) + guard index > 0 else { + return .failure(.mapInterfaceNameToIndex(errno)) + } + + logger.debug("Bind socket to \"\(interfaceName)\" (index: \(index))...") + + let result = setsockopt( + CFSocketGetNative(socket), + IPPROTO_IP, + IP_BOUND_IF, + &index, + socklen_t(MemoryLayout.size(ofValue: index)) + ) + + if result == -1 { + logger.error("Failed to bind socket to \"\(interfaceName)\" (index: \(index), errno: \(errno)).") + + return .failure(.bindSocket(errno)) + } else { + return .success(()) + } + } + + private class func createICMPPacket(identifier: UInt16, sequenceNumber: UInt16, payload: Data?) -> Data { + // Create data buffer. + var data = Data() + + // ICMP type. + data.append(UInt8(ICMP_ECHO)) + + // Code. + data.append(UInt8(0)) + + // Checksum. + withUnsafeBytes(of: UInt16(0)) { data.append(Data($0)) } + + // Identifier. + withUnsafeBytes(of: identifier.bigEndian) { data.append(Data($0)) } + + // Sequence number. + withUnsafeBytes(of: sequenceNumber.bigEndian) { data.append(Data($0)) } + + // Append payload. + if let payload = payload { + data.append(contentsOf: payload) + } + + // Calculate checksum. + let checksum = in_chksum(data) + + // Inject computed checksum into the packet. + data.withUnsafeMutableBytes { buffer in + buffer.storeBytes(of: checksum, toByteOffset: 2, as: UInt16.self) + } + + return data + } +} + +extension Pinger { + enum Error: LocalizedError, Equatable { + /// Failure to create a socket. + case createSocket + + /// Failure to map interface name to index. + case mapInterfaceNameToIndex(Int32) + + /// Failure to bind socket to interface. + case bindSocket(Int32) + + /// Failure to create a runloop for socket. + case createRunLoop + + var errorDescription: String? { + switch self { + case .createSocket: + return "Failure to create socket." + case .mapInterfaceNameToIndex: + return "Failure to map interface name to index." + case .bindSocket: + return "Failure to bind socket to interface." + case .createRunLoop: + return "Failure to create run loop for socket." + } + } + } +} + +private func in_chksum(_ data: Data) -> UInt16 { + return data.withUnsafeBytes { buffer in + let length = buffer.count + + var sum: Int32 = 0 + + let isOdd = length % 2 != 0 + let strideTo = isOdd ? length - 1 : length + + for offset in stride(from: 0, to: strideTo, by: 2) { + let word = buffer.load(fromByteOffset: offset, as: UInt16.self) + sum += Int32(word) + } + + if isOdd { + let byte = buffer.load(fromByteOffset: length - 1, as: UInt8.self) + sum += Int32(byte) + } + + sum = (sum >> 16) + (sum & 0xffff) + sum += (sum >> 16) + + return UInt16(truncatingIfNeeded: ~sum) + } +} diff --git a/ios/PacketTunnel/TunnelMonitor.swift b/ios/PacketTunnel/TunnelMonitor.swift new file mode 100644 index 0000000000..2c5049f919 --- /dev/null +++ b/ios/PacketTunnel/TunnelMonitor.swift @@ -0,0 +1,312 @@ +// +// TunnelMonitor.swift +// PacketTunnel +// +// Created by pronebird on 09/02/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import NetworkExtension +import WireGuardKit +import Logging + +protocol TunnelMonitorDelegate: AnyObject { + /// Invoked when tunnel monitor determined that connection is established. + func tunnelMonitorDidDetermineConnectionEstablished(_ tunnelMonitor: TunnelMonitor) + + /// Invoked when tunnel monitor determined that connection attempt has failed. + func tunnelMonitorDelegateShouldHandleConnectionRecovery(_ tunnelMonitor: TunnelMonitor) + + /// Invoked when network reachability status changes. + func tunnelMonitor(_ tunnelMonitor: TunnelMonitor, networkReachabilityStatusDidChange isNetworkReachable: Bool) +} + +final class TunnelMonitor { + private let adapter: WireGuardAdapter + private let internalQueue = DispatchQueue(label: "TunnelMonitor") + private let delegateQueue: DispatchQueue + + private var address: IPv4Address? + private var pinger: Pinger? + private var pathMonitor: NWPathMonitor? + private var networkBytesReceived: UInt64 = 0 + private var firstAttemptDate: Date? + private var lastAttemptDate: Date? + private var lastError: Pinger.Error? + private var isPinging = false + + private var logger = Logger(label: "TunnelMonitor") + private var timer: DispatchSourceTimer? + + private weak var _delegate: TunnelMonitorDelegate? + weak var delegate: TunnelMonitorDelegate? { + set { + internalQueue.sync { + _delegate = newValue + } + } + get { + return internalQueue.sync { + return _delegate + } + } + } + + var startDate: Date? { + return internalQueue.sync { + return firstAttemptDate + } + } + + init(queue: DispatchQueue, adapter: WireGuardAdapter) { + delegateQueue = queue + self.adapter = adapter + } + + deinit { + stopNoQueue(forRestart: false) + } + + func start(address: IPv4Address) { + internalQueue.async { + self.startNoQueue(address: address) + } + } + + func stop() { + internalQueue.async { + self.stopNoQueue(forRestart: false) + } + } + + private func startNoQueue(address pingAddress: IPv4Address) { + if address == nil { + logger.debug("Start tunnel monitor with address: \(pingAddress).") + } else { + logger.debug("Restart tunnel monitor with address: \(pingAddress).") + } + + stopNoQueue(forRestart: true) + + address = pingAddress + networkBytesReceived = 0 + firstAttemptDate = Date() + lastAttemptDate = firstAttemptDate + lastError = nil + + let newPathMonitor = NWPathMonitor() + newPathMonitor.pathUpdateHandler = { [weak self] path in + self?.handleNetworkPathUpdate(path) + } + newPathMonitor.start(queue: internalQueue) + pathMonitor = newPathMonitor + + handleNetworkPathUpdate(newPathMonitor.currentPath) + } + + private func stopNoQueue(forRestart: Bool) { + if !forRestart { + logger.debug("Stop tunnel monitor.") + } + + address = nil + firstAttemptDate = nil + lastAttemptDate = nil + lastError = nil + + pathMonitor?.cancel() + pathMonitor = nil + + cancelWgStatsTimer() + stopPinging() + } + + private func startPinging(address: IPv4Address) -> Result<(), Pinger.Error> { + let newPinger = Pinger(address: address, interfaceName: adapter.interfaceName) + let pingerResult = newPinger.start(delay: TunnelMonitorConfiguration.pingStartDelay, repeating: TunnelMonitorConfiguration.pingInterval) + + if case .success = pingerResult { + pinger = newPinger + isPinging = true + } + + return pingerResult + } + + private func stopPinging() { + pinger?.stop() + pinger = nil + + isPinging = false + } + + private func setWgStatsTimer() { + // Cancel existing timer. + cancelWgStatsTimer() + + // Create new timer. + timer = DispatchSource.makeTimerSource(queue: internalQueue) + timer?.setEventHandler { [weak self] in + self?.onWgStatsTimer() + } + timer?.schedule(wallDeadline: .now(), repeating: TunnelMonitorConfiguration.wgStatsQueryInterval) + timer?.resume() + + logger.debug("Set WG stats timer.") + } + + private func cancelWgStatsTimer() { + timer?.cancel() + timer = nil + } + + private func onWgStatsTimer() { + adapter.getRuntimeConfiguration { [weak self] str in + guard let self = self else { return } + + self.internalQueue.async { + self.handleWgStatsUpdate(string: str) + } + } + } + + private func handleWgStatsUpdate(string: String?) { + guard let string = string else { + logger.debug("Received no runtime configuration from WireGuard adapter.") + return + } + + guard let newNetworkBytesReceived = Self.parseNetworkBytesReceived(from: string) else { + logger.debug("Failed to parse rx_bytes from runtime configuration.") + return + } + + let oldNetworkBytesReceived = self.networkBytesReceived + networkBytesReceived = newNetworkBytesReceived + + if newNetworkBytesReceived < oldNetworkBytesReceived { + logger.debug("Stats was reset? newNetworkBytesReceived = \(newNetworkBytesReceived), oldNetworkBytesReceived = \(oldNetworkBytesReceived)") + return + } + + if newNetworkBytesReceived > oldNetworkBytesReceived { + // Tell delegate that connection is established. + delegateQueue.async { + self.delegate?.tunnelMonitorDidDetermineConnectionEstablished(self) + } + + // Stop the tunnel monitor. + stopNoQueue(forRestart: false) + + return + } + + if let nextAttemptDate = lastAttemptDate?.addingTimeInterval(TunnelMonitorConfiguration.connectionTimeout), nextAttemptDate <= Date() { + // Reset the last recovery attempt date. + lastAttemptDate = nextAttemptDate + + // Reset last error. + lastError = nil + + // Tell delegate to attempt the connection recovery. + delegateQueue.async { + self.delegate?.tunnelMonitorDelegateShouldHandleConnectionRecovery(self) + } + } + } + + private func handleNetworkPathUpdate(_ networkPath: Network.NWPath) { + guard let address = address else { + return + } + + let isNetworkReachable = isNetworkPathReachable(networkPath) + + switch (isNetworkReachable, isPinging) { + case (true, false): + logger.debug("Network is reachable. Starting to ping.") + + switch startPinging(address: address) { + case .success: + // Reset the last recovery attempt date. + firstAttemptDate = Date() + lastAttemptDate = firstAttemptDate + + // Start WG stats timer. + setWgStatsTimer() + + delegateQueue.async { + self.delegate?.tunnelMonitor(self, networkReachabilityStatusDidChange: isNetworkReachable) + } + + case .failure(let error): + if error != lastError { + logger.error(chainedError: AnyChainedError(error), message: "Failed to start pinging.") + lastError = error + } + } + + case (false, true): + logger.debug("Network is unreachable. Stop pinging and wait...") + + // Cancel timers and ping. + cancelWgStatsTimer() + stopPinging() + + // Reset the last recovery attempt date. + lastAttemptDate = nil + + delegateQueue.async { + self.delegate?.tunnelMonitor(self, networkReachabilityStatusDidChange: isNetworkReachable) + } + + default: + break + } + } + + private func isNetworkPathReachable(_ networkPath: Network.NWPath) -> Bool { + // Get utun interface name. + guard let tunName = adapter.interfaceName else { return false } + + // Check if utun is up. + let utunUp = networkPath.availableInterfaces.contains { interface in + return interface.name == tunName + } + + // Return false if tunnel is down. + guard utunUp else { + return false + } + + // Return false if utun is the only available interface. + if networkPath.availableInterfaces.count == 1 { + return false + } + + switch networkPath.status { + case .requiresConnection, .satisfied: + return true + case .unsatisfied: + return false + @unknown default: + return false + } + } + + private class func parseNetworkBytesReceived(from string: String) -> UInt64? { + guard let range = string.range(of: "rx_bytes=") else { return nil } + + let startIndex = range.upperBound + let endIndex = string[startIndex...].firstIndex { ch in + return ch.isNewline + } + + if let endIndex = endIndex { + return UInt64(string[startIndex..<endIndex]) + } else { + return nil + } + } +} diff --git a/ios/PacketTunnel/TunnelMonitorConfiguration.swift b/ios/PacketTunnel/TunnelMonitorConfiguration.swift new file mode 100644 index 0000000000..13f4c4908d --- /dev/null +++ b/ios/PacketTunnel/TunnelMonitorConfiguration.swift @@ -0,0 +1,23 @@ +// +// TunnelMonitorConfiguration.swift +// PacketTunnel +// +// Created by pronebird on 10/03/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +enum TunnelMonitorConfiguration { + /// Interval at which to query the adapter for stats. + static let wgStatsQueryInterval: DispatchTimeInterval = .milliseconds(50) + + /// Interval for sending echo packets. + static let pingInterval: DispatchTimeInterval = .seconds(3) + + /// Delay before sending the first echo packet. + static let pingStartDelay: DispatchTimeInterval = .milliseconds(500) + + /// Interval after which connection is treated as being lost. + static let connectionTimeout: TimeInterval = 15 +} |
