diff options
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 18 | ||||
| -rw-r--r-- | ios/MullvadVPN/ConnectViewController.swift | 32 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelaySelector.swift | 12 | ||||
| -rw-r--r-- | ios/MullvadVPN/SimulatorTunnelProviderHost.swift | 56 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelIPC/PacketTunnelStatus.swift | 36 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelIPC/TunnelConnectionInfo.swift | 17 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelIPC/TunnelIPCRequest.swift | 8 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift | 6 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift | 36 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/SetAccountOperation.swift | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift | 6 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/TunnelManager.swift | 178 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/TunnelManagerState.swift | 18 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/TunnelState.swift | 69 | ||||
| -rw-r--r-- | ios/PacketTunnel/PacketTunnelProvider.swift | 77 |
16 files changed, 397 insertions, 178 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 80a279f542..427ae1c59e 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -151,11 +151,11 @@ 585DA89426B0323E00B8C587 /* TunnelIPCRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89226B0323E00B8C587 /* TunnelIPCRequest.swift */; }; 585DA89626B0328000B8C587 /* TunnelIPCResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89526B0328000B8C587 /* TunnelIPCResponse.swift */; }; 585DA89726B0328000B8C587 /* TunnelIPCResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89526B0328000B8C587 /* TunnelIPCResponse.swift */; }; - 585DA89926B0329200B8C587 /* TunnelConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* TunnelConnectionInfo.swift */; }; - 585DA89A26B0329200B8C587 /* TunnelConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* TunnelConnectionInfo.swift */; }; + 585DA89926B0329200B8C587 /* PacketTunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */; }; + 585DA89A26B0329200B8C587 /* PacketTunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */; }; 585DA89B26B146B300B8C587 /* TunnelIPCCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88E26B031E200B8C587 /* TunnelIPCCoding.swift */; }; 585DA8A326B14E0D00B8C587 /* ServerRelaysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA88326B0270700B8C587 /* ServerRelaysResponse.swift */; }; - 585DA8A526B14EE000B8C587 /* TunnelConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* TunnelConnectionInfo.swift */; }; + 585DA8A526B14EE000B8C587 /* PacketTunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */; }; 585DA8A626B14F5100B8C587 /* SSLPinningURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */; }; 5860392926DCE7AB00554C79 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; }; 5860392A26DCE7AB00554C79 /* PromiseCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860392826DCE7AB00554C79 /* PromiseCompletion.swift */; }; @@ -462,7 +462,7 @@ 585DA88E26B031E200B8C587 /* TunnelIPCCoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelIPCCoding.swift; sourceTree = "<group>"; }; 585DA89226B0323E00B8C587 /* TunnelIPCRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelIPCRequest.swift; sourceTree = "<group>"; }; 585DA89526B0328000B8C587 /* TunnelIPCResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelIPCResponse.swift; sourceTree = "<group>"; }; - 585DA89826B0329200B8C587 /* TunnelConnectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelConnectionInfo.swift; sourceTree = "<group>"; }; + 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelStatus.swift; sourceTree = "<group>"; }; 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>"; }; @@ -800,7 +800,7 @@ 585DA88D26B031D100B8C587 /* TunnelIPC */ = { isa = PBXGroup; children = ( - 585DA89826B0329200B8C587 /* TunnelConnectionInfo.swift */, + 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */, 5845F841236CBACD00B2D93C /* TunnelIPC.swift */, 585DA88E26B031E200B8C587 /* TunnelIPCCoding.swift */, 5875960626F36B3A00BF6711 /* TunnelIPCError.swift */, @@ -979,6 +979,7 @@ 5807E2BF2432038B00F5FF30 /* String+Split.swift */, 5871FB8225498CA20051A0A4 /* Swizzle.swift */, 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */, + 58E0A98727C8F46300FE6BDD /* Tunnel.swift */, 585DA88D26B031D100B8C587 /* TunnelIPC */, 5823FA5726CE4A4100283BF8 /* TunnelManager */, 587AD7C523421D7000E93A53 /* TunnelSettings.swift */, @@ -991,7 +992,6 @@ 58FD5BF12424F7D700112C88 /* UserInterfaceInteractionRestriction.swift */, 58F7CA872692E34000FC59FD /* WireguardKeysContentView.swift */, 5877152F23981F7B001F8237 /* WireguardKeysViewController.swift */, - 58E0A98727C8F46300FE6BDD /* Tunnel.swift */, ); path = MullvadVPN; sourceTree = "<group>"; @@ -1336,7 +1336,7 @@ 58A94AE626D23C3D001CB97C /* PromiseTests.swift in Sources */, 5857F23824C8446700CF6F47 /* AsyncBlockOperation.swift in Sources */, 582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */, - 585DA8A526B14EE000B8C587 /* TunnelConnectionInfo.swift in Sources */, + 585DA8A526B14EE000B8C587 /* PacketTunnelStatus.swift in Sources */, 588DD76D26FCB4A2006F6233 /* Cancellable.swift in Sources */, 5896AE7E246ACE65005B36CB /* KeychainAttributes.swift in Sources */, 58B0A2A9238EE6A100BC001D /* RelayConstraints.swift in Sources */, @@ -1362,7 +1362,7 @@ 58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */, 5891BF5125E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift in Sources */, 587B75412668FD7800DEF7E9 /* AccountExpiryNotificationProvider.swift in Sources */, - 585DA89926B0329200B8C587 /* TunnelConnectionInfo.swift in Sources */, + 585DA89926B0329200B8C587 /* PacketTunnelStatus.swift in Sources */, 58BA692E23E99EFF009DC256 /* Locking.swift in Sources */, 5896CEF226972DEB00B0FAE8 /* AccountContentView.swift in Sources */, 5840250122B1124600E4CFEC /* IPAddress+Codable.swift in Sources */, @@ -1581,7 +1581,7 @@ 5838318B27C40A3900000571 /* Pinger.swift in Sources */, 5820675C26E6576800655B05 /* RelayCache.swift in Sources */, 58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */, - 585DA89A26B0329200B8C587 /* TunnelConnectionInfo.swift in Sources */, + 585DA89A26B0329200B8C587 /* PacketTunnelStatus.swift in Sources */, 585DA88526B0270700B8C587 /* ServerRelaysResponse.swift in Sources */, 5806767627048E7D00C858CB /* Promise+Result.swift in Sources */, 581503A724D6F4AE00C9C50E /* Logging.swift in Sources */, diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift index 03ca5a2563..59978ec4ad 100644 --- a/ios/MullvadVPN/ConnectViewController.swift +++ b/ios/MullvadVPN/ConnectViewController.swift @@ -208,30 +208,30 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen private func updateTunnelConnectionInfo() { switch tunnelState { - case .connecting(let connectionInfo): - setConnectionInfo(connectionInfo) + case .connecting(let tunnelRelay): + setTunnelRelay(tunnelRelay) - case .connected(let connectionInfo), .reconnecting(let connectionInfo): - setConnectionInfo(connectionInfo) + case .connected(let tunnelRelay), .reconnecting(let tunnelRelay): + setTunnelRelay(tunnelRelay) case .disconnected, .disconnecting, .pendingReconnect: - setConnectionInfo(nil) + setTunnelRelay(nil) } mainContentView.locationContainerView.accessibilityLabel = tunnelState.localizedAccessibilityLabel } - private func setConnectionInfo(_ connectionInfo: TunnelConnectionInfo?) { - if let connectionInfo = connectionInfo { - mainContentView.cityLabel.attributedText = attributedStringForLocation(string: connectionInfo.location.city) - mainContentView.countryLabel.attributedText = attributedStringForLocation(string: connectionInfo.location.country) + private func setTunnelRelay(_ tunnelRelay: PacketTunnelRelay?) { + if let tunnelRelay = tunnelRelay { + mainContentView.cityLabel.attributedText = attributedStringForLocation(string: tunnelRelay.location.city) + mainContentView.countryLabel.attributedText = attributedStringForLocation(string: tunnelRelay.location.country) mainContentView.connectionPanel.dataSource = ConnectionPanelData( - inAddress: "\(connectionInfo.ipv4Relay) UDP", + inAddress: "\(tunnelRelay.ipv4Relay) UDP", outAddress: nil ) mainContentView.connectionPanel.isHidden = false - mainContentView.connectionPanel.connectedRelayName = connectionInfo.hostname + mainContentView.connectionPanel.connectedRelayName = tunnelRelay.hostname } else { mainContentView.countryLabel.attributedText = attributedStringForLocation(string: " ") mainContentView.cityLabel.attributedText = attributedStringForLocation(string: " ") @@ -274,15 +274,15 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen private func updateLocation(animated: Bool) { switch tunnelState { - case .connecting(let connectionInfo): - if let connectionInfo = connectionInfo { - setLocation(coordinate: connectionInfo.location.geoCoordinate, animated: animated) + case .connecting(let tunnelRelay): + if let tunnelRelay = tunnelRelay { + setLocation(coordinate: tunnelRelay.location.geoCoordinate, animated: animated) } else { unsetLocation(animated: animated) } - case .connected(let connectionInfo), .reconnecting(let connectionInfo): - setLocation(coordinate: connectionInfo.location.geoCoordinate, animated: animated) + case .connected(let tunnelRelay), .reconnecting(let tunnelRelay): + setLocation(coordinate: tunnelRelay.location.geoCoordinate, animated: animated) case .disconnected, .disconnecting, .pendingReconnect: unsetLocation(animated: animated) diff --git a/ios/MullvadVPN/RelaySelector.swift b/ios/MullvadVPN/RelaySelector.swift index f0bb12ce7f..fdf587e6d4 100644 --- a/ios/MullvadVPN/RelaySelector.swift +++ b/ios/MullvadVPN/RelaySelector.swift @@ -21,12 +21,12 @@ private struct RelayWithLocation { } extension RelaySelectorResult { - var tunnelConnectionInfo: TunnelConnectionInfo { - return TunnelConnectionInfo( - ipv4Relay: self.endpoint.ipv4Relay, - ipv6Relay: self.endpoint.ipv6Relay, - hostname: self.relay.hostname, - location: self.location + var packetTunnelRelay: PacketTunnelRelay { + return PacketTunnelRelay( + ipv4Relay: endpoint.ipv4Relay, + ipv6Relay: endpoint.ipv6Relay, + hostname: relay.hostname, + location: location ) } } diff --git a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift index cc1cbeef52..7305dbe0f2 100644 --- a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift @@ -14,7 +14,7 @@ import Logging class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { - private var connectionInfo: TunnelConnectionInfo? + private var tunnelStatus = PacketTunnelStatus() private let providerLogger = Logger(label: "SimulatorTunnelProviderHost") private let stateQueue = DispatchQueue(label: "SimulatorTunnelProviderHostQueue") @@ -31,9 +31,9 @@ class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { } if let appSelectorResult = appSelectorResult.flattenValue { - self.connectionInfo = appSelectorResult.tunnelConnectionInfo + self.tunnelStatus.tunnelRelay = appSelectorResult.packetTunnelRelay } else { - self.connectionInfo = self.pickRelay()?.tunnelConnectionInfo + self.tunnelStatus.tunnelRelay = self.pickRelay()?.packetTunnelRelay } completionHandler(nil) @@ -42,41 +42,41 @@ class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { stateQueue.async { - self.connectionInfo = nil + self.tunnelStatus = PacketTunnelStatus() completionHandler() } } override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { - Result { try TunnelIPC.Coding.decodeRequest(messageData) } - .asPromise() - .receive(on: stateQueue) - .onFailure { error in + stateQueue.async { + let request: TunnelIPC.Request + do { + request = try TunnelIPC.Coding.decodeRequest(messageData) + } catch { self.providerLogger.error(chainedError: AnyChainedError(error), message: "Failed to decode the IPC request.") + completionHandler?(nil) + return } - .success() - .mapThen(defaultValue: nil) { request in - switch request { - case .tunnelConnectionInfo: - return Result { try TunnelIPC.Coding.encodeResponse(self.connectionInfo) } - .asPromise() - .onFailure { error in - self.providerLogger.error(chainedError: AnyChainedError(error), message: "Failed to encode tunnel connection info IPC response.") - } - .success() - case .reloadTunnelSettings: - self.reasserting = true - self.connectionInfo = self.pickRelay()?.tunnelConnectionInfo - self.reasserting = false + var response: Data? - return .resolved(nil) + switch request { + case .getTunnelStatus: + do { + response = try TunnelIPC.Coding.encodeResponse(self.tunnelStatus) + } catch { + self.providerLogger.error(chainedError: AnyChainedError(error), message: "Failed to encode tunnel status IPC response.") } + + case .reloadTunnelSettings: + self.reasserting = true + self.tunnelStatus.tunnelRelay = self.pickRelay()?.packetTunnelRelay + self.reasserting = false } - .observe { completion in - completionHandler?(completion.unwrappedValue ?? nil) - } + + completionHandler?(response) + } } private func pickRelay() -> RelaySelectorResult? { @@ -93,13 +93,13 @@ class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { constraints: entry.tunnelSettings.relayConstraints ) case .failure(let error): - self.providerLogger.error(chainedError: error, message: "Failed to load tunnel settings when picking relay") + self.providerLogger.error(chainedError: error, message: "Failed to load tunnel settings when picking relay.") return nil } case .failure(let error): - self.providerLogger.error(chainedError: error, message: "Failed to read relays when picking relay") + self.providerLogger.error(chainedError: error, message: "Failed to read relays when picking relay.") return nil } } diff --git a/ios/MullvadVPN/TunnelIPC/PacketTunnelStatus.swift b/ios/MullvadVPN/TunnelIPC/PacketTunnelStatus.swift new file mode 100644 index 0000000000..3dc17a587d --- /dev/null +++ b/ios/MullvadVPN/TunnelIPC/PacketTunnelStatus.swift @@ -0,0 +1,36 @@ +// +// PacketTunnelStatus.swift +// PacketTunnelStatus +// +// Created by pronebird on 27/07/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// A struct that holds packet tunnel process status. +struct PacketTunnelStatus: Codable, Equatable { + /// Flag indicating whether network is reachable. + var isNetworkReachable: Bool + + /// When the packet tunnel started connecting. + var connectingDate: Date? + + /// Current relay. + var tunnelRelay: PacketTunnelRelay? +} + +/// A struct that holds the relay endpoints and location. +struct PacketTunnelRelay: Codable, Equatable { + /// IPv4 relay endpoint. + let ipv4Relay: IPv4Endpoint + + /// IPv6 relay endpoint. + let ipv6Relay: IPv6Endpoint? + + /// Relay hostname. + let hostname: String + + /// Relay location. + let location: Location +} diff --git a/ios/MullvadVPN/TunnelIPC/TunnelConnectionInfo.swift b/ios/MullvadVPN/TunnelIPC/TunnelConnectionInfo.swift deleted file mode 100644 index 9558864e6a..0000000000 --- a/ios/MullvadVPN/TunnelIPC/TunnelConnectionInfo.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// TunnelConnectionInfo.swift -// TunnelConnectionInfo -// -// Created by pronebird on 27/07/2021. -// Copyright © 2021 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -/// A struct that holds basic information regarding the tunnel connection. -struct TunnelConnectionInfo: Codable, Equatable { - let ipv4Relay: IPv4Endpoint - let ipv6Relay: IPv6Endpoint? - let hostname: String - let location: Location -} diff --git a/ios/MullvadVPN/TunnelIPC/TunnelIPCRequest.swift b/ios/MullvadVPN/TunnelIPC/TunnelIPCRequest.swift index 7bfa4de803..f331e7d1f5 100644 --- a/ios/MullvadVPN/TunnelIPC/TunnelIPCRequest.swift +++ b/ios/MullvadVPN/TunnelIPC/TunnelIPCRequest.swift @@ -14,15 +14,15 @@ extension TunnelIPC { /// Request the tunnel to reload settings. case reloadTunnelSettings - /// Request the tunnel connection info. - case tunnelConnectionInfo + /// Request the tunnel status. + case getTunnelStatus var description: String { switch self { case .reloadTunnelSettings: return "reloadTunnelSettings" - case .tunnelConnectionInfo: - return "tunnelConnectionInfo" + case .getTunnelStatus: + return "getTunnelStatus" } } diff --git a/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift b/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift index 5847af8b5c..ce430b5b81 100644 --- a/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift +++ b/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift @@ -37,11 +37,11 @@ extension TunnelIPC { } } - func getTunnelConnectionInfo(completionHandler: @escaping (OperationCompletion<TunnelConnectionInfo?, TunnelIPC.Error>) -> Void) -> Cancellable { - let operation = RequestOperation<TunnelConnectionInfo?>( + func getTunnelStatus(completionHandler: @escaping (OperationCompletion<PacketTunnelStatus, TunnelIPC.Error>) -> Void) -> Cancellable { + let operation = RequestOperation<PacketTunnelStatus>( queue: queue, tunnel: tunnel, - request: .tunnelConnectionInfo, + request: .getTunnelStatus, options: TunnelIPC.RequestOptions(), completionHandler: completionHandler ) diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift index d69e2d1028..bef06ac128 100644 --- a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift +++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift @@ -48,7 +48,7 @@ class MapConnectionStatusOperation: AsyncOperation { return } - let tunnelState = state.tunnelState + let tunnelState = state.tunnelStatus.state switch connectionStatus { case .connecting: @@ -56,17 +56,19 @@ class MapConnectionStatusOperation: AsyncOperation { case .connecting(.some(_)): break default: - state.tunnelState = .connecting(nil) + state.tunnelStatus.state = .connecting(nil) } let session = TunnelIPC.Session(tunnel: tunnel) - request = session.getTunnelConnectionInfo { [weak self] completion in + request = session.getTunnelStatus { [weak self] completion in guard let self = self else { return } self.queue.async { - if case .success(.some(let connectionInfo)) = completion, !self.isCancelled { - self.state.tunnelState = .connecting(connectionInfo) + if case .success(let packetTunnelStatus) = completion, !self.isCancelled { + self.state.tunnelStatus.update(from: packetTunnelStatus) { relay in + return .connecting(relay) + } } self.finish() @@ -76,12 +78,14 @@ class MapConnectionStatusOperation: AsyncOperation { case .reasserting: let session = TunnelIPC.Session(tunnel: tunnel) - request = session.getTunnelConnectionInfo { [weak self] completion in + request = session.getTunnelStatus { [weak self] completion in guard let self = self else { return } self.queue.async { - if case .success(.some(let connectionInfo)) = completion, !self.isCancelled { - self.state.tunnelState = .reconnecting(connectionInfo) + if case .success(let packetTunnelStatus) = completion, !self.isCancelled { + self.state.tunnelStatus.update(from: packetTunnelStatus) { relay in + return relay.map { .reconnecting($0) } + } } self.finish() @@ -93,12 +97,14 @@ class MapConnectionStatusOperation: AsyncOperation { case .connected: let session = TunnelIPC.Session(tunnel: tunnel) - request = session.getTunnelConnectionInfo { [weak self] completion in + request = session.getTunnelStatus { [weak self] completion in guard let self = self else { return } self.queue.async { - if case .success(.some(let connectionInfo)) = completion, !self.isCancelled { - self.state.tunnelState = .connected(connectionInfo) + if case .success(let packetTunnelStatus) = completion, !self.isCancelled { + self.state.tunnelStatus.update(from: packetTunnelStatus) { relay in + return relay.map { .connected($0) } + } } self.finish() @@ -115,13 +121,13 @@ class MapConnectionStatusOperation: AsyncOperation { case .disconnecting(.reconnect): logger.debug("Restart the tunnel on disconnect.") - state.tunnelState = .pendingReconnect + state.tunnelStatus.reset(to: .pendingReconnect) startTunnelHandler?() startTunnelHandler = nil default: - state.tunnelState = .disconnected + state.tunnelStatus.reset(to: .disconnected) } case .disconnecting: @@ -129,11 +135,11 @@ class MapConnectionStatusOperation: AsyncOperation { case .disconnecting: break default: - state.tunnelState = .disconnecting(.nothing) + state.tunnelStatus.reset(to: .disconnecting(.nothing)) } case .invalid: - state.tunnelState = .disconnected + state.tunnelStatus.reset(to: .disconnected) @unknown default: logger.debug("Unknown NEVPNStatus: \(connectionStatus.rawValue)") diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift index b93eca2e72..162ebfff73 100644 --- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift +++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift @@ -165,7 +165,7 @@ class SetAccountOperation: AsyncOperation { willDeleteVPNConfigurationHandler = nil // Reset tunnel state to disconnected - state.tunnelState = .disconnected + state.tunnelStatus.reset(to: .disconnected) // Remove tunnel info state.tunnelInfo = nil diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift index 2a332aab0b..ac8223f963 100644 --- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift @@ -48,9 +48,9 @@ class StartTunnelOperation: AsyncOperation { return } - switch self.state.tunnelState { + switch self.state.tunnelStatus.state { case .disconnecting(.nothing): - self.state.tunnelState = .disconnecting(.reconnect) + self.state.tunnelStatus.state = .disconnecting(.reconnect) completionHandler(.success(())) @@ -115,7 +115,7 @@ class StartTunnelOperation: AsyncOperation { encodeErrorHandler = nil state.setTunnel(Tunnel(tunnelProvider: tunnelProvider), shouldRefreshTunnelState: false) - state.tunnelState = .connecting(selectorResult.tunnelConnectionInfo) + state.tunnelStatus.reset(to: .connecting(selectorResult.packetTunnelRelay)) try tunnelProvider.connection.startVPNTunnel(options: tunnelOptions.rawOptions()) } diff --git a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift index 2d75946e7f..7593c0a02b 100644 --- a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift @@ -43,9 +43,9 @@ class StopTunnelOperation: AsyncOperation { return } - switch self.state.tunnelState { + switch self.state.tunnelStatus.state { case .disconnecting(.reconnect): - state.tunnelState = .disconnecting(.nothing) + state.tunnelStatus.state = .disconnecting(.nothing) completionHandler(.success(())) diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 0e4a379116..d08e273475 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -13,16 +13,32 @@ import UIKit import Logging import class WireGuardKitTypes.PublicKey -/// A class that provides a convenient interface for VPN tunnels configuration, manipulation and -/// monitoring. -class TunnelManager: TunnelManagerStateDelegate -{ - /// Private key rotation interval (in seconds) - private static let privateKeyRotationInterval: TimeInterval = 60 * 60 * 24 * 4 +enum TunnelManagerConfiguration { + /// Delay used before starting to quickly poll the tunnel (in seconds). + /// Usually when the tunnel is either starting or when reconnecting for a brief moment, until + /// the tunnel broadcasts the connecting date which is later used to synchronize polling. + static let tunnelStatusQuickPollDelay: TimeInterval = 1 + + /// Poll interval used when connecting date is unknown (in seconds). + static let tunnelStatusQuickPollInterval: TimeInterval = 3 + + /// Delay used for when connecting date is known (in seconds). + /// Since both GUI and packet tunnel run timers, this accounts for some leeway. + static let tunnelStatusLongPollDelay: TimeInterval = 0.25 - /// Private key rotation retry interval (in seconds) - private static let privateKeyRotationFailureRetryInterval: TimeInterval = 60 * 15 + /// Poll interval used for when connecting date is known (in seconds). + static let tunnelStatusLongPollInterval = TunnelMonitorConfiguration.connectionTimeout + + /// Private key rotation interval (in seconds). + static let privateKeyRotationInterval: TimeInterval = 60 * 60 * 24 * 4 + + /// Private key rotation retry interval (in seconds). + static let privateKeyRotationFailureRetryInterval: TimeInterval = 60 * 15 +} +/// A class that provides a convenient interface for VPN tunnels configuration, manipulation and +/// monitoring. +final class TunnelManager: TunnelManagerStateDelegate { /// Operation categories private enum OperationCategory { static let manageTunnelProvider = "TunnelManager.manageTunnelProvider" @@ -52,12 +68,16 @@ class TunnelManager: TunnelManagerStateDelegate private var privateKeyRotationTimer: DispatchSourceTimer? private var isRunningPeriodicPrivateKeyRotation = false + private var tunnelStatusPollTimer: DispatchSourceTimer? + private var isPolling = false + private var lastConnectingDate: Date? + var tunnelInfo: TunnelInfo? { return state.tunnelInfo } var tunnelState: TunnelState { - return state.tunnelState + return state.tunnelStatus.state } private init(restClient: REST.Client) { @@ -79,7 +99,7 @@ class TunnelManager: TunnelManagerStateDelegate stateQueue.async { guard !self.isRunningPeriodicPrivateKeyRotation else { return } - self.logger.debug("Start periodic private key rotation") + self.logger.debug("Start periodic private key rotation.") self.isRunningPeriodicPrivateKeyRotation = true @@ -91,7 +111,7 @@ class TunnelManager: TunnelManagerStateDelegate stateQueue.async { guard self.isRunningPeriodicPrivateKeyRotation else { return } - self.logger.debug("Stop periodic private key rotation") + self.logger.debug("Stop periodic private key rotation.") self.isRunningPeriodicPrivateKeyRotation = false @@ -107,7 +127,7 @@ class TunnelManager: TunnelManagerStateDelegate if let tunnelInfo = self.state.tunnelInfo { let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate - let scheduleDate = Date(timeInterval: Self.privateKeyRotationInterval, since: creationDate) + let scheduleDate = Date(timeInterval: TunnelManagerConfiguration.privateKeyRotationInterval, since: creationDate) schedulePrivateKeyRotationTimer(scheduleDate) } else { @@ -259,6 +279,11 @@ class TunnelManager: TunnelManagerStateDelegate self.logger.error(chainedError: error, message: "Failed to reconnect the tunnel.") } + // Refresh tunnel status since reasserting may not be lowered until the tunnel is fully + // connected. + self.logger.debug("Refresh tunnel status due to reconnect.") + self.refreshTunnelStatus() + DispatchQueue.main.async { completionHandler?() } @@ -358,7 +383,7 @@ class TunnelManager: TunnelManagerStateDelegate queue: stateQueue, state: state, restClient: restClient, - rotationInterval: Self.privateKeyRotationInterval) { [weak self] completion in + rotationInterval: TunnelManagerConfiguration.privateKeyRotationInterval) { [weak self] completion in guard let self = self else { return } dispatchPrecondition(condition: .onQueue(self.stateQueue)) @@ -450,12 +475,23 @@ class TunnelManager: TunnelManagerStateDelegate } } - func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelState newTunnelState: TunnelState) { - logger.info("Set tunnel state: \(newTunnelState)") + func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelStatus newTunnelStatus: TunnelStatus) { + logger.info("Status: \(newTunnelStatus).") + + switch newTunnelStatus.state { + case .connecting, .reconnecting: + // Start polling tunnel status to keep the relay information up to date + // while the tunnel process is trying to connect. + startPollingTunnelStatus(connectingDate: newTunnelStatus.connectingDate) + + case .pendingReconnect, .connected, .disconnecting, .disconnected: + // Stop polling tunnel status once connection moved to final state. + cancelPollingTunnelStatus() + } DispatchQueue.main.async { self.observerList.forEach { (observer) in - observer.tunnelManager(self, didUpdateTunnelState: newTunnelState) + observer.tunnelManager(self, didUpdateTunnelState: newTunnelStatus.state) } } } @@ -472,7 +508,8 @@ class TunnelManager: TunnelManagerStateDelegate // Update the existing state if shouldRefreshTunnelState { - updateTunnelState() + logger.debug("Refresh tunnel status for new tunnel.") + refreshTunnelStatus() } } @@ -482,7 +519,10 @@ class TunnelManager: TunnelManagerStateDelegate unsubscribeVPNStatusObserver() statusObserver = tunnel.addBlockObserver(queue: stateQueue) { [weak self] tunnel, status in - self?.updateTunnelState() + guard let self = self else { return } + + self.logger.debug("VPN connection status changed to \(status).") + self.updateTunnelStatus(status) } } @@ -491,14 +531,19 @@ class TunnelManager: TunnelManagerStateDelegate statusObserver = nil } - /// Update `TunnelState` from `NEVPNStatus`. - /// Collects the `TunnelConnectionInfo` from the tunnel via IPC if needed before assigning the `tunnelState` - private func updateTunnelState() { + private func refreshTunnelStatus() { dispatchPrecondition(condition: .onQueue(stateQueue)) - guard let connectionStatus = self.state.tunnel?.status else { return } + if let connectionStatus = self.state.tunnel?.status { + updateTunnelStatus(connectionStatus) + } + } - logger.debug("VPN status changed to \(connectionStatus)") + /// Update `TunnelStatus` from `NEVPNStatus`. + /// Collects the `PacketTunnelStatus` from the tunnel via IPC if needed before assigning + /// the `tunnelStatus`. + private func updateTunnelStatus(_ connectionStatus: NEVPNStatus) { + dispatchPrecondition(condition: .onQueue(stateQueue)) let operation = MapConnectionStatusOperation(queue: stateQueue, state: state, connectionStatus: connectionStatus) { [weak self] in guard let self = self else { return } @@ -519,8 +564,8 @@ class TunnelManager: TunnelManagerStateDelegate @objc private func applicationDidBecomeActive() { stateQueue.async { - // Refresh tunnel state when application becomes active. - self.updateTunnelState() + self.logger.debug("Refresh tunnel status due to application becoming active.") + self.refreshTunnelStatus() } } @@ -592,6 +637,79 @@ class TunnelManager: TunnelManagerStateDelegate operationQueue.addOperation(operation) } + // MARK: - Tunnel status polling. + + private func computeNextPollDateAndRepeatInterval(connectingDate: Date?) -> (Date, TimeInterval) { + let delay, repeating: TimeInterval + let fireDate: Date + + if let connectingDate = connectingDate { + // Compute the schedule date for timer relative to when the packet tunnel started + // connecting. + delay = TunnelManagerConfiguration.tunnelStatusLongPollDelay + repeating = TunnelManagerConfiguration.tunnelStatusLongPollInterval + + // Compute the time elapsed since connecting date. + let elapsed = max(0, Date().timeIntervalSince(connectingDate)) + + // Compute how many times the timer has fired so far. + let fireCount = floor(elapsed / repeating) + + // Compute when the timer will fire next time. + let nextDelta = (fireCount + 1) * repeating + + // Compute the fire date adding extra delay to account for leeway. + fireDate = connectingDate.addingTimeInterval(nextDelta + delay) + } else { + // Do quick polling until it's known when the packet tunnel started connecting. + delay = TunnelManagerConfiguration.tunnelStatusQuickPollDelay + repeating = TunnelManagerConfiguration.tunnelStatusQuickPollInterval + + fireDate = Date(timeIntervalSinceNow: delay) + } + + return (fireDate, repeating) + } + + private func startPollingTunnelStatus(connectingDate: Date?) { + guard lastConnectingDate != connectingDate || !isPolling else { return } + + lastConnectingDate = connectingDate + isPolling = true + + let (fireDate, repeating) = computeNextPollDateAndRepeatInterval(connectingDate: connectingDate) + logger.debug("Start polling tunnel status at \(fireDate.logFormatDate()) every \(repeating) second(s).") + + let timer = DispatchSource.makeTimerSource(queue: stateQueue) + timer.setEventHandler { [weak self] in + guard let self = self else { return } + + self.logger.debug("Refresh tunnel status (poll).") + self.refreshTunnelStatus() + } + + timer.schedule( + wallDeadline: .now() + fireDate.timeIntervalSinceNow, + repeating: repeating + ) + + timer.resume() + + tunnelStatusPollTimer?.cancel() + tunnelStatusPollTimer = timer + } + + private func cancelPollingTunnelStatus() { + guard isPolling else { return } + + logger.debug("Cancel tunnel status polling.") + + tunnelStatusPollTimer?.cancel() + tunnelStatusPollTimer = nil + lastConnectingDate = nil + isPolling = false + } + } extension TunnelManager { @@ -638,7 +756,7 @@ extension TunnelManager { func scheduleBackgroundTask() -> Result<(), TunnelManager.Error> { if let tunnelInfo = self.state.tunnelInfo { let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate - let beginDate = Date(timeInterval: Self.privateKeyRotationInterval, since: creationDate) + let beginDate = Date(timeInterval: TunnelManagerConfiguration.privateKeyRotationInterval, since: creationDate) return submitBackgroundTask(at: beginDate) } else { @@ -704,17 +822,17 @@ extension TunnelManager { } else { logger.debug("Private key rotation was cancelled") - return Date(timeIntervalSinceNow: Self.privateKeyRotationFailureRetryInterval) + return Date(timeIntervalSinceNow: TunnelManagerConfiguration.privateKeyRotationFailureRetryInterval) } } fileprivate func nextScheduleDate(_ result: KeyRotationResult) -> Date { switch result { case .finished: - return Date(timeIntervalSinceNow: Self.privateKeyRotationInterval) + return Date(timeIntervalSinceNow: TunnelManagerConfiguration.privateKeyRotationInterval) case .throttled(let lastKeyCreationDate): - return Date(timeInterval: Self.privateKeyRotationInterval, since: lastKeyCreationDate) + return Date(timeInterval: TunnelManagerConfiguration.privateKeyRotationInterval, since: lastKeyCreationDate) } } @@ -729,7 +847,7 @@ extension TunnelManager { return nil default: - return Date(timeIntervalSinceNow: Self.privateKeyRotationFailureRetryInterval) + return Date(timeIntervalSinceNow: TunnelManagerConfiguration.privateKeyRotationFailureRetryInterval) } } } diff --git a/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift b/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift index 0468d835a2..3802390376 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift @@ -11,7 +11,7 @@ import NetworkExtension protocol TunnelManagerStateDelegate: AnyObject { func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelInfo newTunnelInfo: TunnelInfo?) - func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelState newTunnelState: TunnelState) + func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelStatus newTunnelStatus: TunnelStatus) func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelProvider newTunnelObject: Tunnel?, shouldRefreshTunnelState: Bool) } @@ -25,7 +25,11 @@ extension TunnelManager { private var _tunnelInfo: TunnelInfo? private var _tunnelObject: Tunnel? - private var _tunnelState: TunnelState = .disconnected + private var _tunnelStatus = TunnelStatus( + isNetworkReachable: false, + connectingDate: nil, + state: .disconnected + ) var tunnelInfo: TunnelInfo? { get { @@ -50,18 +54,18 @@ extension TunnelManager { } } - var tunnelState: TunnelState { + var tunnelStatus: TunnelStatus { get { return performBlock { - return _tunnelState + return _tunnelStatus } } set { performBlock { - if _tunnelState != newValue { - _tunnelState = newValue + if _tunnelStatus != newValue { + _tunnelStatus = newValue - delegate?.tunnelManagerState(self, didChangeTunnelState: newValue) + delegate?.tunnelManagerState(self, didChangeTunnelStatus: newValue) } } } diff --git a/ios/MullvadVPN/TunnelManager/TunnelState.swift b/ios/MullvadVPN/TunnelManager/TunnelState.swift index b016144dd3..bec15a9044 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelState.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelState.swift @@ -8,16 +8,61 @@ import Foundation -/// A enum that describes the tunnel state +/// A struct describing the tunnel status. +struct TunnelStatus: Equatable, CustomStringConvertible { + /// Whether netowork is reachable. + var isNetworkReachable: Bool + + /// When the packet tunnel started connecting. + var connectingDate: Date? + + /// Tunnel state. + var state: TunnelState + + var description: String { + var s = "\(state), network " + + if isNetworkReachable { + s += "reachable" + } else { + s += "unreachable" + } + + if let connectingDate = connectingDate { + s += ", started connecting at \(connectingDate.logFormatDate())" + } + + return s + } + + /// Updates the tunnel status from packet tunnel status, mapping relay to tunnel state. + mutating func update(from packetTunnelStatus: PacketTunnelStatus, mappingRelayToState mapper: (PacketTunnelRelay?) -> TunnelState?) { + isNetworkReachable = packetTunnelStatus.isNetworkReachable + connectingDate = packetTunnelStatus.connectingDate + + if let newState = mapper(packetTunnelStatus.tunnelRelay) { + state = newState + } + } + + /// Resets all fields to their defaults and assigns the next tunnel state. + mutating func reset(to newState: TunnelState) { + isNetworkReachable = true + connectingDate = nil + state = newState + } +} + +/// An enum that describes the tunnel state. enum TunnelState: Equatable, CustomStringConvertible { /// Pending reconnect after disconnect. case pendingReconnect - /// Connecting the tunnel. Contains the pending action carried over from disconnected state. - case connecting(TunnelConnectionInfo?) + /// Connecting the tunnel. + case connecting(_ relay: PacketTunnelRelay?) /// Connected the tunnel - case connected(TunnelConnectionInfo) + case connected(PacketTunnelRelay) /// Disconnecting the tunnel case disconnecting(ActionAfterDisconnect) @@ -27,26 +72,26 @@ enum TunnelState: Equatable, CustomStringConvertible { /// Reconnecting the tunnel. Normally this state appears in response to changing the /// relay constraints and asking the running tunnel to reload the configuration. - case reconnecting(TunnelConnectionInfo) + case reconnecting(_ relay: PacketTunnelRelay) var description: String { switch self { case .pendingReconnect: return "pending reconnect after disconnect" - case .connecting(let connectionInfo): - if let connectionInfo = connectionInfo { - return "connecting to \(connectionInfo.hostname)" + case .connecting(let tunnelRelay): + if let tunnelRelay = tunnelRelay { + return "connecting to \(tunnelRelay.hostname)" } else { return "connecting, fetching relay" } - case .connected(let connectionInfo): - return "connected to \(connectionInfo.hostname)" + case .connected(let tunnelRelay): + return "connected to \(tunnelRelay.hostname)" case .disconnecting(let actionAfterDisconnect): return "disconnecting and then \(actionAfterDisconnect)" case .disconnected: return "disconnected" - case .reconnecting(let connectionInfo): - return "reconnecting to \(connectionInfo.hostname)" + case .reconnecting(let tunnelRelay): + return "reconnecting to \(tunnelRelay.hostname)" } } } diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index 9c67b084a9..61bc7a9cd4 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -41,16 +41,12 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { /// 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).") - } else { - self.providerLogger.debug("Unset tunnel relay.") - } - } - } + /// Tunnel status. + private var tunnelStatus = PacketTunnelStatus( + isNetworkReachable: true, + connectingDate: nil, + tunnelRelay: nil + ) override init() { let pid = ProcessInfo.processInfo.processIdentifier @@ -85,7 +81,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { switch appSelectorResult { case .some(let selectorResult): - providerLogger.debug("Start the tunnel via app, connect to \(selectorResult.tunnelConnectionInfo.hostname).") + providerLogger.debug("Start the tunnel via app, connect to \(selectorResult.relay.hostname).") case .none: if tunnelOptions.isOnDemand() { @@ -114,9 +110,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { return } - // Set tunnel connection info. + // Set tunnel status. dispatchQueue.async { - self.tunnelConnectionInfo = tunnelConfiguration.selectorResult.tunnelConnectionInfo + let tunnelRelay = tunnelConfiguration.selectorResult.packetTunnelRelay + self.tunnelStatus.tunnelRelay = tunnelRelay + self.providerLogger.debug("Set tunnel relay to \(tunnelRelay.hostname).") } // Start tunnel. @@ -142,7 +140,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { // Start tunnel monitor. let gatewayAddress = tunnelConfiguration.selectorResult.endpoint.ipv4Gateway - self.tunnelMonitor.start(address: gatewayAddress) + + self.startTunnelMonitor(gatewayAddress: gatewayAddress) } } } @@ -203,10 +202,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { completionHandler?(nil) - case .tunnelConnectionInfo: + case .getTunnelStatus: var response: Data? do { - response = try TunnelIPC.Coding.encodeResponse(self.tunnelConnectionInfo) + response = try TunnelIPC.Coding.encodeResponse(self.tunnelStatus) } catch { self.providerLogger.error(chainedError: AnyChainedError(error), message: "Failed to encode the app message response for \(request)") } @@ -232,6 +231,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { providerLogger.debug("Connection established.") + tunnelStatus.connectingDate = nil + startTunnelCompletionHandler?(nil) startTunnelCompletionHandler = nil @@ -272,8 +273,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { return } - // Set tunnel connection info. - self.tunnelConnectionInfo = tunnelConfiguration.selectorResult.tunnelConnectionInfo + // Update tunnel status. + let tunnelRelay = tunnelConfiguration.selectorResult.packetTunnelRelay + tunnelStatus.tunnelRelay = tunnelRelay + providerLogger.debug("Set tunnel relay to \(tunnelRelay.hostname).") // Update WireGuard configuration. adapter.update(tunnelConfiguration: tunnelConfiguration.wgTunnelConfig) { error in @@ -285,6 +288,16 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { } } + func tunnelMonitor(_ tunnelMonitor: TunnelMonitor, networkReachabilityStatusDidChange isNetworkReachable: Bool) { + tunnelStatus.isNetworkReachable = isNetworkReachable + + // Adjust the start reconnect date if tunnel monitor re-started pinging in response to + // network connectivity coming back up. + if let startDate = tunnelMonitor.startDate { + tunnelStatus.connectingDate = startDate + } + } + // MARK: - Private private func makeConfiguration(_ appSelectorResult: RelaySelectorResult? = nil) -> Result<PacketTunnelConfiguration, PacketTunnelProviderError> { @@ -335,10 +348,15 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { return } - // Set tunnel connection info. - let tunnelConnectionInfo = tunnelConfiguration.selectorResult.tunnelConnectionInfo - let oldTunnelConnectionInfo = self.tunnelConnectionInfo - self.tunnelConnectionInfo = tunnelConnectionInfo + // Copy old relay. + let oldTunnelRelay = tunnelStatus.tunnelRelay + let newTunnelRelay = tunnelConfiguration.selectorResult.packetTunnelRelay + + // Update tunnel status. + tunnelStatus.tunnelRelay = newTunnelRelay + tunnelStatus.connectingDate = nil + + providerLogger.debug("Set tunnel relay to \(newTunnelRelay.hostname).") // 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 @@ -355,8 +373,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { // Call completion handler immediately on error to update adapter configuration. if let error = error { - // Revert to previously used tunnel connection info. - self.tunnelConnectionInfo = oldTunnelConnectionInfo + // Revert to previously used tunnel relay. + self.tunnelStatus.tunnelRelay = oldTunnelRelay + self.providerLogger.debug("Reset tunnel relay to \(oldTunnelRelay?.hostname ?? "none").") // Lower the reasserting flag. if self.isConnected { @@ -381,12 +400,20 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate { // Restart tunnel monitor. let gatewayAddress = tunnelConfiguration.selectorResult.endpoint.ipv4Gateway - self.tunnelMonitor.start(address: gatewayAddress) + + self.startTunnelMonitor(gatewayAddress: gatewayAddress) } } } } + private func startTunnelMonitor(gatewayAddress: IPv4Address) { + tunnelMonitor.start(address: gatewayAddress) + + // Mark when the tunnel started monitoring connection. + tunnelStatus.connectingDate = tunnelMonitor.startDate + } + /// Load relay cache with potential networking to refresh the cache and pick the relay for the /// given relay constraints. private class func selectRelayEndpoint(relayConstraints: RelayConstraints) -> Result<RelaySelectorResult, PacketTunnelProviderError> { |
