summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2021-09-30 12:13:46 +0200
committerAndrej Mihajlov <and@mullvad.net>2021-10-01 13:33:07 +0200
commit3ccf307a3356349530b1a2ba53cb552e390f6dfb (patch)
tree2deae073efbdda86bed2a9eb501c9e9bcd574f56
parent981e94ba407d49e328b2f41cbd1a0e35bfca506d (diff)
downloadmullvadvpn-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.pbxproj4
-rw-r--r--ios/MullvadVPN/AppDelegate.swift74
-rw-r--r--ios/MullvadVPN/ApplicationConfiguration.swift9
-rw-r--r--ios/MullvadVPN/Info.plist10
-rw-r--r--ios/MullvadVPN/RelayCache/RelayCacheTracker.swift83
-rw-r--r--ios/MullvadVPN/Result+UIBackgroundFetchResult.swift64
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift77
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 {