diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2020-09-02 13:53:58 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2020-09-02 13:53:58 +0200 |
| commit | 41736337874f987a28ed08289d33a0c9ce87e45a (patch) | |
| tree | dd82a653c5dcd11d8c4e5289652ee25d5c1cdd05 | |
| parent | 58ae8def1aba534b36dbb1e053debcb258c8d3e1 (diff) | |
| parent | 0bddeb9cf8139eb0ed7a32c86fc25fe460feb0e4 (diff) | |
| download | mullvadvpn-41736337874f987a28ed08289d33a0c9ce87e45a.tar.xz mullvadvpn-41736337874f987a28ed08289d33a0c9ce87e45a.zip | |
Merge branch 'fix-set-network-settings-callback'
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 6 | ||||
| -rw-r--r-- | ios/MullvadVPN/AppDelegate.swift | 11 | ||||
| -rw-r--r-- | ios/MullvadVPN/ApplicationConfiguration.swift | 27 | ||||
| -rw-r--r-- | ios/MullvadVPN/AutomaticKeyRotationManager.swift | 42 | ||||
| -rw-r--r-- | ios/MullvadVPN/ConnectViewController.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/Operations/AsyncOperation.swift | 45 | ||||
| -rw-r--r-- | ios/MullvadVPN/Optional+DispatchQueue.swift | 22 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayCache.swift | 41 | ||||
| -rw-r--r-- | ios/PacketTunnel/PacketTunnelProvider.swift | 479 | ||||
| -rw-r--r-- | ios/PacketTunnel/WireguardDevice.swift | 96 |
10 files changed, 480 insertions, 293 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index cab698a459..f27bb0566a 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -61,6 +61,8 @@ 582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */; }; 582BB1B52295780F0055B6EF /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B42295780F0055B6EF /* AccountExpiry.swift */; }; 5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5835B7CB233B76CB0096D79F /* TunnelManager.swift */; }; + 583BC70724FE4DC500C9DE04 /* Optional+DispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583BC70624FE4DC400C9DE04 /* Optional+DispatchQueue.swift */; }; + 583BC70824FE4DC500C9DE04 /* Optional+DispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583BC70624FE4DC400C9DE04 /* Optional+DispatchQueue.swift */; }; 5840250122B1124600E4CFEC /* IpAddress+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250022B1124600E4CFEC /* IpAddress+Codable.swift */; }; 5840250222B1124600E4CFEC /* IpAddress+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250022B1124600E4CFEC /* IpAddress+Codable.swift */; }; 5840250422B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */; }; @@ -288,6 +290,7 @@ 582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountCell.swift; sourceTree = "<group>"; }; 582BB1B42295780F0055B6EF /* AccountExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiry.swift; sourceTree = "<group>"; }; 5835B7CB233B76CB0096D79F /* TunnelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManager.swift; sourceTree = "<group>"; }; + 583BC70624FE4DC400C9DE04 /* Optional+DispatchQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+DispatchQueue.swift"; sourceTree = "<group>"; }; 5840250022B1124600E4CFEC /* IpAddress+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IpAddress+Codable.swift"; sourceTree = "<group>"; }; 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadEndpoint.swift; sourceTree = "<group>"; }; 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelIpc.swift; sourceTree = "<group>"; }; @@ -612,6 +615,7 @@ 58C6B35322BB87C4003C19AD /* WireguardPrivateKey.swift */, 58F3C098249B978C003E76BE /* x25519.c */, 58F3C097249B978C003E76BE /* x25519.h */, + 583BC70624FE4DC400C9DE04 /* Optional+DispatchQueue.swift */, ); path = MullvadVPN; sourceTree = "<group>"; @@ -1002,6 +1006,7 @@ 580EE22124B3240100F9D8A1 /* TransformOperationObserver.swift in Sources */, 582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */, 5873884D239E6D7E00E96C4E /* EmbeddedViewContainerView.swift in Sources */, + 583BC70724FE4DC500C9DE04 /* Optional+DispatchQueue.swift in Sources */, 58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */, 585FE2F124E1365400439C50 /* LogStreamer.swift in Sources */, 58B9EB132488ED2100095626 /* AlertPresenter.swift in Sources */, @@ -1096,6 +1101,7 @@ 581503A424D6F1EC00C9C50E /* ChainedError+Logger.swift in Sources */, 5815039824D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */, 5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */, + 583BC70824FE4DC500C9DE04 /* Optional+DispatchQueue.swift in Sources */, 58BFA5C722A7C97F00A6173D /* RelayCache.swift in Sources */, 580EE21024B322E700F9D8A1 /* TransformOperation.swift in Sources */, 58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 2ee783dad0..d54ac731fc 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -21,9 +21,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let simulatorTunnelProvider = SimulatorTunnelProviderHost() #endif + #if DEBUG + private let packetTunnelLogForwarder = LogStreamer<UTF8>(fileURLs: [ApplicationConfiguration.packetTunnelLogFileURL!]) + #endif + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { initLoggingSystem(bundleIdentifier: Bundle.main.bundleIdentifier!) + #if DEBUG + let stdoutStream = TextFileOutputStream.standardOutputStream() + packetTunnelLogForwarder.start { (str) in + stdoutStream.write("\(str)\n") + } + #endif + #if targetEnvironment(simulator) SimulatorTunnelProvider.shared.delegate = simulatorTunnelProvider #endif diff --git a/ios/MullvadVPN/ApplicationConfiguration.swift b/ios/MullvadVPN/ApplicationConfiguration.swift index 8dc5063fcb..1052df9851 100644 --- a/ios/MullvadVPN/ApplicationConfiguration.swift +++ b/ios/MullvadVPN/ApplicationConfiguration.swift @@ -16,16 +16,23 @@ class ApplicationConfiguration { /// The application identifier for the PacketTunnel extension static let packetTunnelExtensionIdentifier = "net.mullvad.MullvadVPN.PacketTunnel" - /// The application log files - static var logFileURLs: [URL] { - let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Self.securityGroupIdentifier) - let fileNames = ["net.mullvad.MullvadVPN", "net.mullvad.MullvadVPN.PacketTunnel"] + /// Container URL for security group + static var containerURL: URL? { + return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Self.securityGroupIdentifier) + } + + /// The main application log file located in a shared container + static var mainApplicationLogFileURL: URL? { + return Self.containerURL?.appendingPathComponent("Logs/net.mullvad.MullvadVPN.log", isDirectory: false) + } - return fileNames.compactMap { (fileName) -> URL? in - return containerURL? - .appendingPathComponent("Logs", isDirectory: true) - .appendingPathComponent(fileName, isDirectory: false) - .appendingPathExtension("log") - } + /// The packet tunnel log file located in a shared container + static var packetTunnelLogFileURL: URL? { + return Self.containerURL?.appendingPathComponent("Logs/net.mullvad.MullvadVPN.PacketTunnel.log", isDirectory: false) + } + + /// All log files located in a shared container + static var logFileURLs: [URL] { + return [mainApplicationLogFileURL, packetTunnelLogFileURL].compactMap { $0 } } } diff --git a/ios/MullvadVPN/AutomaticKeyRotationManager.swift b/ios/MullvadVPN/AutomaticKeyRotationManager.swift index 79e435a2a6..a6c7466ba8 100644 --- a/ios/MullvadVPN/AutomaticKeyRotationManager.swift +++ b/ios/MullvadVPN/AutomaticKeyRotationManager.swift @@ -69,6 +69,9 @@ class AutomaticKeyRotationManager { /// A variable backing the `eventHandler` public property private var _eventHandler: ((KeyRotationResult) -> Void)? + /// A dispatch queue used for broadcasting events + private let eventQueue: DispatchQueue? + /// An event handler that's invoked when key rotation occurred var eventHandler: ((KeyRotationResult) -> Void)? { get { @@ -83,37 +86,38 @@ class AutomaticKeyRotationManager { } } - init(persistentKeychainReference: Data) { + init(persistentKeychainReference: Data, eventQueue: DispatchQueue?) { self.persistentKeychainReference = persistentKeychainReference + self.eventQueue = eventQueue } - func startAutomaticRotation(completionHandler: @escaping () -> Void) { + func startAutomaticRotation(queue: DispatchQueue?, completionHandler: @escaping () -> Void) { dispatchQueue.async { - guard !self.isAutomaticRotationEnabled else { return } - - self.logger.info("Start automatic key rotation") + if !self.isAutomaticRotationEnabled { + self.logger.info("Start automatic key rotation") - self.isAutomaticRotationEnabled = true - self.performKeyRotation() + self.isAutomaticRotationEnabled = true + self.performKeyRotation() + } - completionHandler() + queue.performOnWrappedOrCurrentQueue(block: completionHandler) } } - func stopAutomaticRotation(completionHandler: @escaping () -> Void) { + func stopAutomaticRotation(queue: DispatchQueue?, completionHandler: @escaping () -> Void) { dispatchQueue.async { - guard self.isAutomaticRotationEnabled else { return } - - self.logger.info("Stop automatic key rotation") + if self.isAutomaticRotationEnabled { + self.logger.info("Stop automatic key rotation") - self.isAutomaticRotationEnabled = false + self.isAutomaticRotationEnabled = false - self.dataTask?.cancel() - self.dataTask = nil + self.dataTask?.cancel() + self.dataTask = nil - self.timerSource?.cancel() + self.timerSource?.cancel() + } - completionHandler() + queue.performOnWrappedOrCurrentQueue(block: completionHandler) } } @@ -215,7 +219,9 @@ class AutomaticKeyRotationManager { if event.isNew { logger.info("Finished private key rotation") - eventHandler?(event) + eventQueue.performOnWrappedOrCurrentQueue { + self.eventHandler?(event) + } } if let rotationDate = Self.nextRotation(creationDate: event.creationDate) { diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift index 5b36d20932..c46ee0270d 100644 --- a/ios/MullvadVPN/ConnectViewController.swift +++ b/ios/MullvadVPN/ConnectViewController.swift @@ -123,7 +123,7 @@ class ConnectViewController: UIViewController, RootContainment, TunnelObserver, private func updateButtons() { switch tunnelState { - case .disconnected: + case .disconnected, .disconnecting: selectLocationButton.setTitle(NSLocalizedString("Select location", comment: ""), for: .normal) connectButton.setTitle(NSLocalizedString("Secure connection", comment: ""), for: .normal) @@ -135,7 +135,7 @@ class ConnectViewController: UIViewController, RootContainment, TunnelObserver, setArrangedButtons([selectLocationButton, splitDisconnectButtonView]) - case .connected, .reconnecting, .disconnecting: + case .connected, .reconnecting: selectLocationButton.setTitle(NSLocalizedString("Switch location", comment: ""), for: .normal) splitDisconnectButtonView.primaryButton.setTitle(NSLocalizedString("Disconnect", comment: ""), for: .normal) diff --git a/ios/MullvadVPN/Operations/AsyncOperation.swift b/ios/MullvadVPN/Operations/AsyncOperation.swift index 98a5f8aa39..7edd3bbdd4 100644 --- a/ios/MullvadVPN/Operations/AsyncOperation.swift +++ b/ios/MullvadVPN/Operations/AsyncOperation.swift @@ -11,9 +11,16 @@ import Foundation /// A base implementation of an asynchronous operation class AsyncOperation: Operation, OperationProtocol { + /// A state transaction lock used to perform critical sections of code within `start`, `cancel` + /// and `finish` calls. + fileprivate let transactionLock = NSRecursiveLock() + /// A state lock used for manipulating the operation state flags in a thread safe fashion. fileprivate let stateLock = NSRecursiveLock() + /// The operation observers. + fileprivate var observers: [AnyOperationObserver<AsyncOperation>] = [] + /// Operation state flags. private var _isExecuting = false private var _isFinished = false @@ -36,8 +43,8 @@ class AsyncOperation: Operation, OperationProtocol { } final override func start() { - stateLock.withCriticalBlock { - if self._isCancelled { + transactionLock.withCriticalBlock { + if self.isCancelled { self.finish() } else { self.setExecuting(true) @@ -53,11 +60,17 @@ class AsyncOperation: Operation, OperationProtocol { /// Cancel operation /// Subclasses should override `operationDidCancel` instead final override func cancel() { - stateLock.withCriticalBlock { - if !self._isCancelled { + transactionLock.withCriticalBlock { + if self.isCancelled { + super.cancel() + } else { self.setCancelled(true) - if self._isExecuting { + super.cancel() + + // Only notify the operation about cancellation when it is already running, + // otherwise the call to `start` should automatically `finish()` the operation. + if self.isExecuting { self.operationDidCancel() } } @@ -71,17 +84,20 @@ class AsyncOperation: Operation, OperationProtocol { } final func finish() { - stateLock.withCriticalBlock { - if !self._isFinished { + transactionLock.withCriticalBlock { + guard !self.isFinished else { return } + + self.stateLock.withCriticalBlock { self.observers.forEach { $0.operationWillFinish(self) } } - if self._isExecuting { + if self.isExecuting { self.setExecuting(false) } - if !self._isFinished { - self.setFinished(true) + self.setFinished(true) + + self.stateLock.withCriticalBlock { self.observers.forEach { $0.operationDidFinish(self) } } } @@ -89,27 +105,24 @@ class AsyncOperation: Operation, OperationProtocol { private func setExecuting(_ value: Bool) { willChangeValue(for: \.isExecuting) - _isExecuting = value + stateLock.withCriticalBlock { _isExecuting = value } didChangeValue(for: \.isExecuting) } private func setFinished(_ value: Bool) { willChangeValue(for: \.isFinished) - _isFinished = value + stateLock.withCriticalBlock { _isFinished = value } didChangeValue(for: \.isFinished) } private func setCancelled(_ value: Bool) { willChangeValue(for: \.isCancelled) - _isCancelled = value + stateLock.withCriticalBlock { _isCancelled = value } didChangeValue(for: \.isCancelled) } // MARK: - Observation - /// The operation observers. - fileprivate var observers: [AnyOperationObserver<AsyncOperation>] = [] - /// Add type-erased operation observer fileprivate func addAnyObserver(_ observer: AnyOperationObserver<AsyncOperation>) { stateLock.withCriticalBlock { diff --git a/ios/MullvadVPN/Optional+DispatchQueue.swift b/ios/MullvadVPN/Optional+DispatchQueue.swift new file mode 100644 index 0000000000..34402977ec --- /dev/null +++ b/ios/MullvadVPN/Optional+DispatchQueue.swift @@ -0,0 +1,22 @@ +// +// Optional+DispatchQueue.swift +// MullvadVPN +// +// Created by pronebird on 01/09/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +extension Optional where Wrapped == DispatchQueue { + /// Unwrap the `DispatchQueue` and perform the block on it, otherwise call the block + /// synchronously on the current queue when `Optional` is `.none`. + func performOnWrappedOrCurrentQueue(block: @escaping () -> Void) { + switch self { + case .some(let queue): + queue.async(execute: block) + case .none: + block() + } + } +} diff --git a/ios/MullvadVPN/RelayCache.swift b/ios/MullvadVPN/RelayCache.swift index 802e278c14..c561303e49 100644 --- a/ios/MullvadVPN/RelayCache.swift +++ b/ios/MullvadVPN/RelayCache.swift @@ -110,35 +110,34 @@ class RelayCache { self.cacheFileURL = cacheFileURL } - func startPeriodicUpdates(completionHandler: (() -> Void)?) { + func startPeriodicUpdates(queue: DispatchQueue?, completionHandler: (() -> Void)?) { dispatchQueue.async { - guard !self.isPeriodicUpdatesEnabled else { - completionHandler?() - return - } - - self.isPeriodicUpdatesEnabled = true + if !self.isPeriodicUpdatesEnabled { + self.isPeriodicUpdatesEnabled = true - switch Self.read(cacheFileURL: self.cacheFileURL) { - case .success(let cachedRelayList): - if let nextUpdate = Self.nextUpdateDate(lastUpdatedAt: cachedRelayList.updatedAt) { - let startTime = Self.makeWalltime(fromDate: nextUpdate) - self.scheduleRepeatingTimer(startTime: startTime) - } + switch Self.read(cacheFileURL: self.cacheFileURL) { + case .success(let cachedRelayList): + if let nextUpdate = Self.nextUpdateDate(lastUpdatedAt: cachedRelayList.updatedAt) { + let startTime = Self.makeWalltime(fromDate: nextUpdate) + self.scheduleRepeatingTimer(startTime: startTime) + } - case .failure(let readError): - self.logger.error(chainedError: readError, message: "Failed to read the relay cache") + case .failure(let readError): + self.logger.error(chainedError: readError, message: "Failed to read the relay cache") - if Self.shouldDownloadRelaysOnReadFailure(readError) { - self.scheduleRepeatingTimer(startTime: .now()) + if Self.shouldDownloadRelaysOnReadFailure(readError) { + self.scheduleRepeatingTimer(startTime: .now()) + } } } - completionHandler?() + queue.performOnWrappedOrCurrentQueue { + completionHandler?() + } } } - func stopPeriodicUpdates(completionHandler: (() -> Void)?) { + func stopPeriodicUpdates(queue: DispatchQueue?, completionHandler: (() -> Void)?) { dispatchQueue.async { self.isPeriodicUpdatesEnabled = false @@ -146,7 +145,9 @@ class RelayCache { self.timerSource = nil self.downloadTask?.cancel() - completionHandler?() + queue.performOnWrappedOrCurrentQueue { + completionHandler?() + } } } diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index eb07274955..784e3fc8fe 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -11,98 +11,6 @@ import Network import NetworkExtension import Logging -enum PacketTunnelProviderError: ChainedError { - /// Failure to read the relay cache - case readRelayCache(RelayCacheError) - - /// Failure to satisfy the relay constraint - case noRelaySatisfyingConstraint - - /// Missing the persistent keychain reference to the tunnel settings - case missingKeychainConfigurationReference - - /// Failure to read the tunnel settings from Keychain - case cannotReadTunnelSettings(TunnelSettingsManager.Error) - - /// Failure to set network settings - case setNetworkSettings(Error) - - /// Failure to start the Wireguard backend - case startWireguardDevice(WireguardDevice.Error) - - /// Failure to stop the Wireguard backend - case stopWireguardDevice(WireguardDevice.Error) - - /// Failure to update the Wireguard configuration - case updateWireguardConfiguration(Error) - - /// IPC handler failure - case ipcHandler(PacketTunnelIpcHandler.Error) - - var errorDescription: String? { - switch self { - case .readRelayCache: - return "Failure to read the relay cache" - - case .noRelaySatisfyingConstraint: - return "No relay satisfying the given constraint" - - case .missingKeychainConfigurationReference: - return "Invalid protocol configuration" - - case .cannotReadTunnelSettings: - return "Failure to read tunnel settings" - - case .setNetworkSettings: - return "Failure to set system network settings" - - case .startWireguardDevice: - return "Failure to start the WireGuard device" - - case .stopWireguardDevice: - return "Failure to stop the WireGuard device" - - case .updateWireguardConfiguration: - return "Failure to update the Wireguard configuration" - - case .ipcHandler: - return "Failure to handle the IPC request" - } - } -} - -struct PacketTunnelConfiguration { - var persistentKeychainReference: Data - var tunnelSettings: TunnelSettings - var selectorResult: RelaySelectorResult -} - -extension PacketTunnelConfiguration { - var wireguardConfig: WireguardConfiguration { - let mullvadEndpoint = selectorResult.endpoint - var peers: [AnyIPEndpoint] = [.ipv4(mullvadEndpoint.ipv4Relay)] - - if let ipv6Relay = mullvadEndpoint.ipv6Relay { - peers.append(.ipv6(ipv6Relay)) - } - - let wireguardPeers = peers.map { - WireguardPeer( - endpoint: $0, - publicKey: selectorResult.endpoint.publicKey) - } - - return WireguardConfiguration( - privateKey: tunnelSettings.interface.privateKey, - peers: wireguardPeers, - allowedIPs: [ - IPAddressRange(address: IPv4Address.any, networkPrefixLength: 0), - IPAddressRange(address: IPv6Address.any, networkPrefixLength: 0) - ] - ) - } -} - class PacketTunnelProvider: NEPacketTunnelProvider { enum OperationCategory { @@ -112,11 +20,12 @@ class PacketTunnelProvider: NEPacketTunnelProvider { /// Tunnel provider logger private let logger: Logger - /// Active wireguard device - private var wireguardDevice: WireguardDevice? - - /// Active tunnel connection information - private var connectionInfo: TunnelConnectionInfo? + /// Current tunnel state + private var tunnelState: PacketTunnelState = .disconnected { + didSet { + logger.info("New tunnel state: \(String(reflecting: self.tunnelState))") + } + } /// Internal queue private let dispatchQueue = DispatchQueue(label: "net.mullvad.MullvadVPN.PacketTunnel", qos: .utility) @@ -131,13 +40,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider { return ExclusivityController(operationQueue: self.operationQueue) }() - private var keyRotationManager: AutomaticKeyRotationManager? - override init() { initLoggingSystem(bundleIdentifier: Bundle.main.bundleIdentifier!) - WireguardDevice.setTunnelLogger(Logger(label: "WireGuard")) logger = Logger(label: "PacketTunnelProvider") + + let wireguardLogger = Logger(label: "WireGuard") + WireguardDevice.setTunnelLogger(wireguardLogger) } // MARK: - Subclass @@ -175,12 +84,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider { case .failure(let error): self.logger.error(chainedError: error, message: "Failed to stop the tunnel") } - - completionHandler() finish() } } + operation.addDidFinishBlockObserver { (op) in + completionHandler() + } + exclusivityController.addOperation(operation, categories: [.exclusive]) } @@ -198,7 +109,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } case .tunnelInformation: - self.replyAppMessage(.success(self.connectionInfo), completionHandler: completionHandler) + self.replyAppMessage(.success(self.tunnelState.tunnelConnectionInfo), completionHandler: completionHandler) } case .failure(let error): @@ -219,50 +130,60 @@ class PacketTunnelProvider: NEPacketTunnelProvider { // MARK: - Tunnel management private func doStartTunnel(completionHandler: @escaping (Result<(), PacketTunnelProviderError>) -> Void) { + self.tunnelState = .connecting(nil) + makePacketTunnelConfig { (result) in guard case .success(let packetTunnelConfig) = result else { + self.tunnelState = .disconnected + completionHandler(result.map { _ in () }) return } + self.tunnelState = .connecting(packetTunnelConfig.selectorResult.tunnelConnectionInfo) + self.updateNetworkSettings(packetTunnelConfig: packetTunnelConfig) { (result) in guard case .success = result else { + self.tunnelState = .disconnected + completionHandler(result) return } self.startWireguardDevice(packetFlow: self.packetFlow, configuration: packetTunnelConfig.wireguardConfig) { (result) in - self.dispatchQueue.async { - guard case .success(let device) = result else { - completionHandler(result.map { _ in () }) - return - } + guard case .success(let device) = result else { + self.tunnelState = .disconnected + + completionHandler(result.map { _ in () }) + return + } - let persistentKeychainReference = packetTunnelConfig.persistentKeychainReference - let keyRotationManager = AutomaticKeyRotationManager(persistentKeychainReference: persistentKeychainReference) - keyRotationManager.eventHandler = { (keyRotationEvent) in - self.dispatchQueue.async { - self.reloadTunnelSettings { (result) in - switch result { - case .success: - break + let persistentKeychainReference = packetTunnelConfig.persistentKeychainReference + let keyRotationManager = AutomaticKeyRotationManager(persistentKeychainReference: persistentKeychainReference, eventQueue: self.dispatchQueue) + keyRotationManager.eventHandler = { [weak self] (keyRotationEvent) in + guard let self = self else { return } - case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to reload tunnel settings") - } - } + self.reloadTunnelSettings { (result) in + switch result { + case .success: + break + + case .failure(let error): + self.logger.error(chainedError: error, message: "Failed to reload tunnel settings") } } + } - self.wireguardDevice = device - self.keyRotationManager = keyRotationManager + RelayCache.shared.startPeriodicUpdates(queue: self.dispatchQueue) { + keyRotationManager.startAutomaticRotation(queue: self.dispatchQueue) { + let context = PacketTunnelContext( + wireguardDevice: device, + keyRotationManager: keyRotationManager + ) - RelayCache.shared.startPeriodicUpdates { - keyRotationManager.startAutomaticRotation { - self.dispatchQueue.async { - completionHandler(.success(())) - } - } + self.tunnelState = .connected(packetTunnelConfig.selectorResult.tunnelConnectionInfo, context) + + completionHandler(.success(())) } } } @@ -271,49 +192,62 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } private func doStopTunnel(completionHandler: @escaping (Result<(), PacketTunnelProviderError>) -> Void) { - guard let device = self.wireguardDevice, let keyRotationManager = self.keyRotationManager - else { - completionHandler(.success(())) - return + guard let context = self.tunnelState.context else { + logger.warning("Cannot stop the tunnel in such state: \(self.tunnelState)") + completionHandler(.failure(.invalidTunnelState)) + return } - RelayCache.shared.stopPeriodicUpdates { - keyRotationManager.stopAutomaticRotation { - device.stop { (result) in - self.dispatchQueue.async { - self.wireguardDevice = nil - self.keyRotationManager = nil + self.tunnelState = .disconnecting - let result = result.mapError({ (error) -> PacketTunnelProviderError in - return .stopWireguardDevice(error) - }) - completionHandler(result) - } + RelayCache.shared.stopPeriodicUpdates(queue: self.dispatchQueue) { + context.keyRotationManager.stopAutomaticRotation(queue: self.dispatchQueue) { + context.wireguardDevice.stop(queue: self.dispatchQueue) { (result) in + let result = result.mapError({ (error) -> PacketTunnelProviderError in + return .stopWireguardDevice(error) + }) + + self.tunnelState = .disconnected + + completionHandler(result) } } } } private func doReloadTunnelSettings(completionHandler: @escaping (Result<(), PacketTunnelProviderError>) -> Void) { - guard let device = self.wireguardDevice else { - logger.warning("Ignore reloading tunnel settings. The WireguardDevice is not set yet.") - - completionHandler(.success(())) + guard let context = self.tunnelState.context else { + logger.warning("Cannot reload tunnel settings in such state: \(self.tunnelState)") + completionHandler(.failure(.invalidTunnelState)) return } logger.info("Reload tunnel settings") + let priorTunnelState = self.tunnelState + self.tunnelState = .reconnecting(nil, context) + makePacketTunnelConfig { (result) in guard case .success(let packetTunnelConfig) = result else { + self.tunnelState = priorTunnelState + completionHandler(result.map { _ in () }) return } + self.tunnelState = .reconnecting(packetTunnelConfig.selectorResult.tunnelConnectionInfo, context) + // Tell the system that the tunnel is about to reconnect with the new endpoint self.reasserting = true let finishReconnecting = { (result: Result<(), PacketTunnelProviderError>) in + switch result { + case .success: + self.tunnelState = .connected(packetTunnelConfig.selectorResult.tunnelConnectionInfo, context) + case .failure: + self.tunnelState = priorTunnelState + } + // Tell the system that the tunnel has finished reconnecting self.reasserting = false @@ -326,10 +260,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider { return } - device.setConfiguration(packetTunnelConfig.wireguardConfig) { (result) in - self.dispatchQueue.async { - finishReconnecting(result.mapError { PacketTunnelProviderError.updateWireguardConfiguration($0) }) - } + context.wireguardDevice.setConfiguration(packetTunnelConfig.wireguardConfig, queue: self.dispatchQueue) { (result) in + finishReconnecting(result.mapError { PacketTunnelProviderError.updateWireguardConfiguration($0) }) } } } @@ -355,34 +287,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } } - private func setTunnelConnectionInfo(selectorResult: RelaySelectorResult) { - self.connectionInfo = TunnelConnectionInfo( - ipv4Relay: selectorResult.endpoint.ipv4Relay, - ipv6Relay: selectorResult.endpoint.ipv6Relay, - hostname: selectorResult.relay.hostname, - location: selectorResult.location - ) - - logger.info("Tunnel connection info: \(selectorResult.relay.hostname)") - } - private func makePacketTunnelConfig(completionHandler: @escaping (Result<PacketTunnelConfiguration, PacketTunnelProviderError>) -> Void) { guard let keychainReference = protocolConfiguration.passwordReference else { completionHandler(.failure(.missingKeychainConfigurationReference)) return } - Self.makePacketTunnelConfig(keychainReference: keychainReference) { (result) in - self.dispatchQueue.async { - guard case .success(let packetTunnelConfig) = result else { - completionHandler(result) - return - } - - self.setTunnelConnectionInfo(selectorResult: packetTunnelConfig.selectorResult) - - completionHandler(result) - } + Self.makePacketTunnelConfig(keychainReference: keychainReference, queue: self.dispatchQueue) { (result) in + completionHandler(result) } } @@ -410,21 +322,18 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } private func reloadTunnelSettings(completionHandler: @escaping (Result<(), PacketTunnelProviderError>) -> Void) { - let operation = ResultOperation<(), PacketTunnelProviderError> { (finish) in + let operation = AsyncBlockOperation { (finish) in self.doReloadTunnelSettings { (result) in - finish(result) + completionHandler(result) + finish() } } - operation.addDidFinishBlockObserver(queue: dispatchQueue) { (operation, result) in - completionHandler(result) - } - exclusivityController.addOperation(operation, categories: [.exclusive]) } /// Returns a `PacketTunnelConfig` that contains the tunnel settings and selected relay - private class func makePacketTunnelConfig(keychainReference: Data, completionHandler: @escaping (Result<PacketTunnelConfiguration, PacketTunnelProviderError>) -> Void) { + private class func makePacketTunnelConfig(keychainReference: Data, queue: DispatchQueue?, completionHandler: @escaping (Result<PacketTunnelConfiguration, PacketTunnelProviderError>) -> Void) { switch Self.readTunnelSettings(keychainReference: keychainReference) { case .success(let tunnelSettings): Self.selectRelayEndpoint(relayConstraints: tunnelSettings.relayConstraints) { (result) in @@ -435,11 +344,16 @@ class PacketTunnelProvider: NEPacketTunnelProvider { selectorResult: selectorResult ) } - completionHandler(result) + + queue.performOnWrappedOrCurrentQueue { + completionHandler(result) + } } case .failure(let error): - completionHandler(.failure(error)) + queue.performOnWrappedOrCurrentQueue { + completionHandler(.failure(error)) + } } } @@ -482,7 +396,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { logger.info("Tunnel interface is \(tunnelDeviceName)") - device.start(configuration: configuration) { (result) in + device.start(queue: dispatchQueue, configuration: configuration) { (result) in let result = result.map { device } .mapError { PacketTunnelProviderError.startWireguardDevice($0) } @@ -490,3 +404,202 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } } } + +enum PacketTunnelProviderError: ChainedError { + /// Failure to perform operation in such state + case invalidTunnelState + + /// Failure to read the relay cache + case readRelayCache(RelayCacheError) + + /// Failure to satisfy the relay constraint + case noRelaySatisfyingConstraint + + /// Missing the persistent keychain reference to the tunnel settings + case missingKeychainConfigurationReference + + /// Failure to read the tunnel settings from Keychain + case cannotReadTunnelSettings(TunnelSettingsManager.Error) + + /// Failure to set network settings + case setNetworkSettings(Error) + + /// Failure to start the Wireguard backend + case startWireguardDevice(WireguardDevice.Error) + + /// Failure to stop the Wireguard backend + case stopWireguardDevice(WireguardDevice.Error) + + /// Failure to update the Wireguard configuration + case updateWireguardConfiguration(Error) + + /// IPC handler failure + case ipcHandler(PacketTunnelIpcHandler.Error) + + var errorDescription: String? { + switch self { + case .invalidTunnelState: + return "Failure to handle request in such tunnel state" + + case .readRelayCache: + return "Failure to read the relay cache" + + case .noRelaySatisfyingConstraint: + return "No relay satisfying the given constraint" + + case .missingKeychainConfigurationReference: + return "Keychain configuration reference is not set on protocol configuration" + + case .cannotReadTunnelSettings: + return "Failure to read tunnel settings" + + case .setNetworkSettings: + return "Failure to set system network settings" + + case .startWireguardDevice: + return "Failure to start the WireGuard device" + + case .stopWireguardDevice: + return "Failure to stop the WireGuard device" + + case .updateWireguardConfiguration: + return "Failure to update the Wireguard configuration" + + case .ipcHandler: + return "Failure to handle the IPC request" + } + } +} + +struct PacketTunnelConfiguration { + var persistentKeychainReference: Data + var tunnelSettings: TunnelSettings + var selectorResult: RelaySelectorResult +} + +extension PacketTunnelConfiguration { + var wireguardConfig: WireguardConfiguration { + let mullvadEndpoint = selectorResult.endpoint + var peers: [AnyIPEndpoint] = [.ipv4(mullvadEndpoint.ipv4Relay)] + + if let ipv6Relay = mullvadEndpoint.ipv6Relay { + peers.append(.ipv6(ipv6Relay)) + } + + let wireguardPeers = peers.map { + WireguardPeer( + endpoint: $0, + publicKey: selectorResult.endpoint.publicKey) + } + + return WireguardConfiguration( + privateKey: tunnelSettings.interface.privateKey, + peers: wireguardPeers, + allowedIPs: [ + IPAddressRange(address: IPv4Address.any, networkPrefixLength: 0), + IPAddressRange(address: IPv6Address.any, networkPrefixLength: 0) + ] + ) + } +} + +struct PacketTunnelContext { + let wireguardDevice: WireguardDevice + let keyRotationManager: AutomaticKeyRotationManager +} + +enum PacketTunnelState { + case connecting(TunnelConnectionInfo?) + case connected(TunnelConnectionInfo, PacketTunnelContext) + case disconnecting + case disconnected + case reconnecting(TunnelConnectionInfo?, PacketTunnelContext) + + var tunnelConnectionInfo: TunnelConnectionInfo? { + switch self { + case .connecting(let connectionInfo): + return connectionInfo + case .connected(let connectionInfo, _): + return connectionInfo + case .disconnecting: + return nil + case .disconnected: + return nil + case .reconnecting(let connectionInfo, _): + return connectionInfo + } + } + + var context: PacketTunnelContext? { + switch self { + case .connecting: + return nil + case .connected(_, let context): + return context + case .disconnecting: + return nil + case .disconnected: + return nil + case .reconnecting(_, let context): + return context + } + } +} + +extension PacketTunnelState: CustomStringConvertible, CustomDebugStringConvertible { + var description: String { + switch self { + case .connecting: + return "connecting" + case .connected: + return "connected" + case .disconnecting: + return "disconnecting" + case .disconnected: + return "disconnected" + case .reconnecting: + return "reconnecting" + } + } + + var debugDescription: String { + var output = "PacketTunnelState." + + switch self { + case .connecting(let connectionInfo): + output.append("connecting(") + output.append(String(reflecting: connectionInfo)) + output.append(")") + + case .connected(let connectionInfo, _): + output.append("connected(") + output.append(String(reflecting: connectionInfo)) + output.append(")") + + case .disconnecting: + output.append("disconnecting") + + case .disconnected: + output.append("disconnected") + + case .reconnecting(let connectionInfo, _): + output.append("reconnecting(") + output.append(String(reflecting: connectionInfo)) + output.append(")") + } + + return output + } +} + +extension RelaySelectorResult { + var tunnelConnectionInfo: TunnelConnectionInfo { + return TunnelConnectionInfo( + ipv4Relay: self.endpoint.ipv4Relay, + ipv6Relay: self.endpoint.ipv6Relay, + hostname: self.relay.hostname, + location: self.location + ) + } +} + diff --git a/ios/PacketTunnel/WireguardDevice.swift b/ios/PacketTunnel/WireguardDevice.swift index 5fcbe2e057..e45cf6bd4b 100644 --- a/ios/PacketTunnel/WireguardDevice.swift +++ b/ios/PacketTunnel/WireguardDevice.swift @@ -65,7 +65,7 @@ class WireguardDevice { /// A private queue used for Wireguard logging private static let loggingQueue = DispatchQueue( label: "net.mullvad.vpn.packet-tunnel.wireguard-device.global-logging-queue", - qos: .background + qos: .utility ) /// A private queue used to synchronize access to `WireguardDevice` members @@ -73,11 +73,6 @@ class WireguardDevice { label: "net.mullvad.vpn.packet-tunnel.wireguard-device.work-queue" ) - /// A private queue used for network monitor - private let networkMonitorQueue = DispatchQueue( - label: "net.mullvad.vpn.packet-tunnel.network-monitor" - ) - /// Network routes monitor private var networkMonitor: NWPathMonitor? @@ -141,14 +136,17 @@ class WireguardDevice { deinit { networkMonitor?.cancel() + stopWireguardBackend() } // MARK: - Public methods - func start(configuration: WireguardConfiguration, completionHandler: @escaping (Result<(), Error>) -> Void) { + func start(queue: DispatchQueue?, configuration: WireguardConfiguration, completionHandler: @escaping (Result<(), Error>) -> Void) { workQueue.async { guard !self.isStarted else { - completionHandler(.failure(.alreadyStarted)) + queue.performOnWrappedOrCurrentQueue { + completionHandler(.failure(.alreadyStarted)) + } return } @@ -164,15 +162,19 @@ class WireguardDevice { self.startNetworkMonitor() - completionHandler(.success(())) + queue.performOnWrappedOrCurrentQueue { + completionHandler(.success(())) + } case .failure(let error): - completionHandler(.failure(error)) + queue.performOnWrappedOrCurrentQueue { + completionHandler(.failure(error)) + } } } } - func stop(completionHandler: @escaping (Result<(), Error>) -> Void) { + func stop(queue: DispatchQueue?, completionHandler: @escaping (Result<(), Error>) -> Void) { workQueue.async { if self.isStarted { self.networkMonitor?.cancel() @@ -181,14 +183,18 @@ class WireguardDevice { self.stopWireguardBackend() self.isStarted = false - completionHandler(.success(())) + queue.performOnWrappedOrCurrentQueue { + completionHandler(.success(())) + } } else { - completionHandler(.failure(.notStarted)) + queue.performOnWrappedOrCurrentQueue { + completionHandler(.failure(.notStarted)) + } } } } - func setConfiguration(_ newConfiguration: WireguardConfiguration, completionHandler: @escaping (Result<(), Error>) -> Void) { + func setConfiguration(_ newConfiguration: WireguardConfiguration, queue: DispatchQueue?, completionHandler: @escaping (Result<(), Error>) -> Void) { workQueue.async { if self.isStarted { if let handle = self.wireguardHandle { @@ -200,9 +206,13 @@ class WireguardDevice { self.configuration = newConfiguration - completionHandler(.success(())) + queue.performOnWrappedOrCurrentQueue { + completionHandler(.success(())) + } } else { - completionHandler(.failure(.notStarted)) + queue.performOnWrappedOrCurrentQueue { + completionHandler(.failure(.notStarted)) + } } } } @@ -328,50 +338,48 @@ class WireguardDevice { networkMonitor.pathUpdateHandler = { [weak self] (path) in self?.didReceiveNetworkPathUpdate(path: path) } - networkMonitor.start(queue: networkMonitorQueue) + networkMonitor.start(queue: workQueue) self.networkMonitor = networkMonitor } private func didReceiveNetworkPathUpdate(path: Network.NWPath) { - workQueue.async { - guard self.isStarted else { return } + guard self.isStarted else { return } - self.logger.info("Network change detected. Status: \(path.status), interfaces \(path.availableInterfaces).") + self.logger.info("Network change detected. Status: \(path.status), interfaces \(path.availableInterfaces).") - let oldPathSatisfied = self.isPathSatisfied - let newPathSatisfied = path.status.isSatisfiable + let oldPathSatisfied = self.isPathSatisfied + let newPathSatisfied = path.status.isSatisfiable - self.isPathSatisfied = newPathSatisfied + self.isPathSatisfied = newPathSatisfied - switch (oldPathSatisfied, newPathSatisfied) { - case (true, false): - self.logger.info("Stop wireguard backend") - self.stopWireguardBackend() + switch (oldPathSatisfied, newPathSatisfied) { + case (true, false): + self.logger.info("Stop wireguard backend") + self.stopWireguardBackend() - case (false, true), (true, true): - guard let currentConfiguration = self.configuration else { return } + case (false, true), (true, true): + guard let currentConfiguration = self.configuration else { return } - self.logger.info("Re-resolve endpoints") + self.logger.info("Re-resolve endpoints") - let resolvedConfiguration = self.resolveConfiguration(currentConfiguration) + let resolvedConfiguration = self.resolveConfiguration(currentConfiguration) - if let handle = self.wireguardHandle { - let commands = resolvedConfiguration.endpointUapiConfiguration() - Self.setWireguardConfig(handle: handle, commands: commands) + if let handle = self.wireguardHandle { + let commands = resolvedConfiguration.endpointUapiConfiguration() + Self.setWireguardConfig(handle: handle, commands: commands) - wgBumpSockets(handle) - } else { - self.logger.info("Start wireguard backend") + wgBumpSockets(handle) + } else { + self.logger.info("Start wireguard backend") - if case .failure(let error) = self.startWireguardBackend(resolvedConfiguration: resolvedConfiguration) { - self.logger.error(chainedError: error, message: "Failed to turn on WireGuard") - } + if case .failure(let error) = self.startWireguardBackend(resolvedConfiguration: resolvedConfiguration) { + self.logger.error(chainedError: error, message: "Failed to turn on WireGuard") } - - case (false, false): - // No-op: device remains offline - break } + + case (false, false): + // No-op: device remains offline + break } } } |
