diff options
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 8 | ||||
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN/AppDelegate.swift | 22 | ||||
| -rw-r--r-- | ios/MullvadVPN/SimulatorTunnelProvider.swift | 14 | ||||
| -rw-r--r-- | ios/MullvadVPN/SimulatorTunnelProviderHost.swift | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift | 11 | ||||
| -rw-r--r-- | ios/MullvadVPN/TransportMonitor/TransportMonitor.swift | 97 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift | 18 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift | 69 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/Tunnel.swift | 55 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/TunnelConfiguration.swift | 26 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/TunnelInteractor.swift | 3 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/TunnelManager.swift | 54 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/TunnelStore.swift | 119 |
14 files changed, 315 insertions, 185 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 244617b56f..03f0efe43a 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -71,6 +71,8 @@ 06D9845428F99133003AABE9 /* libMullvadLogging.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 581943D628F800C900B0CB5E /* libMullvadLogging.a */; }; 06D9845A28F9918C003AABE9 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 06D9845928F9918C003AABE9 /* Logging */; }; 06D9846328F9A049003AABE9 /* libMullvadLogging.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 581943D628F800C900B0CB5E /* libMullvadLogging.a */; }; + 5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4AF2940A47300C23744 /* TunnelConfiguration.swift */; }; + 5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4B12940A48700C23744 /* TunnelStore.swift */; }; 5806767C27048E9B00C858CB /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CE5E7B224146470008646E /* PacketTunnelProvider.swift */; }; 5807483B27DB8A980020ECBF /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 5807483A27DB8A980020ECBF /* WireGuardKitTypes */; }; 5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; }; @@ -571,6 +573,8 @@ 06FAE67B28F83CA50033DD93 /* REST.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = REST.swift; sourceTree = "<group>"; }; 06FAE67C28F83CA50033DD93 /* URLSessionTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionTransport.swift; sourceTree = "<group>"; }; 06FAE67D28F83CA50033DD93 /* RESTTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTTransport.swift; sourceTree = "<group>"; }; + 5803B4AF2940A47300C23744 /* TunnelConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelConfiguration.swift; sourceTree = "<group>"; }; + 5803B4B12940A48700C23744 /* TunnelStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStore.swift; sourceTree = "<group>"; }; 58059DDB28465E8F002B1049 /* TransformOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformOperation.swift; sourceTree = "<group>"; }; 58059DDD28468158002B1049 /* OutputOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputOperation.swift; sourceTree = "<group>"; }; 58059DDF2846823E002B1049 /* ResultOperation+Output.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResultOperation+Output.swift"; sourceTree = "<group>"; }; @@ -1092,6 +1096,8 @@ 58F2E143276A13F300A79513 /* StartTunnelOperation.swift */, 58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */, 58E0A98727C8F46300FE6BDD /* Tunnel.swift */, + 5803B4B12940A48700C23744 /* TunnelStore.swift */, + 5803B4AF2940A47300C23744 /* TunnelConfiguration.swift */, 5875960926F371FC00BF6711 /* Tunnel+Messaging.swift */, 58968FAD28743E2000B799DC /* TunnelInteractor.swift */, 5835B7CB233B76CB0096D79F /* TunnelManager.swift */, @@ -2159,6 +2165,7 @@ 5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */, 58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */, 58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */, + 5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */, 587EB672271451E300123C75 /* PreferencesViewModel.swift in Sources */, 586A950C290125EE007BAF2B /* AlertPresenter.swift in Sources */, 584D26C6270C8741004EA533 /* SettingsDNSTextCell.swift in Sources */, @@ -2298,6 +2305,7 @@ 58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */, 587EB67027143B6500123C75 /* DataSourceSnapshot.swift in Sources */, 580F8B8328197881002E0998 /* TunnelSettingsV2.swift in Sources */, + 5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */, 586A950F29012BEE007BAF2B /* AddressCacheTracker.swift in Sources */, 587B753D2666468F00DEF7E9 /* NotificationController.swift in Sources */, ); diff --git a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme index c0ec326af9..dc70ad4ec7 100644 --- a/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme +++ b/ios/MullvadVPN.xcodeproj/xcshareddata/xcschemes/MullvadVPN.xcscheme @@ -88,7 +88,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - launchStyle = "1" + launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 27515a798e..eec04a0851 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -31,6 +31,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return operationQueue }() + private(set) var tunnelStore: TunnelStore! private(set) var tunnelManager: TunnelManager! private(set) var addressCache: REST.AddressCache! @@ -80,8 +81,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD store: addressCache ) + tunnelStore = TunnelStore(application: application) + tunnelManager = TunnelManager( application: application, + tunnelStore: tunnelStore, relayCacheTracker: relayCacheTracker, accountsProxy: accountsProxy, devicesProxy: devicesProxy @@ -94,7 +98,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD accountsProxy: accountsProxy ) - transportMonitor = TransportMonitor(tunnelManager: tunnelManager) + transportMonitor = TransportMonitor(tunnelManager: tunnelManager, tunnelStore: tunnelStore) #if targetEnvironment(simulator) // Configure mock tunnel provider on simulator @@ -109,6 +113,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD setupNotificationHandler() addApplicationNotifications(application: application) + let loadTunnelsOperation = AsyncBlockOperation(dispatchQueue: .main) { operation in + self.tunnelStore.loadPersistentTunnels { error in + if let error = error { + self.logger.error( + error: error, + message: "Failed to load persistent tunnels." + ) + } + operation.finish() + } + } + let migrateSettingsOperation = AsyncBlockOperation(dispatchQueue: .main) { operation in SettingsManager.migrateStore(with: self.proxyFactory) { error in guard let error = error else { @@ -128,6 +144,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } } } + migrateSettingsOperation.addDependency(loadTunnelsOperation) let loadTunnelConfigurationOperation = AsyncBlockOperation(dispatchQueue: .main) { operation in @@ -148,7 +165,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD loadTunnelConfigurationOperation.addDependency(migrateSettingsOperation) operationQueue.addOperations( - [migrateSettingsOperation, loadTunnelConfigurationOperation], + [loadTunnelsOperation, migrateSettingsOperation, loadTunnelConfigurationOperation], waitUntilFinished: false ) @@ -179,7 +196,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - Notifications @objc private func didBecomeActive(_ notification: Notification) { - tunnelManager.refreshTunnelStatus() tunnelManager.startPeriodicPrivateKeyRotation() relayCacheTracker.startPeriodicUpdates() addressCacheTracker.startPeriodicUpdates() diff --git a/ios/MullvadVPN/SimulatorTunnelProvider.swift b/ios/MullvadVPN/SimulatorTunnelProvider.swift index 4c7e0e091a..bf30237905 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider.swift @@ -85,7 +85,7 @@ class SimulatorTunnelProviderDelegate { } } -class SimulatorTunnelProvider { +final class SimulatorTunnelProvider { static let shared = SimulatorTunnelProvider() private let lock = NSLock() @@ -223,7 +223,9 @@ class SimulatorVPNConnection: NSObject, VPNConnectionProtocol { // MARK: - NETunnelProviderSession stubs -class SimulatorTunnelProviderSession: SimulatorVPNConnection, VPNTunnelProviderSessionProtocol { +final class SimulatorTunnelProviderSession: SimulatorVPNConnection, + VPNTunnelProviderSessionProtocol +{ func sendProviderMessage(_ messageData: Data, responseHandler: ((Data?) -> Void)?) throws { SimulatorTunnelProvider.shared.handleAppMessage( messageData, @@ -267,7 +269,7 @@ private struct SimulatorTunnelInfo { init() {} } -class SimulatorTunnelProviderManager: VPNTunnelProviderManagerProtocol, Equatable { +final class SimulatorTunnelProviderManager: NSObject, VPNTunnelProviderManagerProtocol { static let tunnelsLock = NSRecursiveLock() fileprivate static var tunnels = [SimulatorTunnelInfo]() @@ -370,12 +372,14 @@ class SimulatorTunnelProviderManager: VPNTunnelProviderManagerProtocol, Equatabl completionHandler(tunnelProviders, nil) } - required convenience init() { - self.init(tunnelInfo: SimulatorTunnelInfo()) + override required init() { + tunnelInfo = SimulatorTunnelInfo() + super.init() } private init(tunnelInfo: SimulatorTunnelInfo) { self.tunnelInfo = tunnelInfo + super.init() } func loadFromPreferences(completionHandler: (Error?) -> Void) { diff --git a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift index 6068036cfe..1ddf7b295f 100644 --- a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift @@ -17,7 +17,7 @@ import RelayCache import RelaySelector import TunnelProviderMessaging -class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { +final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { private var selectorResult: RelaySelectorResult? private let urlSession = REST.makeURLSession() private var proxiedRequests = [UUID: URLSessionDataTask]() diff --git a/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift b/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift index 8c583afb4c..65f8f9196f 100644 --- a/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift +++ b/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift @@ -11,15 +11,14 @@ import protocol MullvadREST.RESTTransport import MullvadTypes import TunnelProviderMessaging -final class PacketTunnelTransport: RESTTransport { +struct PacketTunnelTransport: RESTTransport { var name: String { return "packet-tunnel" } - let tunnelManager: TunnelManager - - init(tunnelManager: TunnelManager) { - self.tunnelManager = tunnelManager + let tunnel: Tunnel + init(tunnel: Tunnel) { + self.tunnel = tunnel } func sendRequest( @@ -28,7 +27,7 @@ final class PacketTunnelTransport: RESTTransport { ) throws -> Cancellable { let proxyRequest = try ProxyURLRequest(id: UUID(), urlRequest: request) - return try tunnelManager.sendRequest(proxyRequest) { result in + return tunnel.sendRequest(proxyRequest) { result in switch result { case .cancelled: completion(nil, nil, URLError(.cancelled)) diff --git a/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift b/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift index 972faef6b0..9f02429348 100644 --- a/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift +++ b/ios/MullvadVPN/TransportMonitor/TransportMonitor.swift @@ -9,93 +9,42 @@ import Foundation import MullvadREST -class TransportMonitor: TunnelObserver { +final class TransportMonitor { private let tunnelManager: TunnelManager - private let packetTunnelTransport: PacketTunnelTransport + private let tunnelStore: TunnelStore private let urlSessionTransport: REST.URLSessionTransport - private let nslock = NSLock() - private var _transport: RESTTransport? - - var transport: RESTTransport? { - nslock.lock() - defer { nslock.unlock() } - - return _transport - } - - init(tunnelManager: TunnelManager) { + init(tunnelManager: TunnelManager, tunnelStore: TunnelStore) { self.tunnelManager = tunnelManager + self.tunnelStore = tunnelStore - packetTunnelTransport = PacketTunnelTransport(tunnelManager: tunnelManager) urlSessionTransport = REST.URLSessionTransport(urlSession: REST.makeURLSession()) - - tunnelManager.addObserver(self) - - setTransports() - } - - // MARK: - TunnelObserver - - func tunnelManager(_ manager: TunnelManager, didUpdateTunnelStatus tunnelStatus: TunnelStatus) { - setTransports() - } - - func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) { - setTransports() } - func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) { - setTransports() - } - - func tunnelManager( - _ manager: TunnelManager, - didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2 - ) {} - - func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) {} - - // MARK: - Private - - private func setTransports() { - nslock.lock() - defer { nslock.unlock() } - - _transport = stateUpdated( - tunnelState: tunnelManager.tunnelStatus.state, - deviceState: tunnelManager.deviceState - ) - } - - private func stateUpdated( - tunnelState: TunnelState, - deviceState: DeviceState - ) -> RESTTransport { - switch (tunnelState, deviceState) { - case (.connected, .revoked): - return packetTunnelTransport - - case (.pendingReconnect, _): - return urlSessionTransport + var transport: RESTTransport? { + let tunnel = tunnelStore.getPersistentTunnels().first { tunnel in + return tunnel.status == .connecting || + tunnel.status == .reasserting || + tunnel.status == .connected + } - case (.waitingForConnectivity, _): + if let tunnel = tunnel, shouldByPassVPN(tunnel: tunnel) { + return PacketTunnelTransport(tunnel: tunnel) + } else { return urlSessionTransport + } + } - case (.connecting, _): - return packetTunnelTransport - - case (.reconnecting, _): - return packetTunnelTransport + private func shouldByPassVPN(tunnel: Tunnel) -> Bool { + switch tunnel.status { + case .connected: + return tunnelManager.isConfigurationLoaded && tunnelManager.deviceState == .revoked - case (.disconnecting, _): - return urlSessionTransport + case .connecting, .reasserting: + return true - case (.disconnected, _): - return urlSessionTransport - - case (.connected, _): - return urlSessionTransport + default: + return false } } } diff --git a/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift b/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift index 715ca595b2..4f5bfa60b8 100644 --- a/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift +++ b/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift @@ -22,25 +22,11 @@ class LoadTunnelConfigurationOperation: ResultOperation<Void, Error> { } override func main() { - TunnelProviderManagerType.loadAllFromPreferences { tunnels, error in - self.dispatchQueue.async { - if let error = error { - self.finish(completion: .failure(error)) - } else { - self.didLoadVPNConfigurations(tunnels: tunnels) - } - } - } - } - - private func didLoadVPNConfigurations(tunnels: [TunnelProviderManagerType]?) { let settingsResult = readSettings() let deviceStateResult = readDeviceState() - let tunnel = tunnels?.first.map { tunnelProvider in - return Tunnel(tunnelProvider: tunnelProvider) - } - + let persistentTunnels = interactor.getPersistentTunnels() + let tunnel = persistentTunnels.first let settings = settingsResult.flattenValue() let deviceState = deviceStateResult.flattenValue() diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift index e2f9fa1c37..12934a4790 100644 --- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift @@ -69,13 +69,11 @@ class StartTunnelOperation: ResultOperation<Void, Error> { selectorResult: RelaySelectorResult, completionHandler: @escaping (Error?) -> Void ) { - Self.makeTunnelProvider { result in + makeTunnelProvider { result in self.dispatchQueue.async { do { - let tunnelProvider = try result.get() - try self.startTunnel( - tunnelProvider: tunnelProvider, + tunnel: try result.get(), selectorResult: selectorResult ) @@ -87,10 +85,7 @@ class StartTunnelOperation: ResultOperation<Void, Error> { } } - private func startTunnel( - tunnelProvider: TunnelProviderManagerType, - selectorResult: RelaySelectorResult - ) throws { + private func startTunnel(tunnel: Tunnel, selectorResult: RelaySelectorResult) throws { var tunnelOptions = PacketTunnelOptions() do { @@ -102,10 +97,7 @@ class StartTunnelOperation: ResultOperation<Void, Error> { ) } - interactor.setTunnel( - Tunnel(tunnelProvider: tunnelProvider), - shouldRefreshTunnelState: false - ) + interactor.setTunnel(tunnel, shouldRefreshTunnelState: false) interactor.updateTunnelStatus { tunnelStatus in tunnelStatus = TunnelStatus() @@ -113,54 +105,35 @@ class StartTunnelOperation: ResultOperation<Void, Error> { tunnelStatus.state = .connecting(selectorResult.packetTunnelRelay) } - try tunnelProvider.connection.startVPNTunnel(options: tunnelOptions.rawOptions()) + try tunnel.start(options: tunnelOptions.rawOptions()) } - private class func makeTunnelProvider( - completionHandler: @escaping (Result< - TunnelProviderManagerType, - Error - >) -> Void - ) { - TunnelProviderManagerType.loadAllFromPreferences { tunnelProviders, error in - if let error = error { - completionHandler(.failure(error)) - return - } - - let tunnelProvider = tunnelProviders?.first ?? TunnelProviderManagerType() - - configureTunnelProvider(tunnelProvider) + private func makeTunnelProvider(completionHandler: @escaping (Result<Tunnel, Error>) -> Void) { + let persistentTunnels = interactor.getPersistentTunnels() + let tunnel = persistentTunnels.first ?? interactor.createNewTunnel() + let configuration = Self.makeTunnelConfiguration() - tunnelProvider.saveToPreferences { error in - if let error = error { - completionHandler(.failure(error)) - } else { - // Refresh connection status after saving the tunnel preferences. - // Basically it's only necessary to do for new instances of - // `NETunnelProviderManager`, but we do that for the existing ones too - // for simplicity as it has no side effects. - tunnelProvider.loadFromPreferences { error in - completionHandler(error.map { .failure($0) } ?? .success(tunnelProvider)) - } - } - } + tunnel.setConfiguration(configuration) + tunnel.saveToPreferences { error in + completionHandler(error.map { .failure($0) } ?? .success(tunnel)) } } - private class func configureTunnelProvider(_ tunnelProvider: TunnelProviderManagerType) { + private class func makeTunnelConfiguration() -> TunnelConfiguration { let protocolConfig = NETunnelProviderProtocol() protocolConfig.providerBundleIdentifier = ApplicationConfiguration .packetTunnelExtensionIdentifier protocolConfig.serverAddress = "" - tunnelProvider.isEnabled = true - tunnelProvider.localizedDescription = "WireGuard" - tunnelProvider.protocolConfiguration = protocolConfig - let alwaysOnRule = NEOnDemandRuleConnect() alwaysOnRule.interfaceTypeMatch = .any - tunnelProvider.onDemandRules = [alwaysOnRule] - tunnelProvider.isOnDemandEnabled = true + + return TunnelConfiguration( + isEnabled: true, + localizedDescription: "WireGuard", + protocolConfiguration: protocolConfig, + onDemandRules: [alwaysOnRule], + isOnDemandEnabled: true + ) } } diff --git a/ios/MullvadVPN/TunnelManager/Tunnel.swift b/ios/MullvadVPN/TunnelManager/Tunnel.swift index 260037b7a4..36ef198246 100644 --- a/ios/MullvadVPN/TunnelManager/Tunnel.swift +++ b/ios/MullvadVPN/TunnelManager/Tunnel.swift @@ -21,9 +21,29 @@ protocol TunnelStatusObserver { } /// Tunnel wrapper class. -class Tunnel { - /// Tunnel provider manager. - private let tunnelProvider: TunnelProviderManagerType +final class Tunnel: Equatable { + /// Unique identifier assigned to instance at the time of creation. + let identifier = UUID() + + #if DEBUG + /// System VPN configuration identifier. + /// This property performs a private call to obtain system configuration ID so it does not + /// guarantee to return anything, also it may not return anything for newly created tunnels. + var systemIdentifier: UUID? { + let configurationKey = "configuration" + let identifierKey = "identifier" + + guard tunnelProvider.responds(to: NSSelectorFromString(configurationKey)), + let config = tunnelProvider.value(forKey: configurationKey) as? NSObject, + config.responds(to: NSSelectorFromString(identifierKey)), + let identifier = config.value(forKey: identifierKey) as? UUID + else { + return nil + } + + return identifier + } + #endif /// Tunnel start date. /// @@ -51,10 +71,21 @@ class Tunnel { } } + func logFormat() -> String { + var s = identifier.uuidString + #if DEBUG + if let configurationIdentifier = systemIdentifier?.uuidString { + s += " (system profile ID: \(configurationIdentifier))" + } + #endif + return s + } + private let lock = NSLock() private var observerList = ObserverList<TunnelStatusObserver>() private var _startDate: Date? + private let tunnelProvider: TunnelProviderManagerType init(tunnelProvider: TunnelProviderManagerType) { self.tunnelProvider = tunnelProvider @@ -82,8 +113,22 @@ class Tunnel { try session.sendProviderMessage(messageData, responseHandler: responseHandler) } + func setConfiguration(_ configuration: TunnelConfiguration) { + configuration.apply(to: tunnelProvider) + } + func saveToPreferences(_ completion: @escaping (Error?) -> Void) { - tunnelProvider.saveToPreferences(completionHandler: completion) + tunnelProvider.saveToPreferences { error in + if let error = error { + completion(error) + } else { + // Refresh connection status after saving the tunnel preferences. + // Basically it's only necessary to do for new instances of + // `NETunnelProviderManager`, but we do that for the existing ones too + // for simplicity as it has no side effects. + self.tunnelProvider.loadFromPreferences(completionHandler: completion) + } + } } func removeFromPreferences(completion: @escaping (Error?) -> Void) { @@ -147,9 +192,7 @@ class Tunnel { break } } -} -extension Tunnel: Equatable { static func == (lhs: Tunnel, rhs: Tunnel) -> Bool { return lhs.tunnelProvider == rhs.tunnelProvider } diff --git a/ios/MullvadVPN/TunnelManager/TunnelConfiguration.swift b/ios/MullvadVPN/TunnelManager/TunnelConfiguration.swift new file mode 100644 index 0000000000..2af8b57263 --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/TunnelConfiguration.swift @@ -0,0 +1,26 @@ +// +// TunnelConfiguration.swift +// MullvadVPN +// +// Created by pronebird on 07/12/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import NetworkExtension + +struct TunnelConfiguration { + var isEnabled: Bool + var localizedDescription: String + var protocolConfiguration: NETunnelProviderProtocol + var onDemandRules: [NEOnDemandRule] + var isOnDemandEnabled: Bool + + func apply(to manager: TunnelProviderManagerType) { + manager.isEnabled = isEnabled + manager.localizedDescription = localizedDescription + manager.protocolConfiguration = protocolConfiguration + manager.onDemandRules = onDemandRules + manager.isOnDemandEnabled = isOnDemandEnabled + } +} diff --git a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift index eec47e1863..2f95622fbc 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift @@ -14,6 +14,9 @@ protocol TunnelInteractor { // MARK: - Tunnel manipulation var tunnel: Tunnel? { get } + + func getPersistentTunnels() -> [Tunnel] + func createNewTunnel() -> Tunnel func setTunnel(_ tunnel: Tunnel?, shouldRefreshTunnelState: Bool) // MARK: - Tunnel status diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index c163f992f2..0489d70229 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -50,6 +50,7 @@ final class TunnelManager: StorePaymentObserver { // MARK: - Internal variables private let application: UIApplication + fileprivate let tunnelStore: TunnelStore private let relayCacheTracker: RelayCacheTracker private let accountsProxy: REST.AccountsProxy private let devicesProxy: REST.DevicesProxy @@ -87,16 +88,25 @@ final class TunnelManager: StorePaymentObserver { init( application: UIApplication, + tunnelStore: TunnelStore, relayCacheTracker: RelayCacheTracker, accountsProxy: REST.AccountsProxy, devicesProxy: REST.DevicesProxy ) { self.application = application + self.tunnelStore = tunnelStore self.relayCacheTracker = relayCacheTracker self.accountsProxy = accountsProxy self.devicesProxy = devicesProxy self.operationQueue.name = "TunnelManager.operationQueue" self.operationQueue.underlyingQueue = internalQueue + + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidBecomeActive(_:)), + name: UIApplication.didBecomeActiveNotification, + object: application + ) } // MARK: - Periodic private key rotation @@ -242,13 +252,6 @@ final class TunnelManager: StorePaymentObserver { operationQueue.addOperation(loadTunnelOperation) } - func refreshTunnelStatus() { - #if DEBUG - logger.debug("Refresh tunnel status due to application becoming active.") - #endif - _refreshTunnelStatus() - } - func startTunnel(completionHandler: ((OperationCompletion<Void, Error>) -> Void)? = nil) { let operation = StartTunnelOperation( dispatchQueue: internalQueue, @@ -538,20 +541,6 @@ final class TunnelManager: StorePaymentObserver { ) } - /// Send URL request via packet tunnel process bypassing VPN. - /// This function is primarily used by `PacketTunnelTransport` to go outside of VPN when the - /// tunnel is broken. - func sendRequest( - _ proxyRequest: ProxyURLRequest, - completionHandler: @escaping (OperationCompletion<ProxyURLResponse, Error>) -> Void - ) throws -> Cancellable { - if let tunnel = tunnel { - return tunnel.sendRequest(proxyRequest, completionHandler: completionHandler) - } else { - throw UnsetTunnelError() - } - } - // MARK: - Tunnel observeration /// Add tunnel observer. @@ -663,7 +652,7 @@ final class TunnelManager: StorePaymentObserver { // Update the existing state if shouldRefreshTunnelState { logger.debug("Refresh tunnel status for new tunnel.") - _refreshTunnelStatus() + refreshTunnelStatus() } } @@ -785,6 +774,13 @@ final class TunnelManager: StorePaymentObserver { // MARK: - Private methods + @objc private func applicationDidBecomeActive(_ notification: Notification) { + #if DEBUG + logger.debug("Refresh tunnel status due to application becoming active.") + #endif + refreshTunnelStatus() + } + fileprivate func selectRelay() throws -> RelaySelectorResult { let cachedRelays = try relayCacheTracker.getCachedRelays() @@ -839,7 +835,7 @@ final class TunnelManager: StorePaymentObserver { switch tunnelStatus.state { case .connecting, .reconnecting: logger.debug("Refresh tunnel status due to reconnect.") - _refreshTunnelStatus() + refreshTunnelStatus() default: break @@ -869,7 +865,7 @@ final class TunnelManager: StorePaymentObserver { statusObserver = nil } - private func _refreshTunnelStatus() { + private func refreshTunnelStatus() { nslock.lock() defer { nslock.unlock() } @@ -989,7 +985,7 @@ final class TunnelManager: StorePaymentObserver { let timer = DispatchSource.makeTimerSource(queue: .main) timer.setEventHandler { [weak self] in - self?._refreshTunnelStatus() + self?.refreshTunnelStatus() } timer.schedule(wallDeadline: .now() + interval, repeating: interval) timer.activate() @@ -1020,6 +1016,14 @@ private struct TunnelInteractorProxy: TunnelInteractor { return tunnelManager.tunnel } + func getPersistentTunnels() -> [Tunnel] { + return tunnelManager.tunnelStore.getPersistentTunnels() + } + + func createNewTunnel() -> Tunnel { + return tunnelManager.tunnelStore.createNewTunnel() + } + func setTunnel(_ tunnel: Tunnel?, shouldRefreshTunnelState: Bool) { tunnelManager.setTunnel(tunnel, shouldRefreshTunnelState: shouldRefreshTunnelState) } diff --git a/ios/MullvadVPN/TunnelManager/TunnelStore.swift b/ios/MullvadVPN/TunnelManager/TunnelStore.swift new file mode 100644 index 0000000000..07266bcf17 --- /dev/null +++ b/ios/MullvadVPN/TunnelManager/TunnelStore.swift @@ -0,0 +1,119 @@ +// +// TunnelStore.swift +// MullvadVPN +// +// Created by pronebird on 07/12/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadLogging +import NetworkExtension +import UIKit + +/// Wrapper around system VPN tunnels. +final class TunnelStore: TunnelStatusObserver { + private let logger = Logger(label: "TunnelStore") + private let lock = NSLock() + + /// Persistent tunnels registered with the system. + private var persistentTunnels: [Tunnel] = [] + + /// Newly created tunnels, stored as collection of weak boxes. + private var newTunnels: [WeakBox<Tunnel>] = [] + + init(application: UIApplication) { + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidBecomeActive(_:)), + name: UIApplication.didBecomeActiveNotification, + object: application + ) + } + + func getPersistentTunnels() -> [Tunnel] { + lock.lock() + defer { lock.unlock() } + + return persistentTunnels + } + + func loadPersistentTunnels(completion: @escaping (Error?) -> Void) { + TunnelProviderManagerType.loadAllFromPreferences { managers, error in + self.lock.lock() + defer { + self.lock.unlock() + + completion(error) + } + + guard error == nil else { return } + + self.persistentTunnels.forEach { tunnel in + tunnel.removeObserver(self) + } + + self.persistentTunnels = managers?.map { manager in + let tunnel = Tunnel(tunnelProvider: manager) + tunnel.addObserver(self) + + self.logger.debug( + "Loaded persistent tunnel: \(tunnel.logFormat()) with status: \(tunnel.status)." + ) + + return tunnel + } ?? [] + } + } + + func createNewTunnel() -> Tunnel { + lock.lock() + defer { lock.unlock() } + + let tunnelProviderManager = TunnelProviderManagerType() + let tunnel = Tunnel(tunnelProvider: tunnelProviderManager) + tunnel.addObserver(self) + + newTunnels = newTunnels.filter { $0.value != nil } + newTunnels.append(WeakBox(tunnel)) + + logger.debug("Create new tunnel: \(tunnel.logFormat()).") + + return tunnel + } + + func tunnel(_ tunnel: Tunnel, didReceiveStatus status: NEVPNStatus) { + lock.lock() + defer { lock.unlock() } + + handleTunnelStatus(tunnel: tunnel, status: status) + } + + private func handleTunnelStatus(tunnel: Tunnel, status: NEVPNStatus) { + if status == .invalid, let index = persistentTunnels.firstIndex(of: tunnel) { + persistentTunnels.remove(at: index) + logger.debug("Persistent tunnel was removed: \(tunnel.logFormat()).") + } + + if status != .invalid, let index = newTunnels.firstIndex(where: { $0.value == tunnel }) { + newTunnels.remove(at: index) + persistentTunnels.append(tunnel) + logger.debug("New tunnel became persistent: \(tunnel.logFormat()).") + } + } + + @objc private func applicationDidBecomeActive(_ notification: Notification) { + refreshStatus() + } + + private func refreshStatus() { + lock.lock() + defer { lock.unlock() } + + let allTunnels = persistentTunnels + newTunnels.compactMap { $0.value } + + for tunnel in allTunnels { + handleTunnelStatus(tunnel: tunnel, status: tunnel.status) + } + } +} |
