diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-03-17 17:00:56 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-03-17 17:00:56 +0100 |
| commit | 55cf6fc80cbc60ffe992ee254833f672cb4ac0e7 (patch) | |
| tree | 16d765a865efb760ba4a38d039880003a1aeeba1 | |
| parent | 92304fd9f9436209531aaa78125f843f918f7803 (diff) | |
| parent | 1724aa75ae1f8960c8ac18b03421fc94833485b2 (diff) | |
| download | mullvadvpn-55cf6fc80cbc60ffe992ee254833f672cb4ac0e7.tar.xz mullvadvpn-55cf6fc80cbc60ffe992ee254833f672cb4ac0e7.zip | |
Merge branch 'tunnel-conn-monitor'
22 files changed, 746 insertions, 280 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index e4d5550a11..9cefde6b87 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -24,6 +24,13 @@ Line wrap the file at 100 chars. Th ## [Unreleased] ### Added +- Add tunnel monitor when establishing tunnel connection. Picks next relay every 15 seconds until + any inbound traffic received. This should also keep the tunnel in connecting or reconnecting state + until the tunnel monitor determined that connection is functional. + + +## [2022.1] - 2022-02-15 +### Added - Show privacy overlay when entering app switcher. - Add option to block malware. diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 49ce76af67..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 */; }; @@ -270,6 +270,7 @@ 58D0C7A223F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D0C7A023F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift */; }; 58D67A0A26D7AE3300557C3C /* OSLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */; }; 58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF28A42417CB4B00E836B0 /* AppStorePaymentManager.swift */; }; + 58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E0A98727C8F46300FE6BDD /* Tunnel.swift */; }; 58E1336926D2BE3700CC316B /* PromiseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336826D2BE3700CC316B /* PromiseObserver.swift */; }; 58E1336A26D2BE3700CC316B /* PromiseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336826D2BE3700CC316B /* PromiseObserver.swift */; }; 58E1336B26D2BE3700CC316B /* PromiseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1336826D2BE3700CC316B /* PromiseObserver.swift */; }; @@ -461,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>"; }; @@ -557,6 +558,7 @@ 58D0C79F23F1CECF00FE9BA7 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 58D0C7A023F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MullvadVPNScreenshots.swift; sourceTree = "<group>"; }; 58DF28A42417CB4B00E836B0 /* AppStorePaymentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorePaymentManager.swift; sourceTree = "<group>"; }; + 58E0A98727C8F46300FE6BDD /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = "<group>"; }; 58E1336826D2BE3700CC316B /* PromiseObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromiseObserver.swift; sourceTree = "<group>"; }; 58E1336C26D2BE7500CC316B /* AnyResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyResult.swift; sourceTree = "<group>"; }; 58E1337026D2BE9C00CC316B /* AnyOptional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyOptional.swift; sourceTree = "<group>"; }; @@ -798,7 +800,7 @@ 585DA88D26B031D100B8C587 /* TunnelIPC */ = { isa = PBXGroup; children = ( - 585DA89826B0329200B8C587 /* TunnelConnectionInfo.swift */, + 585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */, 5845F841236CBACD00B2D93C /* TunnelIPC.swift */, 585DA88E26B031E200B8C587 /* TunnelIPCCoding.swift */, 5875960626F36B3A00BF6711 /* TunnelIPCError.swift */, @@ -977,6 +979,7 @@ 5807E2BF2432038B00F5FF30 /* String+Split.swift */, 5871FB8225498CA20051A0A4 /* Swizzle.swift */, 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */, + 58E0A98727C8F46300FE6BDD /* Tunnel.swift */, 585DA88D26B031D100B8C587 /* TunnelIPC */, 5823FA5726CE4A4100283BF8 /* TunnelManager */, 587AD7C523421D7000E93A53 /* TunnelSettings.swift */, @@ -1333,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 */, @@ -1359,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 */, @@ -1494,6 +1497,7 @@ 5815039D24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */, 58CE5E64224146200008646E /* AppDelegate.swift in Sources */, 588DD76B26FCB49E006F6233 /* Cancellable.swift in Sources */, + 58E0A98827C8F46300FE6BDD /* Tunnel.swift in Sources */, 58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */, 5857F24324C8662600CF6F47 /* SelectLocationHeaderView.swift in Sources */, 5840BE35279EDB16002836BA /* OperationCompletion.swift in Sources */, @@ -1577,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/Logging/LogFormatting.swift b/ios/MullvadVPN/Logging/LogFormatting.swift index 2ccbdf4fc6..c41c2b758b 100644 --- a/ios/MullvadVPN/Logging/LogFormatting.swift +++ b/ios/MullvadVPN/Logging/LogFormatting.swift @@ -11,7 +11,7 @@ import Foundation extension Date { func logFormatDate() -> String { let formatter = DateFormatter() - formatter.dateFormat = "dd/MM/yyyy @ HH:mm" + formatter.dateFormat = "dd/MM/yyyy @ HH:mm:ss" return formatter.string(from: self) } 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/Tunnel.swift b/ios/MullvadVPN/Tunnel.swift new file mode 100644 index 0000000000..058b6b2a26 --- /dev/null +++ b/ios/MullvadVPN/Tunnel.swift @@ -0,0 +1,198 @@ +// +// Tunnel.swift +// MullvadVPN +// +// Created by pronebird on 25/02/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import NetworkExtension + +// Switch to stabs on simulator +#if targetEnvironment(simulator) +typealias TunnelProviderManagerType = SimulatorTunnelProviderManager +#else +typealias TunnelProviderManagerType = NETunnelProviderManager +#endif + +protocol TunnelStatusObserver { + func tunnel(_ tunnel: Tunnel, didReceiveStatus status: NEVPNStatus) +} + +/// Tunnel wrapper class. +class Tunnel { + /// Tunnel provider manager. + fileprivate let tunnelProvider: TunnelProviderManagerType + + /// Tunnel start date. + /// + /// It's set to `distantPast` when the VPN connection was established prior to being observed + /// by the class. + var startDate: Date? { + lock.lock() + defer { lock.unlock() } + + return _startDate + } + + /// Tunnel connection status. + var status: NEVPNStatus { + return tunnelProvider.connection.status + } + + /// Whether on-demand VPN is enabled. + var isOnDemandEnabled: Bool { + get { + return tunnelProvider.isOnDemandEnabled + } + set { + tunnelProvider.isOnDemandEnabled = newValue + } + } + + private let lock = NSLock() + private var observerList = ObserverList<TunnelStatusObserver>() + + private var _startDate: Date? + + init(tunnelProvider: TunnelProviderManagerType) { + self.tunnelProvider = tunnelProvider + + NotificationCenter.default.addObserver( + self, selector: #selector(handleVPNStatusChangeNotification(_:)), + name: .NEVPNStatusDidChange, + object: tunnelProvider.connection + ) + + handleVPNStatus(tunnelProvider.connection.status) + } + + func start(options: [String: NSObject]?) throws { + try tunnelProvider.connection.startVPNTunnel(options: options) + } + + func stop() { + tunnelProvider.connection.stopVPNTunnel() + } + + func sendProviderMessage(_ messageData: Data, responseHandler: ((Data?) -> Void)?) throws { + let session = tunnelProvider.connection as! VPNTunnelProviderSessionProtocol + + try session.sendProviderMessage(messageData, responseHandler: responseHandler) + } + + func saveToPreferences(_ completion: @escaping (Error?) -> Void) { + tunnelProvider.saveToPreferences(completionHandler: completion) + } + + func removeFromPreferences(completion: @escaping (Error?) -> Void) { + tunnelProvider.removeFromPreferences(completionHandler: completion) + } + + func addBlockObserver(queue: DispatchQueue? = nil, handler: @escaping (Tunnel, NEVPNStatus) -> Void) -> StatusBlockObserver { + let observer = StatusBlockObserver(tunnel: self, queue: queue, handler: handler) + + addObserver(observer) + + return observer + } + + func addObserver(_ observer: TunnelStatusObserver) { + observerList.append(observer) + } + + func removeObserver(_ observer: TunnelStatusObserver) { + observerList.remove(observer) + } + + @objc private func handleVPNStatusChangeNotification(_ notification: Notification) { + guard let connection = notification.object as? VPNConnectionProtocol else { return } + + let newStatus = connection.status + + handleVPNStatus(newStatus) + + observerList.forEach { observer in + observer.tunnel(self, didReceiveStatus: newStatus) + } + } + + private func handleVPNStatus(_ status: NEVPNStatus) { + switch status { + case .connecting: + lock.lock() + _startDate = Date() + lock.unlock() + + case .connected, .reasserting: + lock.lock() + if _startDate == nil { + _startDate = .distantPast + } + lock.unlock() + + case .disconnecting: + break + + case .disconnected, .invalid: + lock.lock() + _startDate = nil + lock.unlock() + + @unknown default: + break + } + } +} + +extension Tunnel: Equatable { + static func == (lhs: Tunnel, rhs: Tunnel) -> Bool { + return lhs.tunnelProvider == rhs.tunnelProvider + } +} + +extension Tunnel { + + final class StatusBlockObserver: TunnelStatusObserver { + typealias Handler = (Tunnel, NEVPNStatus) -> Void + + private weak var tunnel: Tunnel? + private let queue: DispatchQueue? + private let lock = NSLock() + private var handler: Handler? + + fileprivate init(tunnel: Tunnel, queue: DispatchQueue?, handler: @escaping Handler) { + self.tunnel = tunnel + self.queue = queue + self.handler = handler + } + + func invalidate() { + lock.lock() + handler = nil + lock.unlock() + + tunnel?.removeObserver(self) + } + + func tunnel(_ tunnel: Tunnel, didReceiveStatus status: NEVPNStatus) { + if let queue = queue { + queue.async { + self.invokeHandler(tunnel: tunnel, status: status) + } + } else { + invokeHandler(tunnel: tunnel, status: status) + } + } + + private func invokeHandler(tunnel: Tunnel, status: NEVPNStatus) { + lock.lock() + let block = handler + lock.unlock() + + block?(tunnel, status) + } + } + +} 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/TunnelIPCRequestOperation.swift b/ios/MullvadVPN/TunnelIPC/TunnelIPCRequestOperation.swift index 0b74f6c737..c634b679e6 100644 --- a/ios/MullvadVPN/TunnelIPC/TunnelIPCRequestOperation.swift +++ b/ios/MullvadVPN/TunnelIPC/TunnelIPCRequestOperation.swift @@ -12,9 +12,9 @@ import NetworkExtension extension TunnelIPC { struct RequestOptions { - /// Wait until the tunnel transitioned from reasserting to connected state before sending - /// the request. - var waitIfReasserting: Bool + /// Delay for sending IPC requests to the tunnel when in connecting state. + /// Used to workaround a bug when talking to the tunnel too early may cause it to freeze. + static let connectingStateWaitDelay: TimeInterval = 5 /// Timeout interval in seconds. var timeout: TimeInterval = 5 @@ -25,30 +25,30 @@ extension TunnelIPC { typealias CompletionHandler = (OperationCompletion<Output, TunnelIPC.Error>) -> Void private let queue: DispatchQueue - private let notificationQueue: OperationQueue - private let connection: VPNConnectionProtocol + private let tunnel: Tunnel private let request: TunnelIPC.Request private let options: RequestOptions private let decoderHandler: DecoderHandler private var completionHandler: CompletionHandler? - private var statusObserver: NSObjectProtocol? - private var timeoutTimer: DispatchSourceTimer? + private var statusObserver: Tunnel.StatusBlockObserver? + private var timeoutWork: DispatchWorkItem? + private var waitForConnectingStateWork: DispatchWorkItem? + + private var requestSent = false init(queue: DispatchQueue, - connection: VPNConnectionProtocol, + tunnel: Tunnel, request: TunnelIPC.Request, options: TunnelIPC.RequestOptions, decoderHandler: @escaping DecoderHandler, completionHandler: @escaping CompletionHandler) { self.queue = queue - self.notificationQueue = OperationQueue() - self.notificationQueue.underlyingQueue = queue - self.connection = connection + self.tunnel = tunnel self.request = request self.options = options @@ -78,47 +78,39 @@ extension TunnelIPC { return } - startTimeoutTimer() - - statusObserver = NotificationCenter.default.addObserver( - forName: .NEVPNStatusDidChange, - object: connection, - queue: notificationQueue) { [weak self] notification in - guard let self = self else { return } - guard let connection = notification.object as? VPNConnectionProtocol else { return } + setTimeoutTimer(connectingStateWaitDelay: 0) - self.handleVPNStatus(connection.status) - } + statusObserver = tunnel.addBlockObserver(queue: queue) { [weak self] tunnel, status in + self?.handleVPNStatus(status) + } - handleVPNStatus(connection.status) + handleVPNStatus(tunnel.status) } private func removeVPNStatusObserver() { - if let statusObserver = statusObserver { - NotificationCenter.default.removeObserver(statusObserver) - self.statusObserver = nil - } + statusObserver?.invalidate() + statusObserver = nil } - private func startTimeoutTimer() { - let timer = DispatchSource.makeTimerSource(queue: queue) - timer.setEventHandler { [weak self] in + private func setTimeoutTimer(connectingStateWaitDelay: TimeInterval) { + let workItem = DispatchWorkItem { [weak self] in self?.completeOperation(completion: .failure(.send(.timeout))) } - timer.schedule(wallDeadline: .now() + options.timeout) - timer.activate() + // Cancel pending timeout work. + timeoutWork?.cancel() - timeoutTimer = timer - } + // Assign new timeout work. + timeoutWork = workItem - private func stopTimeoutTimer() { - timeoutTimer?.cancel() - timeoutTimer = nil + // Schedule timeout work. + let deadline: DispatchWallTime = .now() + options.timeout + connectingStateWaitDelay + + queue.asyncAfter(wallDeadline: deadline, execute: workItem) } private func handleVPNStatus(_ status: NEVPNStatus) { - guard !isCancelled else { + guard !isCancelled && !requestSent else { return } @@ -127,14 +119,12 @@ extension TunnelIPC { sendRequest() case .connecting: - // Sending IPC message while in connecting state may cause the tunnel process to - // freeze for no apparent reason. - break + waitForConnectingState { [weak self] in + self?.sendRequest() + } case .reasserting: - if !options.waitIfReasserting { - sendRequest() - } + sendRequest() case .invalid, .disconnecting, .disconnected: completeOperation(completion: .failure(.send(.tunnelDown(status)))) @@ -144,11 +134,51 @@ extension TunnelIPC { } } + private func waitForConnectingState(block: @escaping () -> Void) { + // Compute amount of time elapsed since the tunnel was launched. + let timeElapsed: TimeInterval + if let startDate = tunnel.startDate { + timeElapsed = Date().timeIntervalSince(startDate) + } else { + timeElapsed = 0 + } + + // Cancel pending work. + waitForConnectingStateWork?.cancel() + waitForConnectingStateWork = nil + + // Execute right away if enough time passed since the tunnel was launched. + guard timeElapsed < RequestOptions.connectingStateWaitDelay else { + block() + return + } + + let waitDelay = RequestOptions.connectingStateWaitDelay - timeElapsed + let workItem = DispatchWorkItem(block: block) + + // Assign new work. + waitForConnectingStateWork = workItem + + // Reschedule the timeout work. + setTimeoutTimer(connectingStateWaitDelay: waitDelay) + + // Schedule delayed work. + let deadline: DispatchWallTime = .now() + waitDelay + + queue.asyncAfter(wallDeadline: deadline, execute: workItem) + } + private func sendRequest() { - let session = connection as! VPNTunnelProviderSessionProtocol + // Mark request sent. + requestSent = true + // Release status observer. removeVPNStatusObserver() + // Cancel pending delayed work. + waitForConnectingStateWork?.cancel() + + // Encode request. let messageData: Data do { messageData = try TunnelIPC.Coding.encodeRequest(request) @@ -157,8 +187,9 @@ extension TunnelIPC { return } + // Send IPC message. do { - try session.sendProviderMessage(messageData) { [weak self] responseData in + try tunnel.sendProviderMessage(messageData) { [weak self] responseData in guard let self = self else { return } self.queue.async { @@ -173,12 +204,18 @@ extension TunnelIPC { } private func completeOperation(completion: OperationCompletion<Output, TunnelIPC.Error>) { + // Release status observer. removeVPNStatusObserver() - stopTimeoutTimer() + // Cancel pending work. + timeoutWork?.cancel() + waitForConnectingStateWork?.cancel() + + // Call completion handler. completionHandler?(completion) completionHandler = nil + // Finish operation. finish() } } @@ -187,7 +224,7 @@ extension TunnelIPC { extension TunnelIPC.RequestOperation where Output: Codable { convenience init( queue: DispatchQueue, - connection: VPNConnectionProtocol, + tunnel: Tunnel, request: TunnelIPC.Request, options: TunnelIPC.RequestOptions, completionHandler: @escaping CompletionHandler @@ -195,7 +232,7 @@ extension TunnelIPC.RequestOperation where Output: Codable { { self.init( queue: queue, - connection: connection, + tunnel: tunnel, request: request, options: options, decoderHandler: { data in @@ -215,14 +252,14 @@ extension TunnelIPC.RequestOperation where Output: Codable { extension TunnelIPC.RequestOperation where Output == Void { convenience init( queue: DispatchQueue, - connection: VPNConnectionProtocol, + tunnel: Tunnel, request: TunnelIPC.Request, options: TunnelIPC.RequestOptions, completionHandler: @escaping CompletionHandler ) { self.init( queue: queue, - connection: connection, + tunnel: tunnel, request: request, options: options, decoderHandler: { _ in .success(()) }, diff --git a/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift b/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift index 4b12b15db4..ce430b5b81 100644 --- a/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift +++ b/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift @@ -13,20 +13,20 @@ extension TunnelIPC { /// Wrapper class around `NETunnelProviderSession` that provides convenient interface for /// interacting with the Packet Tunnel process. final class Session { - private let connection: VPNConnectionProtocol + private let tunnel: Tunnel private let queue = DispatchQueue(label: "TunnelIPC.SessionQueue") private let operationQueue = OperationQueue() - init(connection: VPNConnectionProtocol) { - self.connection = connection + init(tunnel: Tunnel) { + self.tunnel = tunnel } func reloadTunnelSettings(completionHandler: @escaping (OperationCompletion<(), TunnelIPC.Error>) -> Void) -> Cancellable { let operation = RequestOperation( queue: queue, - connection: connection, + tunnel: tunnel, request: .reloadTunnelSettings, - options: TunnelIPC.RequestOptions(waitIfReasserting: true), + options: TunnelIPC.RequestOptions(), completionHandler: completionHandler ) @@ -37,12 +37,12 @@ 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, - connection: connection, - request: .tunnelConnectionInfo, - options: TunnelIPC.RequestOptions(waitIfReasserting: false), + tunnel: tunnel, + request: .getTunnelStatus, + options: TunnelIPC.RequestOptions(), completionHandler: completionHandler ) diff --git a/ios/MullvadVPN/TunnelManager/LoadTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/LoadTunnelOperation.swift index 8cdb64bcab..fc393b7494 100644 --- a/ios/MullvadVPN/TunnelManager/LoadTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/LoadTunnelOperation.swift @@ -118,7 +118,7 @@ class LoadTunnelOperation: AsyncOperation { let tunnelInfo = TunnelInfo(token: accountToken, tunnelSettings: keychainEntry.tunnelSettings) state.tunnelInfo = tunnelInfo - state.setTunnelProvider(tunnelProvider, shouldRefreshTunnelState: true) + state.setTunnel(Tunnel(tunnelProvider: tunnelProvider), shouldRefreshTunnelState: true) completionHandler(.success(())) diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift index 6292b831b7..bef06ac128 100644 --- a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift +++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift @@ -43,31 +43,49 @@ class MapConnectionStatusOperation: AsyncOperation { } private func execute() { - guard let tunnelProvider = state.tunnelProvider, !isCancelled else { + guard let tunnel = state.tunnel, !isCancelled else { finish() return } - let tunnelState = state.tunnelState + let tunnelState = state.tunnelStatus.state switch connectionStatus { case .connecting: switch tunnelState { case .connecting(.some(_)): - logger.debug("Ignore repeating connecting state.") + break default: - state.tunnelState = .connecting(nil) + state.tunnelStatus.state = .connecting(nil) + } + + let session = TunnelIPC.Session(tunnel: tunnel) + + request = session.getTunnelStatus { [weak self] completion in + guard let self = self else { return } + + self.queue.async { + if case .success(let packetTunnelStatus) = completion, !self.isCancelled { + self.state.tunnelStatus.update(from: packetTunnelStatus) { relay in + return .connecting(relay) + } + } + + self.finish() + } } case .reasserting: - let session = TunnelIPC.Session(connection: tunnelProvider.connection) + 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() @@ -77,14 +95,16 @@ class MapConnectionStatusOperation: AsyncOperation { return case .connected: - let session = TunnelIPC.Session(connection: tunnelProvider.connection) + 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() @@ -101,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: @@ -115,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/ReloadTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift index 3758c9e4e9..090a29f59f 100644 --- a/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift @@ -29,12 +29,12 @@ class ReloadTunnelOperation: AsyncOperation { return } - guard let tunnelProvider = self.state.tunnelProvider else { + guard let tunnel = self.state.tunnel else { self.completeOperation(completion: .failure(.unsetAccount)) return } - let session = TunnelIPC.Session(connection: tunnelProvider.connection) + let session = TunnelIPC.Session(tunnel: tunnel) self.request = session.reloadTunnelSettings { [weak self] completion in guard let self = self else { return } diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift index f09ee90f68..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 @@ -182,13 +182,13 @@ class SetAccountOperation: AsyncOperation { } // Finish immediately if tunnel provider is not set. - guard let tunnelProvider = state.tunnelProvider else { + guard let tunnel = state.tunnel else { completionHandler() return } // Remove VPN configuration - tunnelProvider.removeFromPreferences { error in + tunnel.removeFromPreferences { error in self.queue.async { // Ignore error but log it if let error = error { @@ -198,7 +198,7 @@ class SetAccountOperation: AsyncOperation { ) } - self.state.setTunnelProvider(nil, shouldRefreshTunnelState: false) + self.state.setTunnel(nil, shouldRefreshTunnelState: false) completionHandler() } diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift index 1037253da5..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(())) @@ -114,8 +114,8 @@ class StartTunnelOperation: AsyncOperation { encodeErrorHandler = nil - state.setTunnelProvider(tunnelProvider, shouldRefreshTunnelState: false) - state.tunnelState = .connecting(selectorResult.tunnelConnectionInfo) + state.setTunnel(Tunnel(tunnelProvider: tunnelProvider), shouldRefreshTunnelState: false) + 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 297805eb8e..377d946399 100644 --- a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift @@ -38,27 +38,27 @@ class StopTunnelOperation: AsyncOperation { return } - guard let tunnelProvider = state.tunnelProvider else { + guard let tunnel = state.tunnel else { completionHandler(.failure(.unsetAccount)) return } - switch self.state.tunnelState { + switch self.state.tunnelStatus.state { case .disconnecting(.reconnect): - state.tunnelState = .disconnecting(.nothing) + state.tunnelStatus.state = .disconnecting(.nothing) completionHandler(.success(())) - case .connected, .connecting: + case .connected, .connecting, .reconnecting: // Disable on-demand when stopping the tunnel to prevent it from coming back up - tunnelProvider.isOnDemandEnabled = false + tunnel.isOnDemandEnabled = false - tunnelProvider.saveToPreferences { error in + tunnel.saveToPreferences { error in self.queue.async { if let error = error { completionHandler(.failure(.saveVPNConfiguration(error))) } else { - tunnelProvider.connection.stopVPNTunnel() + tunnel.stop() completionHandler(.success(())) } } diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 96cf744505..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" @@ -43,17 +59,25 @@ class TunnelManager: TunnelManagerStateDelegate private let operationQueue = OperationQueue() private let exclusivityController = ExclusivityController() + private var statusObserver: Tunnel.StatusBlockObserver? private var lastMapConnectionStatusOperation: Operation? private let observerList = ObserverList<TunnelObserver>() private let state: TunnelManager.State + 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) { @@ -71,14 +95,11 @@ class TunnelManager: TunnelManagerStateDelegate // MARK: - Periodic private key rotation - private var privateKeyRotationTimer: DispatchSourceTimer? - private var isRunningPeriodicPrivateKeyRotation = false - func startPeriodicPrivateKeyRotation() { 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 @@ -90,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 @@ -106,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 { @@ -258,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?() } @@ -357,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)) @@ -449,63 +475,76 @@ 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) } } } - func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelProvider newTunnelProvider: TunnelProviderManagerType?, shouldRefreshTunnelState: Bool) { + func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelProvider newTunnelObject: Tunnel?, shouldRefreshTunnelState: Bool) { dispatchPrecondition(condition: .onQueue(stateQueue)) // Register for tunnel connection status changes - if let newTunnelProvider = newTunnelProvider { - subscribeVPNStatusObserver(for: newTunnelProvider) + if let newTunnelObject = newTunnelObject { + subscribeVPNStatusObserver(tunnel: newTunnelObject) } else { unsubscribeVPNStatusObserver() } // Update the existing state if shouldRefreshTunnelState { - updateTunnelState() + logger.debug("Refresh tunnel status for new tunnel.") + refreshTunnelStatus() } } // MARK: - Private methods - private func subscribeVPNStatusObserver(for tunnelProvider: TunnelProviderManagerType) { + private func subscribeVPNStatusObserver(tunnel: Tunnel) { unsubscribeVPNStatusObserver() - NotificationCenter.default.addObserver( - self, selector: #selector(didReceiveVPNStatusChange(_:)), - name: .NEVPNStatusDidChange, - object: tunnelProvider.connection - ) + statusObserver = tunnel.addBlockObserver(queue: stateQueue) { [weak self] tunnel, status in + guard let self = self else { return } + + self.logger.debug("VPN connection status changed to \(status).") + self.updateTunnelStatus(status) + } } private func unsubscribeVPNStatusObserver() { - NotificationCenter.default.removeObserver(self, name: .NEVPNStatusDidChange, object: nil) + statusObserver?.invalidate() + statusObserver = nil } - @objc private func didReceiveVPNStatusChange(_ notification: Notification) { - stateQueue.async { - self.updateTunnelState() + private func refreshTunnelStatus() { + dispatchPrecondition(condition: .onQueue(stateQueue)) + + if let connectionStatus = self.state.tunnel?.status { + updateTunnelStatus(connectionStatus) } } - /// Update `TunnelState` from `NEVPNStatus`. - /// Collects the `TunnelConnectionInfo` from the tunnel via IPC if needed before assigning the `tunnelState` - private func updateTunnelState() { + /// 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)) - guard let connectionStatus = self.state.tunnelProvider?.connection.status else { return } - - logger.debug("VPN status changed to \(connectionStatus)") - let operation = MapConnectionStatusOperation(queue: stateQueue, state: state, connectionStatus: connectionStatus) { [weak self] in guard let self = self else { return } @@ -525,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() } } @@ -598,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 { @@ -644,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 { @@ -710,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) } } @@ -735,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 f8ebaf86b6..3802390376 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift @@ -9,17 +9,10 @@ import Foundation import NetworkExtension -// Switch to stabs on simulator -#if targetEnvironment(simulator) -typealias TunnelProviderManagerType = SimulatorTunnelProviderManager -#else -typealias TunnelProviderManagerType = NETunnelProviderManager -#endif - protocol TunnelManagerStateDelegate: AnyObject { func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelInfo newTunnelInfo: TunnelInfo?) - func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelState newTunnelState: TunnelState) - func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelProvider newTunnelProvider: TunnelProviderManagerType?, shouldRefreshTunnelState: Bool) + func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelStatus newTunnelStatus: TunnelStatus) + func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelProvider newTunnelObject: Tunnel?, shouldRefreshTunnelState: Bool) } extension TunnelManager { @@ -31,8 +24,12 @@ extension TunnelManager { private let queueMarkerKey = DispatchSpecificKey<Bool>() private var _tunnelInfo: TunnelInfo? - private var _tunnelProvider: TunnelProviderManagerType? - private var _tunnelState: TunnelState = .disconnected + private var _tunnelObject: Tunnel? + private var _tunnelStatus = TunnelStatus( + isNetworkReachable: false, + connectingDate: nil, + state: .disconnected + ) var tunnelInfo: TunnelInfo? { get { @@ -51,24 +48,24 @@ extension TunnelManager { } } - var tunnelProvider: TunnelProviderManagerType? { + var tunnel: Tunnel? { return performBlock { - return _tunnelProvider + return _tunnelObject } } - 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) } } } @@ -84,12 +81,12 @@ extension TunnelManager { queue.setSpecific(key: queueMarkerKey, value: nil) } - func setTunnelProvider(_ newTunnelProvider: TunnelProviderManagerType?, shouldRefreshTunnelState: Bool) { + func setTunnel(_ newTunnelObject: Tunnel?, shouldRefreshTunnelState: Bool) { performBlock { - if _tunnelProvider != newTunnelProvider { - _tunnelProvider = newTunnelProvider + if _tunnelObject != newTunnelObject { + _tunnelObject = newTunnelObject - delegate?.tunnelManagerState(self, didChangeTunnelProvider: newTunnelProvider, shouldRefreshTunnelState: shouldRefreshTunnelState) + delegate?.tunnelManagerState(self, didChangeTunnelProvider: newTunnelObject, shouldRefreshTunnelState: shouldRefreshTunnelState) } } } 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> { |
