diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2021-09-30 12:13:46 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2021-10-01 13:33:07 +0200 |
| commit | 3ccf307a3356349530b1a2ba53cb552e390f6dfb (patch) | |
| tree | 2deae073efbdda86bed2a9eb501c9e9bcd574f56 | |
| parent | 981e94ba407d49e328b2f41cbd1a0e35bfca506d (diff) | |
| download | mullvadvpn-3ccf307a3356349530b1a2ba53cb552e390f6dfb.tar.xz mullvadvpn-3ccf307a3356349530b1a2ba53cb552e390f6dfb.zip | |
iOS: add background tasks
iOS 12:
Use UIApplicationDelegate.performFetchWithCompletionHandler to update
relays and rotate the private key.
iOS 13:
1. Background refresh task to update relays once an hour.
2. Background processing task for private key rotation.
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/AppDelegate.swift | 74 | ||||
| -rw-r--r-- | ios/MullvadVPN/ApplicationConfiguration.swift | 9 | ||||
| -rw-r--r-- | ios/MullvadVPN/Info.plist | 10 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayCache/RelayCacheTracker.swift | 83 | ||||
| -rw-r--r-- | ios/MullvadVPN/Result+UIBackgroundFetchResult.swift | 64 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager/TunnelManager.swift | 77 |
7 files changed, 321 insertions, 0 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 793b6c025b..40b1cbc403 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 5820676126E75A4D00655B05 /* Promise+Delay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675F26E75A4D00655B05 /* Promise+Delay.swift */; }; 5820676226E75D8500655B05 /* REST.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674D26E6510200655B05 /* REST.swift */; }; 5820676426E771DB00655B05 /* TunnelManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820676326E771DB00655B05 /* TunnelManagerError.swift */; }; + 5820676826E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820676726E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift */; }; 5823FA5426CE49F700283BF8 /* TunnelObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */; }; 5823FA5626CE4A2B00283BF8 /* AnyTunnelObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA5526CE4A2B00283BF8 /* AnyTunnelObserver.swift */; }; 58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */; }; @@ -360,6 +361,7 @@ 5820675D26E6839900655B05 /* PresentAlertOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentAlertOperation.swift; sourceTree = "<group>"; }; 5820675F26E75A4D00655B05 /* Promise+Delay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Delay.swift"; sourceTree = "<group>"; }; 5820676326E771DB00655B05 /* TunnelManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerError.swift; sourceTree = "<group>"; }; + 5820676726E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+UIBackgroundFetchResult.swift"; sourceTree = "<group>"; }; 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogHandler.swift; sourceTree = "<group>"; }; 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObserver.swift; sourceTree = "<group>"; }; 5823FA5526CE4A2B00283BF8 /* AnyTunnelObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyTunnelObserver.swift; sourceTree = "<group>"; }; @@ -848,6 +850,7 @@ 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */, 58781CD422AFBA39009B9D8E /* RelaySelector.swift */, 585DA87F26B0268500B8C587 /* REST */, + 5820676726E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift */, 587425C02299833500CA2045 /* RootContainerViewController.swift */, 5888AD82227B11080051EB06 /* SelectLocationCell.swift */, 5857F24224C8662600CF6F47 /* SelectLocationHeaderView.swift */, @@ -1341,6 +1344,7 @@ 5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */, 585DA89326B0323E00B8C587 /* TunnelIPCRequest.swift in Sources */, 5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */, + 5820676826E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift in Sources */, 5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */, 58E1337926D2BEDD00CC316B /* Promise+ReceiveOn.swift in Sources */, 58CE5E66224146200008646E /* LoginViewController.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index fa9e453bc2..cb1bb4518c 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -10,6 +10,7 @@ import UIKit import StoreKit import UserNotifications import Logging +import BackgroundTasks @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -52,6 +53,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { SimulatorTunnelProvider.shared.delegate = simulatorTunnelProvider #endif + if #available(iOS 13.0, *) { + // Register background tasks on iOS 13 + RelayCache.Tracker.shared.registerAppRefreshTask() + TunnelManager.shared.registerBackgroundTask() + } else { + // Set background refresh interval on iOS 12 + application.setMinimumBackgroundFetchInterval(ApplicationConfiguration.minimumBackgroundFetchInterval) + } + // Assign user notification center delegate UNUserNotificationCenter.current().delegate = self @@ -131,10 +141,74 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Stop periodic private key rotation TunnelManager.shared.stopPeriodicPrivateKeyRotation() } + + func applicationDidEnterBackground(_ application: UIApplication) { + if #available(iOS 13, *) { + scheduleBackgroundTasks() + } + } + + func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + logger?.info("Start background refresh") + + RelayCache.Tracker.shared.updateRelays() + .then { fetchRelaysResult -> Promise<UIBackgroundFetchResult> in + switch fetchRelaysResult { + case .success(let result): + self.logger?.debug("Finished updating relays: \(result)") + case .failure(let error): + self.logger?.error(chainedError: error, message: "Failed to update relays") + } + + return TunnelManager.shared.rotatePrivateKey() + .then { rotationResult -> UIBackgroundFetchResult in + switch rotationResult { + case .success(let result): + self.logger?.debug("Finished rotating the key: \(result)") + case .failure(let error): + self.logger?.error(chainedError: error, message: "Failed to rotate the key") + } + + return fetchRelaysResult.backgroundFetchResult.combine(with: rotationResult.backgroundFetchResult) + } + } + .receive(on: .main) + .observe { completion in + switch completion { + case .finished(let backgroundFetchResult): + self.logger?.info("Finish background refresh with \(backgroundFetchResult)") + completionHandler(backgroundFetchResult) + + case .cancelled: + self.logger?.info("Finish background refresh with cancelled promise") + completionHandler(.failed) + } + } } // MARK: - Private + @available(iOS 13.0, *) + private func scheduleBackgroundTasks() { + if case .finished(let result) = RelayCache.Tracker.shared.scheduleAppRefreshTask().await() { + switch result { + case .success: + self.logger?.debug("Scheduled app refresh task") + case .failure(let error): + self.logger?.error(chainedError: error, message: "Could not schedule app refresh task") + } + } + + if case .finished(let result) = TunnelManager.shared.scheduleBackgroundTask().await() { + switch result { + case .success: + self.logger?.debug("Scheduled private key rotation task") + case .failure(let error): + self.logger?.error(chainedError: error, message: "Could not schedule private key rotation task") + } + } + } + private func didFinishInitialization() { self.logger?.debug("Finished initialization. Show user interface.") diff --git a/ios/MullvadVPN/ApplicationConfiguration.swift b/ios/MullvadVPN/ApplicationConfiguration.swift index 5ca35e377c..023da47c78 100644 --- a/ios/MullvadVPN/ApplicationConfiguration.swift +++ b/ios/MullvadVPN/ApplicationConfiguration.swift @@ -37,4 +37,13 @@ extension ApplicationConfiguration { static var logFileURLs: [URL] { return [mainApplicationLogFileURL, packetTunnelLogFileURL].compactMap { $0 } } + + /// Background fetch minimum interval + static let minimumBackgroundFetchInterval: TimeInterval = 3600 + + /// App refresh background task identifier + static let appRefreshTaskIdentifier = "net.mullvad.MullvadVPN.AppRefresh" + + /// Key rotation background task identifier + static let privateKeyRotationTaskIdentifier = "net.mullvad.MullvadVPN.PrivateKeyRotation" } diff --git a/ios/MullvadVPN/Info.plist b/ios/MullvadVPN/Info.plist index 169f726c0f..6971236d8f 100644 --- a/ios/MullvadVPN/Info.plist +++ b/ios/MullvadVPN/Info.plist @@ -2,6 +2,11 @@ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> + <key>BGTaskSchedulerPermittedIdentifiers</key> + <array> + <string>net.mullvad.MullvadVPN.AppRefresh</string> + <string>net.mullvad.MullvadVPN.PrivateKeyRotation</string> + </array> <key>CFBundleDevelopmentRegion</key> <string>$(DEVELOPMENT_LANGUAGE)</string> <key>CFBundleDisplayName</key> @@ -24,6 +29,11 @@ <false/> <key>LSRequiresIPhoneOS</key> <true/> + <key>UIBackgroundModes</key> + <array> + <string>fetch</string> + <string>processing</string> + </array> <key>UILaunchStoryboardName</key> <string>LaunchScreen</string> <key>UIRequiredDeviceCapabilities</key> diff --git a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift index 401fea19f8..6e8228e400 100644 --- a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift +++ b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift @@ -6,6 +6,7 @@ // Copyright © 2019 Mullvad VPN AB. All rights reserved. // +import BackgroundTasks import Foundation import Logging @@ -245,3 +246,85 @@ extension RelayCache { } } + +// MARK: - Background tasks + +@available(iOS 13.0, *) +extension RelayCache.Tracker { + + /// Register app refresh task with scheduler. + func registerAppRefreshTask() { + let taskIdentifier = ApplicationConfiguration.appRefreshTaskIdentifier + + let isRegistered = BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil) { task in + self.handleAppRefreshTask(task as! BGAppRefreshTask) + } + + if isRegistered { + logger.debug("Registered app refresh task") + } else { + logger.error("Failed to register app refresh task") + } + } + + /// Schedules app refresh task relative to the last relays update. + func scheduleAppRefreshTask() -> Result<(), RelayCache.Error>.Promise { + return self.read().flatMap { cachedRelays in + let beginDate = cachedRelays.updatedAt.addingTimeInterval(Self.relayUpdateInterval) + + return self.submitAppRefreshTask(at: beginDate) + } + } + + /// Create and submit task request to scheduler. + private func submitAppRefreshTask(at beginDate: Date) -> Result<(), RelayCache.Error> { + let taskIdentifier = ApplicationConfiguration.appRefreshTaskIdentifier + + let request = BGAppRefreshTaskRequest(identifier: taskIdentifier) + request.earliestBeginDate = beginDate + + return Result { try BGTaskScheduler.shared.submit(request) } + .mapError { error in + return .backgroundTaskScheduler(error) + } + } + + /// Background task handler + private func handleAppRefreshTask(_ task: BGAppRefreshTask) { + var cancellationToken: PromiseCancellationToken? + + self.logger.debug("Start app refresh task") + + self.updateRelays() + .storeCancellationToken(in: &cancellationToken) + .observe { completion in + switch completion { + case .finished(.success(let fetchResult)): + self.logger.debug("Finished updating relays in app refresh task: \(fetchResult)") + + case .finished(.failure(let error)): + self.logger.error(chainedError: error, message: "Failed to update relays in app refresh task") + + case .cancelled: + self.logger.debug("App refresh task was cancelled") + } + + task.setTaskCompleted(success: !completion.isCancelled) + } + + task.expirationHandler = { + cancellationToken?.cancel() + } + + // Schedule next refresh + let scheduleDate = Date(timeIntervalSinceNow: Self.relayUpdateInterval) + + switch self.submitAppRefreshTask(at: scheduleDate) { + case .success: + self.logger.debug("Scheduled next app refresh task at \(scheduleDate.logFormatDate())") + + case .failure(let error): + self.logger.error(chainedError: error, message: "Failed to schedule next app refresh task") + } + } +} diff --git a/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift b/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift new file mode 100644 index 0000000000..7febb2aa9b --- /dev/null +++ b/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift @@ -0,0 +1,64 @@ +// +// Result+UIBackgroundFetchResult.swift +// Result+UIBackgroundFetchResult +// +// Created by pronebird on 07/09/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +extension Result where Success == TunnelManager.KeyRotationResult { + var backgroundFetchResult: UIBackgroundFetchResult { + switch self.asConcreteType() { + case .success(.finished): + return .newData + + case .success(.throttled): + return .noData + + case .failure: + return .failed + } + } +} + +extension Result where Success == RelayCache.FetchResult { + var backgroundFetchResult: UIBackgroundFetchResult { + switch self.asConcreteType() { + case .success(.newContent): + return .newData + + case .success(.throttled), .success(.sameContent): + return .noData + + case .failure: + return .failed + } + } +} + +extension UIBackgroundFetchResult: CustomStringConvertible { + public var description: String { + switch self { + case .newData: + return "new data" + case .noData: + return "no data" + case .failed: + return "failed" + @unknown default: + return "unknown (rawValue: \(self.rawValue)" + } + } + + func combine(with other: UIBackgroundFetchResult) -> UIBackgroundFetchResult { + if self == .failed || other == .failed { + return .failed + } else if self == .newData || other == .newData { + return .newData + } else { + return .noData + } + } +} diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 66cb8dafad..8dd38b759a 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -1040,6 +1040,83 @@ extension TunnelManager { } } +// MARK: - Background tasks + +@available(iOS 13.0, *) +extension TunnelManager { + + /// Register background task with scheduler. + func registerBackgroundTask() { + let taskIdentifier = ApplicationConfiguration.privateKeyRotationTaskIdentifier + + let isRegistered = BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil) { task in + self.handleBackgroundTask(task as! BGProcessingTask) + } + + if isRegistered { + logger.debug("Registered private key rotation task") + } else { + logger.error("Failed to register private key rotation task") + } + } + + /// Schedule background task relative to the private key creation date. + func scheduleBackgroundTask() -> Result<(), TunnelManager.Error>.Promise { + return Promise.deferred { self.tunnelInfo } + .some(or: .missingAccount) + .flatMap { tunnelInfo -> Result<(), TunnelManager.Error> in + let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate + let beginDate = Date(timeInterval: Self.privateKeyRotationInterval, since: creationDate) + + return self.submitBackgroundTask(at: beginDate) + } + .schedule(on: stateQueue) + } + + /// Create and submit task request to scheduler. + private func submitBackgroundTask(at beginDate: Date) -> Result<(), TunnelManager.Error> { + let taskIdentifier = ApplicationConfiguration.privateKeyRotationTaskIdentifier + + let request = BGProcessingTaskRequest(identifier: taskIdentifier) + request.earliestBeginDate = beginDate + request.requiresNetworkConnectivity = true + + return Result { try BGTaskScheduler.shared.submit(request) } + .mapError { error in + return .backgroundTaskScheduler(error) + } + } + + /// Background task handler. + private func handleBackgroundTask(_ task: BGProcessingTask) { + var cancellationToken: PromiseCancellationToken? + + self.logger.debug("Start private key rotation task") + + self.rotatePrivateKey() + .storeCancellationToken(in: &cancellationToken) + .observe { completion in + if let scheduleDate = self.handlePrivateKeyRotationCompletion(completion: completion) { + // Schedule next background task + switch self.submitBackgroundTask(at: scheduleDate) { + case .success: + self.logger.debug("Scheduled next private key rotation task at \(scheduleDate.logFormatDate())") + + case .failure(let error): + self.logger.error(chainedError: error, message: "Failed to schedule next private key rotation task") + } + } + + // Complete current task + task.setTaskCompleted(success: !completion.isCancelled) + } + + task.expirationHandler = { + cancellationToken?.cancel() + } + } +} + extension TunnelManager { fileprivate func handlePrivateKeyRotationCompletion(completion: PromiseCompletion<Result<KeyRotationResult, TunnelManager.Error>>) -> Date? { switch completion { |
