summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPN/AccountViewController.swift4
-rw-r--r--ios/MullvadVPN/AddressCache/AddressCacheTracker.swift227
-rw-r--r--ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift86
-rw-r--r--ios/MullvadVPN/AppDelegate.swift437
-rw-r--r--ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift8
-rw-r--r--ios/MullvadVPN/ConnectViewController.swift12
-rw-r--r--ios/MullvadVPN/DisplayChainedError.swift7
-rw-r--r--ios/MullvadVPN/NotificationController.swift16
-rw-r--r--ios/MullvadVPN/NotificationManager.swift14
-rw-r--r--ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift4
-rw-r--r--ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift6
-rw-r--r--ios/MullvadVPN/Operations/InputInjectionBuilder.swift17
-rw-r--r--ios/MullvadVPN/Operations/OperationCompletion.swift8
-rw-r--r--ios/MullvadVPN/PreferencesViewController.swift4
-rw-r--r--ios/MullvadVPN/RelayCache/RelayCacheIO.swift16
-rw-r--r--ios/MullvadVPN/RelayCache/RelayCacheObserver.swift5
-rw-r--r--ios/MullvadVPN/RelayCache/RelayCacheTracker.swift511
-rw-r--r--ios/MullvadVPN/Result+UIBackgroundFetchResult.swift36
-rw-r--r--ios/MullvadVPN/SettingsDataSource.swift4
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProviderHost.swift10
-rw-r--r--ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift120
-rw-r--r--ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift19
-rw-r--r--ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift6
-rw-r--r--ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift21
-rw-r--r--ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift2
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift401
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManagerError.swift7
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManagerState.swift134
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelObserver.swift1
-rw-r--r--ios/MullvadVPN/WireguardKeysViewController.swift8
31 files changed, 964 insertions, 1191 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index d41d678a23..81453daf7e 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -21,7 +21,6 @@
58095C4F2760BA9100890776 /* AddressCacheStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58095C4E2760BA9100890776 /* AddressCacheStore.swift */; };
58095C512760BBB500890776 /* AddressCacheTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58095C502760BBB400890776 /* AddressCacheTracker.swift */; };
58095C532760EEC700890776 /* RESTNetworkOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58095C522760EEC700890776 /* RESTNetworkOperation.swift */; };
- 58095C552760F02500890776 /* UpdateAddressCacheOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58095C542760F02500890776 /* UpdateAddressCacheOperation.swift */; };
58095C572760F47900890776 /* api-ip-address.json in Resources */ = {isa = PBXBuildFile; fileRef = 58095C562760F47900890776 /* api-ip-address.json */; };
58095C592762155700890776 /* RESTRetryStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58095C582762155700890776 /* RESTRetryStrategy.swift */; };
580CBFB82848D503007878F0 /* OperationConditionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580CBFB72848D503007878F0 /* OperationConditionTests.swift */; };
@@ -368,7 +367,6 @@
58095C4E2760BA9100890776 /* AddressCacheStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCacheStore.swift; sourceTree = "<group>"; };
58095C502760BBB400890776 /* AddressCacheTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCacheTracker.swift; sourceTree = "<group>"; };
58095C522760EEC700890776 /* RESTNetworkOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTNetworkOperation.swift; sourceTree = "<group>"; };
- 58095C542760F02500890776 /* UpdateAddressCacheOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateAddressCacheOperation.swift; sourceTree = "<group>"; };
58095C562760F47900890776 /* api-ip-address.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "api-ip-address.json"; sourceTree = "<group>"; };
58095C582762155700890776 /* RESTRetryStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTRetryStrategy.swift; sourceTree = "<group>"; };
580CBFB72848D503007878F0 /* OperationConditionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationConditionTests.swift; sourceTree = "<group>"; };
@@ -640,7 +638,6 @@
58095C4E2760BA9100890776 /* AddressCacheStore.swift */,
58095C4A2760B4F200890776 /* AddressCacheStoreError.swift */,
58095C502760BBB400890776 /* AddressCacheTracker.swift */,
- 58095C542760F02500890776 /* UpdateAddressCacheOperation.swift */,
);
path = AddressCache;
sourceTree = "<group>";
@@ -1430,7 +1427,6 @@
58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */,
58FEEB46260A028D00A621A8 /* GeoJSON.swift in Sources */,
5815039724D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */,
- 58095C552760F02500890776 /* UpdateAddressCacheOperation.swift in Sources */,
5820675E26E6839900655B05 /* PresentAlertOperation.swift in Sources */,
5815039D24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */,
58CE5E64224146200008646E /* AppDelegate.swift in Sources */,
diff --git a/ios/MullvadVPN/AccountViewController.swift b/ios/MullvadVPN/AccountViewController.swift
index 4a4dd542ed..9753d8aac9 100644
--- a/ios/MullvadVPN/AccountViewController.swift
+++ b/ios/MullvadVPN/AccountViewController.swift
@@ -309,6 +309,10 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb
// MARK: - TunnelObserver
+ func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) {
+ // no-op
+ }
+
func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) {
// no-op
}
diff --git a/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift b/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift
index bbfaeb08c8..70397032ca 100644
--- a/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift
+++ b/ios/MullvadVPN/AddressCache/AddressCacheTracker.swift
@@ -7,11 +7,19 @@
//
import UIKit
-import BackgroundTasks
import Logging
extension AddressCache {
+
class Tracker {
+ /// Shared instance.
+ static let shared: AddressCache.Tracker = {
+ return AddressCache.Tracker(
+ apiProxy: REST.ProxyFactory.shared.createAPIProxy(),
+ store: AddressCache.Store.shared
+ )
+ }()
+
/// Update interval (in seconds).
private static let updateInterval: TimeInterval = 60 * 60 * 24
@@ -43,58 +51,71 @@ extension AddressCache {
return operationQueue
}()
- /// Queue used for synchronizing access to instance members.
- private let stateQueue = DispatchQueue(label: "AddressCache.Tracker.stateQueue")
+ /// Lock used for synchronizing member access.
+ private let nslock = NSLock()
/// Designated initializer
- init(apiProxy: REST.APIProxy, store: AddressCache.Store) {
+ private init(apiProxy: REST.APIProxy, store: AddressCache.Store) {
self.apiProxy = apiProxy
self.store = store
}
func startPeriodicUpdates() {
- stateQueue.async {
- guard !self.isPeriodicUpdatesEnabled else {
- return
- }
+ nslock.lock()
+ defer { nslock.unlock() }
- self.logger.debug("Start periodic address cache updates")
+ guard !isPeriodicUpdatesEnabled else {
+ return
+ }
- self.isPeriodicUpdatesEnabled = true
+ logger.debug("Start periodic address cache updates.")
- let scheduleDate = self.nextScheduleDate()
+ isPeriodicUpdatesEnabled = true
- self.logger.debug("Schedule address cache update on \(scheduleDate.logFormatDate())")
+ let scheduleDate = _nextScheduleDate()
- self.scheduleEndpointsUpdate(startTime: .now() + scheduleDate.timeIntervalSinceNow)
- }
+ logger.debug("Schedule address cache update at \(scheduleDate.logFormatDate()).")
+
+ scheduleEndpointsUpdate(startTime: .now() + scheduleDate.timeIntervalSinceNow)
}
func stopPeriodicUpdates() {
- stateQueue.async {
- guard self.isPeriodicUpdatesEnabled else { return }
+ nslock.lock()
+ defer { nslock.unlock() }
- self.logger.debug("Stop periodic address cache updates")
+ guard isPeriodicUpdatesEnabled else { return }
- self.isPeriodicUpdatesEnabled = false
+ logger.debug("Stop periodic address cache updates.")
- self.timer?.cancel()
- self.timer = nil
- }
+ isPeriodicUpdatesEnabled = false
+
+ timer?.cancel()
+ timer = nil
}
- func updateEndpoints(completionHandler: ((_ completion: OperationCompletion<CacheUpdateResult, Error>) -> Void)? = nil) -> Cancellable {
- let operation = UpdateAddressCacheOperation(
- dispatchQueue: stateQueue,
- apiProxy: apiProxy,
- store: store,
- updateInterval: Self.updateInterval,
- completionHandler: { [weak self] completion in
- self?.handleCacheUpdateCompletion(completion)
+ func updateEndpoints(
+ completionHandler: ((OperationCompletion<Bool, Error>) -> Void)? = nil
+ ) -> Cancellable
+ {
+ let operation = ResultBlockOperation<Bool, Error> { operation in
+ guard self.nextScheduleDate() <= Date() else {
+ operation.finish(completion: .success(false))
+ return
+ }
- completionHandler?(completion)
+ let task = self.apiProxy.getAddressList(retryStrategy: .default) { completion in
+ operation.finish(
+ completion: self.handleResponse(completion: completion)
+ )
}
- )
+
+ operation.addCancellationBlock {
+ task.cancel()
+ }
+ }
+
+ operation.completionQueue = .main
+ operation.completionHandler = completionHandler
operation.addObserver(
BackgroundObserver(name: "Update endpoints", cancelUponExpiration: true)
@@ -105,6 +126,45 @@ extension AddressCache {
return operation
}
+ func nextScheduleDate() -> Date {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ return _nextScheduleDate()
+ }
+
+ private func handleResponse(
+ completion: OperationCompletion<[AnyIPEndpoint], REST.Error>
+ ) -> OperationCompletion<Bool, Error>
+ {
+ let mappedCompletion = completion
+ .flatMapError { error -> OperationCompletion<[AnyIPEndpoint], REST.Error> in
+ if case URLError.cancelled = error {
+ return .cancelled
+ } else {
+ return .failure(error)
+ }
+ }
+ .tryMap { endpoints -> Bool in
+ try store.setEndpoints(endpoints)
+
+ return true
+ }
+
+ nslock.lock()
+ lastFailureAttemptDate = mappedCompletion.isSuccess ? nil : Date()
+ nslock.unlock()
+
+ if let error = mappedCompletion.error {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to update address cache."
+ )
+ }
+
+ return mappedCompletion
+ }
+
private func scheduleEndpointsUpdate(startTime: DispatchWallTime) {
let newTimer = DispatchSource.makeTimerSource()
newTimer.setEventHandler { [weak self] in
@@ -119,104 +179,33 @@ extension AddressCache {
}
private func handleTimer() {
- _ = updateEndpoints { result in
+ _ = updateEndpoints { _ in
+ self.nslock.lock()
+ defer { self.nslock.unlock() }
+
guard self.isPeriodicUpdatesEnabled else { return }
- let scheduleDate = self.nextScheduleDate()
+ let scheduleDate = self._nextScheduleDate()
- self.logger.debug("Schedule next address cache update on \(scheduleDate.logFormatDate())")
+ self.logger.debug("Schedule next address cache update at \(scheduleDate.logFormatDate()).")
self.scheduleEndpointsUpdate(startTime: .now() + scheduleDate.timeIntervalSinceNow)
}
}
- private func nextScheduleDate() -> Date {
- if let lastFailureAttemptDate = lastFailureAttemptDate {
- return Date(timeInterval: Self.retryInterval, since: lastFailureAttemptDate)
- } else {
- let updatedAt = store.getLastUpdateDate()
-
- return Date(timeInterval: Self.updateInterval, since: updatedAt)
- }
- }
-
- private func handleCacheUpdateCompletion(_ completion: OperationCompletion<AddressCache.CacheUpdateResult, Error>) {
- switch completion {
- case .success(let updateResult):
- switch updateResult {
- case .finished:
- logger.debug("Finished updating address cache.")
- case .throttled:
- logger.debug("Address cache update was throttled.")
- }
-
- lastFailureAttemptDate = nil
-
- case .failure(let error):
- logger.error(chainedError: AnyChainedError(error), message: "Failed to update address cache.")
- lastFailureAttemptDate = Date()
-
- case .cancelled:
- logger.debug("Address cache update was cancelled.")
- lastFailureAttemptDate = Date()
- }
- }
-
- }
-}
-
-// MARK: - Background tasks
-
-@available(iOS 13.0, *)
-extension AddressCache.Tracker {
-
- /// Register background task with scheduler.
- func registerBackgroundTask() {
- let taskIdentifier = ApplicationConfiguration.addressCacheUpdateTaskIdentifier
-
- let isRegistered = BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil) { task in
- self.handleBackgroundTask(task as! BGProcessingTask)
- }
-
- if isRegistered {
- logger.debug("Registered address cache update task")
- } else {
- logger.error("Failed to register address cache update task")
- }
- }
-
- /// Create and submit task request to scheduler.
- func scheduleBackgroundTask() throws {
- let beginDate = nextScheduleDate()
-
- logger.debug("Schedule address cache update task on \(beginDate.logFormatDate())")
-
- let taskIdentifier = ApplicationConfiguration.addressCacheUpdateTaskIdentifier
-
- let request = BGProcessingTaskRequest(identifier: taskIdentifier)
- request.earliestBeginDate = beginDate
- request.requiresNetworkConnectivity = true
-
- return try BGTaskScheduler.shared.submit(request)
- }
-
- /// Background task handler.
- private func handleBackgroundTask(_ task: BGProcessingTask) {
- logger.debug("Start address cache update task")
-
- let cancellable = updateEndpoints { completion in
- do {
- // Schedule next background task
- try self.scheduleBackgroundTask()
- } catch {
- self.logger.error(chainedError: AnyChainedError(error), message: "Failed to schedule next address cache update task")
- }
+ private func _nextScheduleDate() -> Date {
+ let nextDate = lastFailureAttemptDate.map { date in
+ return Date(
+ timeInterval: Self.retryInterval,
+ since: date
+ )
+ } ?? Date(
+ timeInterval: Self.updateInterval,
+ since: store.getLastUpdateDate()
+ )
- task.setTaskCompleted(success: completion.isSuccess)
+ return max(nextDate, Date())
}
- task.expirationHandler = {
- cancellable.cancel()
- }
}
}
diff --git a/ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift b/ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift
deleted file mode 100644
index 4eaeda18e1..0000000000
--- a/ios/MullvadVPN/AddressCache/UpdateAddressCacheOperation.swift
+++ /dev/null
@@ -1,86 +0,0 @@
-//
-// UpdateAddressCacheOperation.swift
-// MullvadVPN
-//
-// Created by pronebird on 08/12/2021.
-// Copyright © 2021 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-
-extension AddressCache {
-
- enum CacheUpdateResult {
- /// Address cache update was throttled as it was requested too early.
- case throttled(_ lastUpdateDate: Date)
-
- /// Address cache is successfully updated.
- case finished
- }
-
- class UpdateAddressCacheOperation: ResultOperation<CacheUpdateResult, Error> {
- private let apiProxy: REST.APIProxy
- private let store: AddressCache.Store
- private let updateInterval: TimeInterval
-
- private var requestTask: Cancellable?
-
- init(
- dispatchQueue: DispatchQueue,
- apiProxy: REST.APIProxy,
- store: AddressCache.Store,
- updateInterval: TimeInterval,
- completionHandler: CompletionHandler?
- )
- {
- self.apiProxy = apiProxy
- self.store = store
- self.updateInterval = updateInterval
-
- super.init(
- dispatchQueue: dispatchQueue,
- completionQueue: dispatchQueue,
- completionHandler: completionHandler
- )
- }
-
- override func main() {
- let lastUpdate = store.getLastUpdateDate()
- let nextUpdate = Date(timeInterval: updateInterval, since: lastUpdate)
-
- guard nextUpdate <= Date() else {
- finish(completion: .success(.throttled(lastUpdate)))
- return
- }
-
- requestTask = apiProxy.getAddressList(retryStrategy: .default) { [weak self] completion in
- self?.dispatchQueue.async {
- self?.handleResponse(completion)
- }
- }
- }
-
- override func operationDidCancel() {
- requestTask?.cancel()
- requestTask = nil
- }
-
- private func handleResponse(_ completion: OperationCompletion<[AnyIPEndpoint], REST.Error>) {
- let mappedCompletion = completion
- .flatMapError { error -> OperationCompletion<[AnyIPEndpoint], Error> in
- if case URLError.cancelled = error {
- return .cancelled
- } else {
- return .failure(error)
- }
- }
- .tryMap { endpoints -> CacheUpdateResult in
- try store.setEndpoints(endpoints)
-
- return .finished
- }
-
- finish(completion: mappedCompletion)
- }
- }
-}
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index 9c853b018b..1084014e3d 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -17,7 +17,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
- private var logger: Logger?
+ private var logger: Logger!
#if targetEnvironment(simulator)
private let simulatorTunnelProvider = SimulatorTunnelProviderHost()
@@ -36,23 +36,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
private var connectController: ConnectViewController?
private weak var settingsNavController: SettingsNavigationController?
- private lazy var addressCacheTracker: AddressCache.Tracker = {
- return AddressCache.Tracker(
- apiProxy: REST.ProxyFactory.shared.createAPIProxy(),
- store: AddressCache.Store.shared
- )
- }()
-
- private var cachedRelays: RelayCache.CachedRelays? {
- didSet {
- if let cachedRelays = cachedRelays {
- self.selectLocationViewController?.setCachedRelays(cachedRelays)
- }
- }
- }
private var relayConstraints: RelayConstraints?
- private let notificationManager = NotificationManager()
+ private let operationQueue: AsyncOperationQueue = {
+ let operationQueue = AsyncOperationQueue()
+ operationQueue.maxConcurrentOperationCount = 1
+ return operationQueue
+ }()
// MARK: - Application lifecycle
@@ -60,7 +50,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Setup logging
initLoggingSystem(bundleIdentifier: Bundle.main.bundleIdentifier!)
- self.logger = Logger(label: "AppDelegate")
+ logger = Logger(label: "AppDelegate")
#if targetEnvironment(simulator)
// Configure mock tunnel provider on simulator
@@ -69,54 +59,73 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
if #available(iOS 13.0, *) {
// Register background tasks on iOS 13
- RelayCache.Tracker.shared.registerAppRefreshTask()
- TunnelManager.shared.registerBackgroundTask()
- addressCacheTracker.registerBackgroundTask()
+ registerBackgroundTasks()
} else {
// Set background refresh interval on iOS 12
- application.setMinimumBackgroundFetchInterval(ApplicationConfiguration.minimumBackgroundFetchInterval)
+ application.setMinimumBackgroundFetchInterval(
+ ApplicationConfiguration.minimumBackgroundFetchInterval
+ )
}
- // Assign user notification center delegate
- UNUserNotificationCenter.current().delegate = self
-
- // Create an app window
- self.window = UIWindow(frame: UIScreen.main.bounds)
-
- // Set an empty view controller while loading tunnels
- self.window?.rootViewController = LaunchViewController()
+ setupPaymentHandler()
+ setupNotificationHandler()
// Add relay cache observer
RelayCache.Tracker.shared.addObserver(self)
- // Load initial relays
- RelayCache.Tracker.shared.read { result in
- DispatchQueue.main.async {
- switch result {
- case .success(let cachedRelays):
- self.cachedRelays = cachedRelays
+ // Start initialization
+ let setupTunnelManagerOperation = AsyncBlockOperation(dispatchQueue: .main) { blockOperation in
+ TunnelManager.shared.loadConfiguration { error in
+ dispatchPrecondition(condition: .onQueue(.main))
- case .failure(let error):
- self.logger?.error(chainedError: error, message: "Failed to load initial relays")
+ if let error = error {
+ self.logger.error(chainedError: error, message: "Failed to load tunnels")
+
+ // TODO: avoid throwing fatal error and show the problem report UI instead.
+ fatalError(
+ error.displayChain(message: "Failed to load VPN tunnel configuration")
+ )
}
+
+ blockOperation.finish()
}
}
- // Load tunnels
- TunnelManager.shared.loadConfiguration { error in
- dispatchPrecondition(condition: .onQueue(.main))
+ let setupUIOperation = AsyncBlockOperation(dispatchQueue: .main) {
+ self.logger.debug("Finished initialization. Show user interface.")
- if let error = error {
- self.logger?.error(chainedError: error, message: "Failed to load tunnels")
+ self.relayConstraints = TunnelManager.shared.tunnelSettings?.relayConstraints
- // TODO: avoid throwing fatal error and show the problem report UI instead.
- fatalError(error.displayChain(message: "Failed to load VPN tunnel configuration"))
- } else {
- self.relayConstraints = TunnelManager.shared.tunnelSettings?.relayConstraints
- self.didFinishInitialization()
+ self.rootContainer = RootContainerViewController()
+ self.rootContainer?.delegate = self
+ self.window?.rootViewController = self.rootContainer
+
+ switch UIDevice.current.userInterfaceIdiom {
+ case .pad:
+ self.setupPadUI()
+
+ case .phone:
+ self.setupPhoneUI()
+
+ default:
+ fatalError()
}
+
+ NotificationManager.shared.updateNotifications()
+ AppStorePaymentManager.shared.startPaymentQueueMonitoring()
}
+ operationQueue.addOperations([
+ setupTunnelManagerOperation,
+ setupUIOperation
+ ], waitUntilFinished: false)
+
+ // Create an app window
+ self.window = UIWindow(frame: UIScreen.main.bounds)
+
+ // Set an empty view controller while loading tunnels
+ self.window?.rootViewController = LaunchViewController()
+
// Show the window
self.window?.makeKeyAndVisible()
@@ -124,6 +133,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
func applicationDidBecomeActive(_ application: UIApplication) {
+ // Refresh tunnel status.
+ TunnelManager.shared.refreshTunnelStatus()
+
// Start periodic relays updates
RelayCache.Tracker.shared.startPeriodicUpdates()
@@ -131,7 +143,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
TunnelManager.shared.startPeriodicPrivateKeyRotation()
// Start periodic API address list updates
- addressCacheTracker.startPeriodicUpdates()
+ AddressCache.Tracker.shared.startPeriodicUpdates()
// Reveal application content
occlusionWindow.isHidden = true
@@ -146,7 +158,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
TunnelManager.shared.stopPeriodicPrivateKeyRotation()
// Stop periodic API address list updates
- addressCacheTracker.stopPeriodicUpdates()
+ AddressCache.Tracker.shared.stopPeriodicUpdates()
// Hide application content
occlusionWindow.makeKeyAndVisible()
@@ -158,19 +170,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
- func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
- logger?.info("Start background refresh")
-
- var addressCacheFetchResult: UIBackgroundFetchResult?
- var relaysFetchResult: UIBackgroundFetchResult?
- var rotatePrivateKeyFetchResult: UIBackgroundFetchResult?
-
- let operationQueue = AsyncOperationQueue()
+ func application(
+ _ application: UIApplication,
+ performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
+ )
+ {
+ logger.debug("Start background refresh.")
- let updateAddressCacheOperation = AsyncBlockOperation(dispatchQueue: .main) { operation in
- let handle = self.addressCacheTracker.updateEndpoints { completion in
- addressCacheFetchResult = completion.backgroundFetchResult
- operation.finish()
+ let updateAddressCacheOperation = ResultBlockOperation<Bool, Error> { operation in
+ let handle = AddressCache.Tracker.shared.updateEndpoints { completion in
+ operation.finish(completion: completion)
}
operation.addCancellationBlock {
@@ -178,19 +187,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
- let updateRelaysOperation = AsyncBlockOperation(dispatchQueue: .main) { operation in
+ let updateRelaysOperation = ResultBlockOperation<RelayCache.FetchResult, RelayCache.Error>
+ { operation in
let handle = RelayCache.Tracker.shared.updateRelays { completion in
- switch completion {
- 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.")
- case .cancelled:
- break
- }
-
- relaysFetchResult = completion.backgroundFetchResult
- operation.finish()
+ operation.finish(completion: completion)
}
operation.addCancellationBlock {
@@ -198,124 +198,234 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
- let rotatePrivateKeyOperation = AsyncBlockOperation(dispatchQueue: .main) { operation in
- let handle = TunnelManager.shared.rotatePrivateKey { completion in
- switch completion {
- case .success(let rotationResult):
- self.logger?.debug("Finished rotating the key: \(rotationResult).")
- case .failure(let error):
- self.logger?.error(chainedError: error, message: "Failed to rotate the key.")
- case .cancelled:
- break
- }
-
- rotatePrivateKeyFetchResult = completion.backgroundFetchResult
- operation.finish()
+ let rotatePrivateKeyOperation = ResultBlockOperation<Bool, TunnelManager.Error>
+ { operation in
+ let handle = TunnelManager.shared.rotatePrivateKey(forceRotate: false) { completion in
+ operation.finish(completion: completion)
}
operation.addCancellationBlock {
handle.cancel()
}
}
+ rotatePrivateKeyOperation.addDependencies([
+ updateRelaysOperation,
+ updateAddressCacheOperation
+ ])
- rotatePrivateKeyOperation.addDependencies([updateRelaysOperation, updateAddressCacheOperation])
+ let operations = [
+ updateAddressCacheOperation,
+ updateRelaysOperation,
+ rotatePrivateKeyOperation
+ ]
+
+ let completeOperation = TransformOperation<UIBackgroundFetchResult, Void, Never>(
+ dispatchQueue: .main
+ )
+
+ completeOperation.setExecutionBlock { backgroundFetchResult in
+ self.logger.debug("Finish background refresh. Status: \(backgroundFetchResult).")
- let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "AppDelegate.performFetch") {
- operationQueue.cancelAllOperations()
+ completionHandler(backgroundFetchResult)
}
- let fetchOperations = [updateAddressCacheOperation, updateRelaysOperation, rotatePrivateKeyOperation]
+ completeOperation.injectMany(context: [UIBackgroundFetchResult]())
+ .injectCompletion(from: updateAddressCacheOperation, via: { results, completion in
+ results.append(completion.backgroundFetchResult { $0 })
+ })
+ .injectCompletion(from: updateRelaysOperation, via: { results, completion in
+ results.append(completion.backgroundFetchResult { $0 == .newContent })
+ })
+ .injectCompletion(from: rotatePrivateKeyOperation, via: { results, completion in
+ results.append(completion.backgroundFetchResult { $0 })
+ })
+ .reduce { operationResults in
+ let initialResult = operationResults.first ?? .failed
+ let backgroundFetchResult = operationResults
+ .reduce(initialResult) { partialResult, other in
+ return partialResult.combine(with: other)
+ }
- let completionOperation = BlockOperation {
- let operationResults = [addressCacheFetchResult, relaysFetchResult, rotatePrivateKeyFetchResult].compactMap { $0 }
- let initialResult = operationResults.first ?? .failed
- let backgroundFetchResult = operationResults.reduce(initialResult) { partialResult, other in
- return partialResult.combine(with: other)
+ return backgroundFetchResult
}
- self.logger?.info("Finish background refresh with \(backgroundFetchResult)")
+ let groupOperation = GroupOperation(operations: operations)
+ groupOperation.addObserver(
+ BackgroundObserver(name: "Background refresh", cancelUponExpiration: true)
+ )
- completionHandler(backgroundFetchResult)
+ let operationQueue = AsyncOperationQueue()
+ operationQueue.addOperation(groupOperation)
+ operationQueue.addOperation(completeOperation)
+ }
+
+ // MARK: - Background tasks
+ @available(iOS 13, *)
+ private func registerBackgroundTasks() {
+ registerAppRefreshTask()
+ registerAddressCacheUpdateTask()
+ registerKeyRotationTask()
+ }
+
+ @available(iOS 13.0, *)
+ private func registerAppRefreshTask() {
+ let isRegistered = BGTaskScheduler.shared.register(
+ forTaskWithIdentifier: ApplicationConfiguration.appRefreshTaskIdentifier,
+ using: nil
+ ) { task in
+ let handle = RelayCache.Tracker.shared.updateRelays { completion in
+ task.setTaskCompleted(success: completion.isSuccess)
+ }
+
+ task.expirationHandler = {
+ handle.cancel()
+ }
+
+ self.scheduleAppRefreshTask()
+ }
- UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
+ if isRegistered {
+ logger.debug("Registered app refresh task.")
+ } else {
+ logger.error("Failed to register app refresh task.")
}
+ }
+
+ @available(iOS 13.0, *)
+ private func registerKeyRotationTask() {
+ let isRegistered = BGTaskScheduler.shared.register(
+ forTaskWithIdentifier: ApplicationConfiguration.privateKeyRotationTaskIdentifier,
+ using: nil
+ ) { task in
+ let handle = TunnelManager.shared.rotatePrivateKey(forceRotate: false) { completion in
+ self.scheduleKeyRotationTask()
+
+ task.setTaskCompleted(success: completion.isSuccess)
+ }
- completionOperation.addDependencies(fetchOperations)
+ task.expirationHandler = {
+ handle.cancel()
+ }
+ }
- operationQueue.addOperations(fetchOperations, waitUntilFinished: false)
- OperationQueue.main.addOperation(completionOperation)
+ if isRegistered {
+ logger.debug("Registered private key rotation task.")
+ } else {
+ logger.error("Failed to register private key rotation task.")
+ }
}
- // MARK: - Private
+ @available(iOS 13.0, *)
+ private func registerAddressCacheUpdateTask() {
+ let isRegistered = BGTaskScheduler.shared.register(
+ forTaskWithIdentifier: ApplicationConfiguration.addressCacheUpdateTaskIdentifier,
+ using: nil
+ ) { task in
+ let handle = AddressCache.Tracker.shared.updateEndpoints { completion in
+ self.scheduleAddressCacheUpdateTask()
+
+ task.setTaskCompleted(success: completion.isSuccess)
+ }
+
+ task.expirationHandler = {
+ handle.cancel()
+ }
+ }
+
+ if isRegistered {
+ logger.debug("Registered address cache update task.")
+ } else {
+ logger.error("Failed to register address cache update task.")
+ }
+ }
@available(iOS 13.0, *)
- private func scheduleBackgroundTasks() {
+ func scheduleBackgroundTasks() {
+ scheduleAppRefreshTask()
+ scheduleKeyRotationTask()
+ scheduleAddressCacheUpdateTask()
+ }
+
+ @available(iOS 13.0, *)
+ private func scheduleAppRefreshTask() {
do {
- try RelayCache.Tracker.shared.scheduleAppRefreshTask()
+ let date = RelayCache.Tracker.shared.getNextUpdateDate()
+
+ let request = BGAppRefreshTaskRequest(
+ identifier: ApplicationConfiguration.appRefreshTaskIdentifier
+ )
+ request.earliestBeginDate = date
+
+ logger.debug("Schedule app refresh task at \(date.logFormatDate()).")
- logger?.debug("Scheduled app refresh task.")
+ try BGTaskScheduler.shared.submit(request)
} catch {
- logger?.error(
+ logger.error(
chainedError: AnyChainedError(error),
message: "Could not schedule app refresh task."
)
}
+ }
+ @available(iOS 13.0, *)
+ private func scheduleKeyRotationTask() {
do {
- try TunnelManager.shared.scheduleBackgroundTask()
+ guard let date = TunnelManager.shared.getNextKeyRotationDate() else {
+ return
+ }
+
+ let request = BGProcessingTaskRequest(
+ identifier: ApplicationConfiguration.privateKeyRotationTaskIdentifier
+ )
+ request.requiresNetworkConnectivity = true
+ request.earliestBeginDate = date
+
+ logger.debug("Schedule key rotation task at \(date.logFormatDate()).")
- logger?.debug("Scheduled private key rotation task.")
+ try BGTaskScheduler.shared.submit(request)
} catch {
- logger?.error(
+ logger.error(
chainedError: AnyChainedError(error),
message: "Could not schedule private key rotation task."
)
}
+ }
+ @available(iOS 13.0, *)
+ private func scheduleAddressCacheUpdateTask() {
do {
- try addressCacheTracker.scheduleBackgroundTask()
+ let date = AddressCache.Tracker.shared.nextScheduleDate()
+
+ let request = BGProcessingTaskRequest(
+ identifier: ApplicationConfiguration.addressCacheUpdateTaskIdentifier
+ )
+ request.requiresNetworkConnectivity = true
+ request.earliestBeginDate = date
- self.logger?.debug("Scheduled address cache update task.")
+ logger.debug("Schedule address cache update task at \(date.logFormatDate()).")
+
+ try BGTaskScheduler.shared.submit(request)
} catch {
- self.logger?.error(
+ logger.error(
chainedError: AnyChainedError(error),
message: "Could not schedule address cache update task."
)
}
}
- private func didFinishInitialization() {
- self.logger?.debug("Finished initialization. Show user interface.")
-
- self.rootContainer = RootContainerViewController()
- self.rootContainer?.delegate = self
- self.window?.rootViewController = self.rootContainer
-
- switch UIDevice.current.userInterfaceIdiom {
- case .pad:
- self.setupPadUI()
-
- case .phone:
- self.setupPhoneUI()
+ // MARK: - Private
- default:
- fatalError()
- }
+ private func setupPaymentHandler() {
+ AppStorePaymentManager.shared.delegate = self
+ AppStorePaymentManager.shared.addPaymentObserver(TunnelManager.shared)
+ }
- notificationManager.notificationProviders = [
+ private func setupNotificationHandler() {
+ NotificationManager.shared.notificationProviders = [
AccountExpiryNotificationProvider(),
TunnelErrorNotificationProvider()
]
- notificationManager.updateNotifications()
-
- startPaymentQueueHandling()
- }
-
- private func startPaymentQueueHandling() {
- let paymentManager = AppStorePaymentManager.shared
- paymentManager.delegate = self
- paymentManager.addPaymentObserver(TunnelManager.shared)
- paymentManager.startPaymentQueueMonitoring()
+ UNUserNotificationCenter.current().delegate = self
}
private func setupPadUI() {
@@ -393,7 +503,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
private func makeConnectViewController() -> ConnectViewController {
let connectController = ConnectViewController()
connectController.delegate = self
- notificationManager.delegate = connectController.notificationController
+ NotificationManager.shared.delegate = self
return connectController
}
@@ -402,12 +512,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
let selectLocationController = SelectLocationViewController()
selectLocationController.delegate = self
- if let cachedRelays = cachedRelays {
+ if let cachedRelays = RelayCache.Tracker.shared.getCachedRelays() {
selectLocationController.setCachedRelays(cachedRelays)
}
if let relayLocation = relayConstraints?.location.value {
- selectLocationController.setSelectedRelayLocation(relayLocation, animated: false, scrollPosition: .middle)
+ selectLocationController.setSelectedRelayLocation(
+ relayLocation,
+ animated: false,
+ scrollPosition: .middle
+ )
}
return selectLocationController
@@ -544,6 +658,13 @@ extension AppDelegate: RootContainerViewControllerDelegate {
}
}
+// MARK: - NotificationManagerDelegate
+extension AppDelegate: NotificationManagerDelegate {
+ func notificationManagerDidUpdateInAppNotifications(_ manager: NotificationManager, notifications: [InAppNotificationDescriptor]) {
+ connectController?.notificationController.setNotifications(notifications, animated: true)
+ }
+}
+
// MARK: - LoginViewControllerDelegate
extension AppDelegate: LoginViewControllerDelegate {
@@ -554,11 +675,11 @@ extension AppDelegate: LoginViewControllerDelegate {
TunnelManager.shared.setAccount(action: .existing(accountNumber)) { operationCompletion in
switch operationCompletion {
case .success:
- self.logger?.debug("Logged in with existing account.")
+ self.logger.debug("Logged in with existing account.")
// RootContainer's settings button will be re-enabled in `loginViewControllerDidLogin`
case .failure(let error):
- self.logger?.error(chainedError: error, message: "Failed to log in with existing account.")
+ self.logger.error(chainedError: error, message: "Failed to log in with existing account.")
fallthrough
case .cancelled:
@@ -575,11 +696,11 @@ extension AppDelegate: LoginViewControllerDelegate {
TunnelManager.shared.setAccount(action: .new) { operationCompletion in
switch operationCompletion {
case .success:
- self.logger?.debug("Logged in with new account number.")
+ self.logger.debug("Logged in with new account number.")
// RootContainer's settings button will be re-enabled in `loginViewControllerDidLogin`
case .failure(let error):
- self.logger?.error(chainedError: error, message: "Failed to log in with new account.")
+ self.logger.error(chainedError: error, message: "Failed to log in with new account.")
fallthrough
case .cancelled:
@@ -703,9 +824,9 @@ extension AppDelegate: SelectLocationViewControllerDelegate {
self.relayConstraints = relayConstraints
if let error = error {
- self.logger?.error(chainedError: error, message: "Failed to update relay constraints")
+ self.logger.error(chainedError: error, message: "Failed to update relay constraints")
} else {
- self.logger?.debug("Updated relay constraints: \(relayConstraints)")
+ self.logger.debug("Updated relay constraints: \(relayConstraints)")
TunnelManager.shared.startTunnel()
}
}
@@ -761,9 +882,9 @@ extension AppDelegate: UIAdaptivePresentationControllerDelegate {
})
} else {
if let containerView = presentationController.containerView {
- self.rootContainer?.addSettingsButtonToPresentationContainer(containerView)
+ rootContainer?.addSettingsButtonToPresentationContainer(containerView)
} else {
- logger?.warning("Cannot obtain the containerView for presentation controller when presenting with adaptive style \(actualStyle.rawValue) and missing transition coordinator.")
+ logger.warning("Cannot obtain the containerView for presentation controller when presenting with adaptive style \(actualStyle.rawValue) and missing transition coordinator.")
}
}
}
@@ -774,9 +895,7 @@ extension AppDelegate: UIAdaptivePresentationControllerDelegate {
extension AppDelegate: RelayCacheObserver {
func relayCache(_ relayCache: RelayCache.Tracker, didUpdateCachedRelays cachedRelays: RelayCache.CachedRelays) {
- DispatchQueue.main.async {
- self.cachedRelays = cachedRelays
- }
+ selectLocationViewController?.setCachedRelays(cachedRelays)
}
}
@@ -825,12 +944,16 @@ extension AppDelegate: UISplitViewControllerDelegate {
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
- if response.notification.request.identifier == accountExpiryNotificationIdentifier,
- response.actionIdentifier == UNNotificationDefaultActionIdentifier {
- rootContainer?.showSettings(navigateTo: .account, animated: true)
+ let blockOperation = AsyncBlockOperation(dispatchQueue: .main) {
+ if response.notification.request.identifier == accountExpiryNotificationIdentifier,
+ response.actionIdentifier == UNNotificationDefaultActionIdentifier {
+ self.rootContainer?.showSettings(navigateTo: .account, animated: true)
+ }
+
+ completionHandler()
}
- completionHandler()
+ operationQueue.addOperation(blockOperation)
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
diff --git a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift
index 6fb85bd1dd..6b581edf84 100644
--- a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift
+++ b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift
@@ -56,20 +56,20 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver {
}
}
- /// A private hash map that maps each payment to account token
+ /// A private hash map that maps each payment to account token.
private var paymentToAccountToken = [SKPayment: String]()
- /// Returns true if the device is able to make payments
+ /// Returns true if the device is able to make payments.
class var canMakePayments: Bool {
return SKPaymentQueue.canMakePayments()
}
init(queue: SKPaymentQueue) {
- self.paymentQueue = queue
+ paymentQueue = queue
}
func startPaymentQueueMonitoring() {
- self.logger.debug("Start payment queue monitoring")
+ logger.debug("Start payment queue monitoring")
paymentQueue.add(self)
}
diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift
index 3e2f6926ba..b83ae621e2 100644
--- a/ios/MullvadVPN/ConnectViewController.swift
+++ b/ios/MullvadVPN/ConnectViewController.swift
@@ -131,6 +131,10 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen
// MARK: - TunnelObserver
+ func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) {
+ // no-op
+ }
+
func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) {
setNeedsHeaderBarStyleAppearanceUpdate()
}
@@ -358,7 +362,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen
}
@objc func handleReconnect(_ sender: Any) {
- TunnelManager.shared.reconnectTunnel(completionHandler: nil)
+ TunnelManager.shared.reconnectTunnel()
}
@objc func handleSelectLocation(_ sender: Any) {
@@ -409,13 +413,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen
return nil
}
- func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
- print("mapView regionWillChangeAnimated: \(animated)")
- }
-
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
- print("mapView regionDidChangeAnimated: \(animated)")
-
mapRegionAnimationDidEnd?()
mapRegionAnimationDidEnd = nil
}
diff --git a/ios/MullvadVPN/DisplayChainedError.swift b/ios/MullvadVPN/DisplayChainedError.swift
index 065baddee4..0120d2fb14 100644
--- a/ios/MullvadVPN/DisplayChainedError.swift
+++ b/ios/MullvadVPN/DisplayChainedError.swift
@@ -183,6 +183,13 @@ extension TunnelManager.Error: DisplayChainedError {
value: "Internal error: account is unset",
comment: ""
)
+ case .unsetTunnel:
+ return NSLocalizedString(
+ "UNSET_TUNNEL_ERROR",
+ tableName: "TunnelManager",
+ value: "Tunnel is unset.",
+ comment: ""
+ )
case .readRelays:
return NSLocalizedString(
"READ_RELAYS_ERROR",
diff --git a/ios/MullvadVPN/NotificationController.swift b/ios/MullvadVPN/NotificationController.swift
index 6bb875a8f8..ed7d03e0ee 100644
--- a/ios/MullvadVPN/NotificationController.swift
+++ b/ios/MullvadVPN/NotificationController.swift
@@ -15,7 +15,7 @@ struct InAppNotificationDescriptor: Equatable {
var body: String
}
-class NotificationController: UIViewController, NotificationManagerDelegate {
+class NotificationController: UIViewController {
let bannerView: NotificationBannerView = {
let bannerView = NotificationBannerView()
bannerView.translatesAutoresizingMaskIntoConstraints = false
@@ -125,12 +125,7 @@ class NotificationController: UIViewController, NotificationManagerDelegate {
}
}
- private func updateAccessibilityFrame() {
- let layoutFrame = bannerView.layoutMarginsGuide.layoutFrame
- bannerView.accessibilityFrame = UIAccessibility.convertToScreenCoordinates(layoutFrame, in: view)
- }
-
- private func setNotifications(_ notifications: [InAppNotificationDescriptor], animated: Bool) {
+ func setNotifications(_ notifications: [InAppNotificationDescriptor], animated: Bool) {
let nextNotification = notifications.first
if let notification = nextNotification {
@@ -141,10 +136,9 @@ class NotificationController: UIViewController, NotificationManagerDelegate {
}
}
- // MARK: - NotificationManagerDelegate
-
- func notificationManagerDidUpdateInAppNotifications(_ manager: NotificationManager, notifications: [InAppNotificationDescriptor]) {
- setNotifications(notifications, animated: true)
+ private func updateAccessibilityFrame() {
+ let layoutFrame = bannerView.layoutMarginsGuide.layoutFrame
+ bannerView.accessibilityFrame = UIAccessibility.convertToScreenCoordinates(layoutFrame, in: view)
}
}
diff --git a/ios/MullvadVPN/NotificationManager.swift b/ios/MullvadVPN/NotificationManager.swift
index 0204396c57..d2b1842cd6 100644
--- a/ios/MullvadVPN/NotificationManager.swift
+++ b/ios/MullvadVPN/NotificationManager.swift
@@ -92,6 +92,10 @@ class NotificationManager: NotificationProviderDelegate {
}
}
+ static let shared = NotificationManager()
+
+ private init() {}
+
func updateNotifications() {
assert(Thread.isMainThread)
@@ -132,7 +136,10 @@ class NotificationManager: NotificationProviderDelegate {
for newRequest in newSystemNotificationRequests {
notificationCenter.add(newRequest) { (error) in
if let error = error {
- self.logger.error("Failed to add notification request with identifier \(newRequest.identifier). Error: \(error.localizedDescription)")
+ self.logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to add notification request with identifier \(newRequest.identifier)."
+ )
}
}
}
@@ -154,7 +161,10 @@ class NotificationManager: NotificationProviderDelegate {
case .notDetermined:
userNotificationCenter.requestAuthorization(options: authorizationOptions) { (granted, error) in
if let error = error {
- self.logger.error("Failed to obtain user notifications authorization: \(error.localizedDescription)")
+ self.logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to obtain user notifications authorization"
+ )
}
completion(granted)
}
diff --git a/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift b/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift
index 51633cc85a..5fa9794bf9 100644
--- a/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift
+++ b/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift
@@ -126,6 +126,10 @@ class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificatio
// MARK: - TunnelObserver
+ func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) {
+ // no-op
+ }
+
func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) {
// no-op
}
diff --git a/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift b/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift
index de0aab4653..3018400c22 100644
--- a/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift
+++ b/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift
@@ -36,6 +36,12 @@ class TunnelErrorNotificationProvider: NotificationProvider, InAppNotificationPr
TunnelManager.shared.addObserver(self)
}
+ // MARK: - TunnelObserver
+
+ func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) {
+ // no-op
+ }
+
func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) {
// Reset error with each new connection attempt
if case .connecting = tunnelState {
diff --git a/ios/MullvadVPN/Operations/InputInjectionBuilder.swift b/ios/MullvadVPN/Operations/InputInjectionBuilder.swift
index 14f2ed9ce3..87fff94146 100644
--- a/ios/MullvadVPN/Operations/InputInjectionBuilder.swift
+++ b/ios/MullvadVPN/Operations/InputInjectionBuilder.swift
@@ -54,6 +54,23 @@ class InputInjectionBuilder<OperationType, Context> where OperationType: InputOp
return self
}
+ func injectCompletion<T, Success, Failure>(
+ from dependency: T,
+ via block: @escaping (inout Context, T.Completion) -> Void
+ ) -> Self
+ where T: ResultOperation<Success, Failure>
+ {
+ inputBlocks.append { context in
+ if let completion = dependency.completion {
+ block(&context, completion)
+ }
+ }
+
+ operation.addDependency(dependency)
+
+ return self
+ }
+
func reduce(_ reduceBlock: @escaping (Context) -> OperationType.Input?) {
operation.setInputBlock {
for inputBlock in self.inputBlocks {
diff --git a/ios/MullvadVPN/Operations/OperationCompletion.swift b/ios/MullvadVPN/Operations/OperationCompletion.swift
index 393dbd8ecd..e8055572e1 100644
--- a/ios/MullvadVPN/Operations/OperationCompletion.swift
+++ b/ios/MullvadVPN/Operations/OperationCompletion.swift
@@ -121,4 +121,12 @@ enum OperationCompletion<Success, Failure: Error> {
return success as! NewSuccess
}
}
+
+ func assertFailure<NewFailure: Error>(_ failureType: NewFailure.Type)
+ -> OperationCompletion<Success, NewFailure>
+ {
+ return mapError { error -> NewFailure in
+ return error as! NewFailure
+ }
+ }
}
diff --git a/ios/MullvadVPN/PreferencesViewController.swift b/ios/MullvadVPN/PreferencesViewController.swift
index a67fd9f47d..d3ab3ffa74 100644
--- a/ios/MullvadVPN/PreferencesViewController.swift
+++ b/ios/MullvadVPN/PreferencesViewController.swift
@@ -82,6 +82,10 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
// MARK: - TunnelObserver
+ func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) {
+ // no-op
+ }
+
func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) {
// no-op
}
diff --git a/ios/MullvadVPN/RelayCache/RelayCacheIO.swift b/ios/MullvadVPN/RelayCache/RelayCacheIO.swift
index ccd623d25a..20cbe90f6a 100644
--- a/ios/MullvadVPN/RelayCache/RelayCacheIO.swift
+++ b/ios/MullvadVPN/RelayCache/RelayCacheIO.swift
@@ -14,8 +14,12 @@ extension RelayCache {
extension RelayCache.IO {
/// The default cache file location bound by app group container.
- static func defaultCacheFileURL(forSecurityApplicationGroupIdentifier appGroupIdentifier: String) -> URL? {
- let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
+ static func defaultCacheFileURL(
+ forSecurityApplicationGroupIdentifier appGroupIdentifier: String
+ ) -> URL? {
+ let containerURL = FileManager.default.containerURL(
+ forSecurityApplicationGroupIdentifier: appGroupIdentifier
+ )
return containerURL?.appendingPathComponent("relays.json")
}
@@ -57,9 +61,11 @@ extension RelayCache.IO {
return try result!.get()
}
- /// Safely read the cache file from disk using file coordinator and fallback to prebundled relays in case if the
- /// relay cache file is missing.
- static func readWithFallback(cacheFileURL: URL, preBundledRelaysFileURL: URL) throws -> RelayCache.CachedRelays {
+ /// Safely read the cache file from disk using file coordinator and fallback to prebundled
+ /// relays in case if the relay cache file is missing.
+ static func readWithFallback(cacheFileURL: URL, preBundledRelaysFileURL: URL)
+ throws -> RelayCache.CachedRelays
+ {
do {
return try Self.read(cacheFileURL: cacheFileURL)
} catch {
diff --git a/ios/MullvadVPN/RelayCache/RelayCacheObserver.swift b/ios/MullvadVPN/RelayCache/RelayCacheObserver.swift
index 53a9edc299..879157bf60 100644
--- a/ios/MullvadVPN/RelayCache/RelayCacheObserver.swift
+++ b/ios/MullvadVPN/RelayCache/RelayCacheObserver.swift
@@ -9,5 +9,8 @@
import Foundation
protocol RelayCacheObserver: AnyObject {
- func relayCache(_ relayCache: RelayCache.Tracker, didUpdateCachedRelays cachedRelays: RelayCache.CachedRelays)
+ func relayCache(
+ _ relayCache: RelayCache.Tracker,
+ didUpdateCachedRelays cachedRelays: RelayCache.CachedRelays
+ )
}
diff --git a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
index 218c55f69c..f7ffcf2f72 100644
--- a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
+++ b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
@@ -6,46 +6,73 @@
// Copyright © 2019 Mullvad VPN AB. All rights reserved.
//
-import BackgroundTasks
import Foundation
import Logging
import UIKit
extension RelayCache {
+ /// Type describing the result of an attempt to fetch the new relay list from server.
+ enum FetchResult: CustomStringConvertible {
+ /// Request to update relays was throttled.
+ case throttled
+
+ /// Refreshed relays but the same content was found on remote.
+ case sameContent
+
+ /// Refreshed relays with new content.
+ case newContent
+
+ var description: String {
+ switch self {
+ case .throttled:
+ return "throttled"
+ case .sameContent:
+ return "same content"
+ case .newContent:
+ return "new content"
+ }
+ }
+ }
+
class Tracker {
- /// Relay update interval (in seconds)
- private static let relayUpdateInterval: TimeInterval = 60 * 60
+ /// Relay update interval (in seconds).
+ static let relayUpdateInterval: TimeInterval = 60 * 60
- /// Tracker log
+ /// Tracker log.
private let logger = Logger(label: "RelayCacheTracker")
- /// The cache location used by the class instance
+ /// The cache location used by the class instance.
private let cacheFileURL: URL
- /// The location of prebundled `relays.json`
+ /// The location of prebundled `relays.json`.
private let prebundledRelaysFileURL: URL
- /// A dispatch queue used for thread synchronization
- private let stateQueue = DispatchQueue(label: "RelayCacheTrackerStateQueue")
+ /// Lock used for synchronization.
+ private let nslock = NSLock()
/// Internal operation queue.
private let operationQueue: OperationQueue = {
let operationQueue = AsyncOperationQueue()
- operationQueue.name = "RelayCacheTrackerQueue"
operationQueue.maxConcurrentOperationCount = 1
return operationQueue
}()
- /// A timer source used for periodic updates
+ /// A timer source used for periodic updates.
private var timerSource: DispatchSourceTimer?
- /// A flag that indicates whether periodic updates are running
+ /// A flag that indicates whether periodic updates are running.
private var isPeriodicUpdatesEnabled = false
- /// Observers
+ /// API proxy.
+ private let apiProxy = REST.ProxyFactory.shared.createAPIProxy()
+
+ /// Observers.
private let observerList = ObserverList<RelayCacheObserver>()
+ /// Memory cache.
+ private var cachedRelays: CachedRelays?
+
/// A shared instance of `RelayCache`
static let shared: RelayCache.Tracker = {
let cacheFileURL = RelayCache.IO.defaultCacheFileURL(forSecurityApplicationGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier)!
@@ -60,102 +87,105 @@ extension RelayCache {
private init(cacheFileURL: URL, prebundledRelaysFileURL: URL) {
self.cacheFileURL = cacheFileURL
self.prebundledRelaysFileURL = prebundledRelaysFileURL
+
+ do {
+ cachedRelays = try RelayCache.IO.readWithFallback(
+ cacheFileURL: cacheFileURL,
+ preBundledRelaysFileURL: prebundledRelaysFileURL
+ )
+ } catch {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to read the relay cache during initialization."
+ )
+
+ _ = updateRelays(completionHandler: nil)
+ }
}
func startPeriodicUpdates() {
- stateQueue.async {
- guard !self.isPeriodicUpdatesEnabled else { return }
+ nslock.lock()
+ defer { nslock.unlock() }
- self.logger.debug("Start periodic relay updates.")
+ guard !isPeriodicUpdatesEnabled else { return }
- self.isPeriodicUpdatesEnabled = true
+ logger.debug("Start periodic relay updates.")
- do {
- let cachedRelays = try RelayCache.IO.read(cacheFileURL: self.cacheFileURL)
- let nextUpdate = cachedRelays.updatedAt
- .addingTimeInterval(Self.relayUpdateInterval)
+ isPeriodicUpdatesEnabled = true
- self.scheduleRepeatingTimer(startTime: .now() + nextUpdate.timeIntervalSinceNow)
- } catch {
- self.logger.error(
- chainedError: AnyChainedError(error),
- message: "Failed to read the relay cache."
- )
+ let nextUpdate = _getNextUpdateDate()
- if let readError = error as? RelayCache.Error,
- Self.shouldDownloadRelaysOnReadFailure(readError) {
- self.scheduleRepeatingTimer(startTime: .now())
- }
- }
- }
+ scheduleRepeatingTimer(startTime: .now() + nextUpdate.timeIntervalSinceNow)
}
func stopPeriodicUpdates() {
- stateQueue.async {
- guard self.isPeriodicUpdatesEnabled else { return }
+ nslock.lock()
+ defer { nslock.unlock() }
- self.logger.debug("Stop periodic relay updates.")
+ guard isPeriodicUpdatesEnabled else { return }
- self.isPeriodicUpdatesEnabled = false
+ logger.debug("Stop periodic relay updates.")
- self.timerSource?.cancel()
- self.timerSource = nil
- }
+ isPeriodicUpdatesEnabled = false
+
+ timerSource?.cancel()
+ timerSource = nil
}
func updateRelays(
- completionHandler: @escaping (
- OperationCompletion<RelayCache.FetchResult, RelayCache.Error>
- ) -> Void
- ) -> Cancellable {
- let operation = UpdateRelaysOperation(
- dispatchQueue: stateQueue,
- apiProxy: REST.ProxyFactory.shared.createAPIProxy(),
- cacheFileURL: self.cacheFileURL,
- relayUpdateInterval: Self.relayUpdateInterval,
- updateHandler: { [weak self] newCachedRelays in
- guard let self = self else { return }
+ completionHandler: (
+ (OperationCompletion<RelayCache.FetchResult, RelayCache.Error>) -> Void
+ )? = nil
+ ) -> Cancellable
+ {
+ let operation = ResultBlockOperation<RelayCache.FetchResult, RelayCache.Error>(
+ dispatchQueue: nil
+ ) { operation in
+ let cachedRelays = self.getCachedRelays()
- DispatchQueue.main.async {
- self.observerList.forEach { observer in
- observer.relayCache(self, didUpdateCachedRelays: newCachedRelays)
- }
- }
- },
- completionHandler: completionHandler
- )
+ if self.getNextUpdateDate() > Date() {
+ operation.finish(completion: .success(.throttled))
+ return
+ }
+
+ let task = self.apiProxy.getRelays(
+ etag: cachedRelays?.etag,
+ retryStrategy: .noRetry
+ ) { completion in
+ operation.finish(
+ completion: self.handleResponse(completion: completion)
+ )
+ }
+
+ operation.addCancellationBlock {
+ task.cancel()
+ }
+ }
operation.addObserver(
BackgroundObserver(name: "Update relays", cancelUponExpiration: true)
)
+ operation.completionQueue = .main
+ operation.completionHandler = completionHandler
+
operationQueue.addOperation(operation)
return operation
}
- func read(completionHandler: @escaping (Result<CachedRelays, RelayCache.Error>) -> Void) {
- stateQueue.async {
- let result = Result {
- try RelayCache.IO.readWithFallback(
- cacheFileURL: self.cacheFileURL,
- preBundledRelaysFileURL: self.prebundledRelaysFileURL
- )
- }.mapError { error in
- return error as! RelayCache.Error
- }
+ func getCachedRelays() -> CachedRelays? {
+ nslock.lock()
+ defer { nslock.unlock() }
- completionHandler(result)
- }
+ return cachedRelays
}
- func readAndWait() throws -> CachedRelays {
- return try stateQueue.sync {
- return try RelayCache.IO.readWithFallback(
- cacheFileURL: cacheFileURL,
- preBundledRelaysFileURL: prebundledRelaysFileURL
- )
- }
+ func getNextUpdateDate() -> Date {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ return _getNextUpdateDate()
}
// MARK: - Observation
@@ -168,300 +198,101 @@ extension RelayCache {
observerList.remove(observer)
}
- // MARK: - Private instance methods
-
- private func scheduleRepeatingTimer(startTime: DispatchWallTime) {
- let timerSource = DispatchSource.makeTimerSource(queue: stateQueue)
- timerSource.setEventHandler { [weak self] in
- _ = self?.updateRelays(completionHandler: { _ in
- // no-op
- })
- }
-
- timerSource.schedule(wallDeadline: startTime, repeating: .seconds(Int(Self.relayUpdateInterval)))
- timerSource.activate()
-
- self.timerSource = timerSource
- }
-
- // MARK: - Private class methods
+ // MARK: - Private
- private class func shouldDownloadRelaysOnReadFailure(_ error: RelayCache.Error) -> Bool {
- switch error {
- case .readPrebundledRelays, .decodePrebundledRelays, .decodeCache:
- return true
+ private func _getNextUpdateDate() -> Date {
+ let now = Date()
- case .readCache(CocoaError.fileReadNoSuchFile):
- return true
-
- default:
- return false
+ guard let cachedRelays = cachedRelays else {
+ return now
}
- }
- }
-}
-
-extension RelayCache {
+ let nextUpdate = cachedRelays.updatedAt.addingTimeInterval(Self.relayUpdateInterval)
- /// Type describing the result of an attempt to fetch the new relay list from server.
- enum FetchResult: CustomStringConvertible {
- /// Request to update relays was throttled.
- case throttled
-
- /// Refreshed relays but the same content was found on remote.
- case sameContent
-
- /// Refreshed relays with new content.
- case newContent
-
- var description: String {
- switch self {
- case .throttled:
- return "throttled"
- case .sameContent:
- return "same content"
- case .newContent:
- return "new content"
- }
+ return max(nextUpdate, Date())
}
- }
-
-}
-
-// 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() throws {
- let cachedRelays = try readAndWait()
- let beginDate = cachedRelays.updatedAt.addingTimeInterval(Self.relayUpdateInterval)
-
- try submitAppRefreshTask(at: beginDate)
- }
-
- /// Create and submit task request to scheduler.
- private func submitAppRefreshTask(at beginDate: Date) throws {
- let taskIdentifier = ApplicationConfiguration.appRefreshTaskIdentifier
-
- let request = BGAppRefreshTaskRequest(identifier: taskIdentifier)
- request.earliestBeginDate = beginDate
- try BGTaskScheduler.shared.submit(request)
- }
+ private func handleResponse(
+ completion: OperationCompletion<REST.ServerRelaysCacheResponse, REST.Error>
+ ) -> OperationCompletion<FetchResult, RelayCache.Error>
+ {
+ let mappedCompletion = completion
+ .mapError { error -> RelayCache.Error in
+ return .rest(error)
+ }
+ .tryMap { response -> FetchResult in
+ switch response {
+ case .newContent(let etag, let relays):
+ try self.storeResponse(etag: etag, relays: relays)
- /// Background task handler
- private func handleAppRefreshTask(_ task: BGAppRefreshTask) {
- logger.debug("Start app refresh task.")
+ return .newContent
- let cancellable = updateRelays { completion in
- switch completion {
- case .success(let fetchResult):
- self.logger.debug("Finished updating relays in app refresh task: \(fetchResult).")
+ case .notModified:
+ return .sameContent
+ }
+ }
+ .assertFailure(RelayCache.Error.self)
- case .failure(let error):
- self.logger.error(
+ if let error = mappedCompletion.error {
+ logger.error(
chainedError: error,
- message: "Failed to update relays in app refresh task."
+ message: "Failed to update relays."
)
-
- case .cancelled:
- self.logger.debug("App refresh task was cancelled.")
}
- task.setTaskCompleted(success: completion.isSuccess)
+ return mappedCompletion
}
- task.expirationHandler = {
- cancellable.cancel()
- }
+ private func storeResponse(etag: String?, relays: REST.ServerRelaysResponse) throws {
+ let numRelays = relays.wireguard.relays.count
- // Schedule next refresh
- let scheduleDate = Date(timeIntervalSinceNow: Self.relayUpdateInterval)
- do {
- try submitAppRefreshTask(at: scheduleDate)
+ logger.info("Downloaded \(numRelays) relays.")
- logger.debug("Scheduled next app refresh task at \(scheduleDate.logFormatDate()).")
- } catch {
- logger.error(
- chainedError: AnyChainedError(error),
- message: "Failed to schedule next app refresh task."
+ let newCachedRelays = RelayCache.CachedRelays(
+ etag: etag,
+ relays: relays,
+ updatedAt: Date()
)
- }
- }
-}
-
-fileprivate class UpdateRelaysOperation: ResultOperation<RelayCache.FetchResult, RelayCache.Error> {
- typealias UpdateHandler = (RelayCache.CachedRelays) -> Void
-
- private let apiProxy: REST.APIProxy
- private let cacheFileURL: URL
- private let relayUpdateInterval: TimeInterval
-
- private let logger = Logger(label: "RelayCacheTracker.UpdateRelaysOperation")
-
- private let updateHandler: UpdateHandler
- private var downloadTask: Cancellable?
- init(
- dispatchQueue: DispatchQueue,
- apiProxy: REST.APIProxy,
- cacheFileURL: URL,
- relayUpdateInterval: TimeInterval,
- updateHandler: @escaping UpdateHandler,
- completionHandler: @escaping CompletionHandler
- )
- {
- self.apiProxy = apiProxy
- self.cacheFileURL = cacheFileURL
- self.relayUpdateInterval = relayUpdateInterval
- self.updateHandler = updateHandler
+ nslock.lock()
+ cachedRelays = newCachedRelays
+ nslock.unlock()
- super.init(
- dispatchQueue: dispatchQueue,
- completionQueue: dispatchQueue,
- completionHandler: completionHandler
- )
- }
-
- override func main() {
- do {
- let cachedRelays = try RelayCache.IO.read(cacheFileURL: cacheFileURL)
- let nextUpdate = cachedRelays.updatedAt.addingTimeInterval(relayUpdateInterval)
-
- if nextUpdate <= Date() {
- downloadRelays(previouslyCachedRelays: cachedRelays)
- } else {
- finish(completion: .success(.throttled))
+ DispatchQueue.main.async {
+ self.observerList.forEach { observer in
+ observer.relayCache(self, didUpdateCachedRelays: newCachedRelays)
+ }
}
- } catch {
- let error = error as! RelayCache.Error
- logger.error(
- chainedError: error,
- message: "Failed to read the relay cache to determine if it needs to be updated."
- )
-
- if shouldDownloadRelaysOnReadFailure(error) {
- downloadRelays(previouslyCachedRelays: nil)
- } else {
- finish(completion: .failure(error))
+ do {
+ try RelayCache.IO.write(
+ cacheFileURL: cacheFileURL,
+ record: newCachedRelays
+ )
+ } catch {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to store downloaded relays."
+ )
+ throw error
}
}
- }
-
- override func operationDidCancel() {
- downloadTask?.cancel()
- downloadTask = nil
- }
-
- private func didReceiveNewRelays(etag: String?, relays: REST.ServerRelaysResponse) {
- let numRelays = relays.wireguard.relays.count
-
- logger.info("Downloaded \(numRelays) relays.")
-
- let cachedRelays = RelayCache.CachedRelays(
- etag: etag,
- relays: relays,
- updatedAt: Date()
- )
-
- do {
- try RelayCache.IO.write(cacheFileURL: cacheFileURL, record: cachedRelays)
- updateHandler(cachedRelays)
- finish(completion: .success(.newContent))
- } catch {
- let error = error as! RelayCache.Error
-
- logger.error(
- chainedError: error,
- message: "Failed to store downloaded relays."
- )
-
- finish(completion: .failure(error))
- }
- }
-
- private func didReceiveNotModified(previouslyCachedRelays: RelayCache.CachedRelays) {
- var cachedRelays = previouslyCachedRelays
- cachedRelays.updatedAt = Date()
-
- logger.info("Relays haven't changed since last check.")
-
- do {
- try RelayCache.IO.write(cacheFileURL: cacheFileURL, record: cachedRelays)
-
- finish(completion: .success(.sameContent))
- } catch {
- let error = error as! RelayCache.Error
+ private func scheduleRepeatingTimer(startTime: DispatchWallTime) {
+ let timerSource = DispatchSource.makeTimerSource()
+ timerSource.setEventHandler { [weak self] in
+ _ = self?.updateRelays()
+ }
- logger.error(
- chainedError: error,
- message: "Failed to update cached relays timestamp."
+ timerSource.schedule(
+ wallDeadline: startTime,
+ repeating: .seconds(Int(Self.relayUpdateInterval))
)
+ timerSource.activate()
- finish(completion: .failure(error))
- }
- }
-
- private func didFailToDownloadRelays(error: REST.Error) {
- logger.error(chainedError: error, message: "Failed to download relays.")
-
- finish(completion: .failure(.rest(error)))
- }
-
- private func downloadRelays(previouslyCachedRelays: RelayCache.CachedRelays?) {
- downloadTask = apiProxy.getRelays(etag: previouslyCachedRelays?.etag, retryStrategy: .noRetry) { [weak self] completion in
- guard let self = self else { return }
-
- self.dispatchQueue.async {
- switch completion {
- case .success(.newContent(let etag, let relays)):
- self.didReceiveNewRelays(etag: etag, relays: relays)
-
- case .success(.notModified):
- self.didReceiveNotModified(previouslyCachedRelays: previouslyCachedRelays!)
-
- case .failure(let error):
- self.didFailToDownloadRelays(error: error)
-
- case .cancelled:
- self.logger.debug("Cancelled relays download.")
- self.finish(completion: .cancelled)
- }
- }
+ self.timerSource = timerSource
}
}
- private func shouldDownloadRelaysOnReadFailure(_ error: RelayCache.Error) -> Bool {
- switch error {
- case .readPrebundledRelays, .decodePrebundledRelays, .decodeCache:
- return true
-
- case .readCache(CocoaError.fileReadNoSuchFile):
- return true
-
- default:
- return false
- }
- }
}
diff --git a/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift b/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift
index d1a5679bf7..6cebcaaa75 100644
--- a/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift
+++ b/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift
@@ -8,38 +8,12 @@
import UIKit
-extension OperationCompletion where Success == AddressCache.CacheUpdateResult {
- var backgroundFetchResult: UIBackgroundFetchResult {
+extension OperationCompletion {
+ func backgroundFetchResult(_ hasNewData: (Success) -> Bool) -> UIBackgroundFetchResult {
switch self {
- case .success(.finished):
- return .newData
- case .success(.throttled), .cancelled:
- return .noData
- case .failure:
- return .failed
- }
- }
-}
-
-extension OperationCompletion where Success == TunnelManager.KeyRotationResult {
- var backgroundFetchResult: UIBackgroundFetchResult {
- switch self {
- case .success(.finished):
- return .newData
- case .success(.throttled), .cancelled:
- return .noData
- case .failure:
- return .failed
- }
- }
-}
-
-extension OperationCompletion where Success == RelayCache.FetchResult {
- var backgroundFetchResult: UIBackgroundFetchResult {
- switch self {
- case .success(.newContent):
- return .newData
- case .success(.throttled), .success(.sameContent), .cancelled:
+ case .success(let value):
+ return hasNewData(value) ? .newData : .noData
+ case .cancelled:
return .noData
case .failure:
return .failed
diff --git a/ios/MullvadVPN/SettingsDataSource.swift b/ios/MullvadVPN/SettingsDataSource.swift
index f0c0835c9a..551d78b2f2 100644
--- a/ios/MullvadVPN/SettingsDataSource.swift
+++ b/ios/MullvadVPN/SettingsDataSource.swift
@@ -234,6 +234,10 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab
// MARK: - TunnelObserver
+ func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) {
+ // no-op
+ }
+
func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) {
// no-op
}
diff --git a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift
index 7ad0d19a34..975eb98ab4 100644
--- a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift
+++ b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift
@@ -83,14 +83,8 @@ class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate {
}
private func pickRelay() -> RelaySelectorResult? {
- let cachedRelays: RelayCache.CachedRelays
- do {
- cachedRelays = try RelayCache.Tracker.shared.readAndWait()
- } catch {
- providerLogger.error(
- chainedError: AnyChainedError(error),
- message: "Failed to read relays when picking relay."
- )
+ guard let cachedRelays = RelayCache.Tracker.shared.getCachedRelays() else {
+ providerLogger.error("Failed to obtain relays when picking relay.")
return nil
}
diff --git a/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift b/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift
index 50fad00647..131bc97873 100644
--- a/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift
@@ -13,18 +13,10 @@ class LoadTunnelConfigurationOperation: ResultOperation<(), TunnelManager.Error>
private let logger = Logger(label: "LoadTunnelConfigurationOperation")
private let state: TunnelManager.State
- init(
- dispatchQueue: DispatchQueue,
- state: TunnelManager.State,
- completionHandler: @escaping CompletionHandler
- ) {
+ init(dispatchQueue: DispatchQueue, state: TunnelManager.State) {
self.state = state
- super.init(
- dispatchQueue: dispatchQueue,
- completionQueue: dispatchQueue,
- completionHandler: completionHandler
- )
+ super.init(dispatchQueue: dispatchQueue)
}
override func main() {
@@ -40,88 +32,72 @@ class LoadTunnelConfigurationOperation: ResultOperation<(), TunnelManager.Error>
}
private func didLoadVPNConfigurations(tunnels: [TunnelProviderManagerType]?) {
- let tunnelProvider = tunnels?.first
-
+ var returnError: TunnelManager.Error?
+ var tunnelSettings: TunnelSettingsV2?
do {
- let tunnelSettings = try SettingsManager.readSettings()
- let tunnel = tunnelProvider.map { tunnelProvider in
- return Tunnel(tunnelProvider: tunnelProvider)
- }
-
- state.tunnelSettings = tunnelSettings
- state.setTunnel(tunnel, shouldRefreshTunnelState: true)
-
- finish(completion: .success(()))
- } catch .itemNotFound as KeychainError {
+ tunnelSettings = try SettingsManager.readSettings()
+ } catch .itemNotFound as KeychainError {
logger.debug("Settings not found in keychain.")
-
- state.tunnelSettings = nil
- state.setTunnel(nil, shouldRefreshTunnelState: true)
-
- if let tunnelProvider = tunnelProvider {
- removeOrphanedTunnel(tunnelProvider: tunnelProvider) { error in
- self.finish(completion: error.map { .failure($0) } ?? .success(()))
- }
- } else {
- finish(completion: .success(()))
- }
} catch let error as DecodingError {
- state.tunnelSettings = nil
- state.setTunnel(nil, shouldRefreshTunnelState: true)
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Cannot decode settings. Will attempt to delete them from keychain."
+ )
do {
- logger.error(
- chainedError: AnyChainedError(error),
- message: "Cannot decode settings. Will attempt to delete them from keychain."
- )
-
try SettingsManager.deleteSettings()
} catch {
+ returnError = .deleteSettings(error)
+
logger.error(
chainedError: AnyChainedError(error),
message: "Failed to delete settings from keychain."
)
}
+ } catch {
+ returnError = .readSettings(error)
- let returnError: TunnelManager.Error = .readSettings(error)
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Unexpected error when reading settings."
+ )
+ }
- if let tunnelProvider = tunnelProvider {
- removeOrphanedTunnel(tunnelProvider: tunnelProvider) { _ in
- self.finish(completion: .failure(returnError))
- }
- } else {
- finish(completion: .failure(returnError))
- }
- } catch {
- state.tunnelSettings = nil
- state.setTunnel(nil, shouldRefreshTunnelState: true)
+ let tunnel = tunnels?.first.map { tunnelProvider in
+ return Tunnel(tunnelProvider: tunnelProvider)
+ }
- let returnError: TunnelManager.Error = .readSettings(error)
+ if let tunnelSettings = tunnelSettings {
+ state.tunnelSettings = tunnelSettings
+ state.setTunnel(tunnel, shouldRefreshTunnelState: true)
+ state.isLoadedConfiguration = true
- if let tunnelProvider = tunnelProvider {
- removeOrphanedTunnel(tunnelProvider: tunnelProvider) { _ in
- self.finish(completion: .failure(returnError))
- }
- } else {
- finish(completion: .failure(returnError))
+ finish(completion: .success(()))
+ } else {
+ let onFinish = {
+ self.state.tunnelSettings = nil
+ self.state.setTunnel(nil, shouldRefreshTunnelState: true)
+ self.state.isLoadedConfiguration = returnError == nil
+
+ self.finish(completion: returnError.map { .failure($0) } ?? .success(()))
}
- }
- }
- private func removeOrphanedTunnel(tunnelProvider: TunnelProviderManagerType, completion: @escaping (TunnelManager.Error?) -> Void) {
- logger.debug("Remove orphaned VPN configuration.")
+ if let tunnel = tunnel {
+ logger.debug("Remove orphaned VPN configuration.")
- tunnelProvider.removeFromPreferences { error in
- self.dispatchQueue.async {
- if let error = error {
- self.logger.error(
- chainedError: AnyChainedError(error),
- message: "Failed to remove VPN configuration."
- )
- completion(.removeVPNConfiguration(error))
- } else {
- completion(nil)
+ tunnel.removeFromPreferences { error in
+ self.dispatchQueue.async {
+ if let error = error {
+ self.logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to remove VPN configuration."
+ )
+ }
+ onFinish()
+ }
}
+ } else {
+ onFinish()
}
}
}
diff --git a/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift
index ab610d1fc8..e1d6429d6f 100644
--- a/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift
@@ -12,24 +12,15 @@ class ReloadTunnelOperation: ResultOperation<(), TunnelManager.Error> {
private let state: TunnelManager.State
private var task: Cancellable?
- init(
- queue: DispatchQueue,
- state: TunnelManager.State,
- completionHandler: @escaping CompletionHandler
- )
- {
+ init(dispatchQueue: DispatchQueue, state: TunnelManager.State) {
self.state = state
- super.init(
- dispatchQueue: queue,
- completionQueue: queue,
- completionHandler: completionHandler
- )
+ super.init(dispatchQueue: dispatchQueue)
}
override func main() {
guard let tunnel = self.state.tunnel else {
- finish(completion: .failure(.unsetAccount))
+ finish(completion: .failure(.unsetTunnel))
return
}
@@ -38,9 +29,7 @@ class ReloadTunnelOperation: ResultOperation<(), TunnelManager.Error> {
task = session.reloadTunnelSettings { [weak self] completion in
guard let self = self else { return }
- self.dispatchQueue.async {
- self.finish(completion: completion.mapError { .reloadTunnel($0) })
- }
+ self.finish(completion: completion.mapError { .reloadTunnel($0) })
}
}
diff --git a/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift b/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift
index bde9f12811..d8a0a4fed1 100644
--- a/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift
@@ -10,7 +10,7 @@ import Foundation
import Logging
import class WireGuardKitTypes.PrivateKey
-class RotateKeyOperation: ResultOperation<TunnelManager.KeyRotationResult, TunnelManager.Error> {
+class RotateKeyOperation: ResultOperation<Bool, TunnelManager.Error> {
private let state: TunnelManager.State
private let devicesProxy: REST.DevicesProxy
@@ -51,7 +51,7 @@ class RotateKeyOperation: ResultOperation<TunnelManager.KeyRotationResult, Tunne
if nextRotationDate > Date() {
logger.debug("Throttle private key rotation.")
- finish(completion: .success(.throttled(creationDate)))
+ finish(completion: .success(false))
return
} else {
logger.debug("Private key is old enough, rotate right away.")
@@ -119,7 +119,7 @@ class RotateKeyOperation: ResultOperation<TunnelManager.KeyRotationResult, Tunne
state.tunnelSettings = newTunnelSettings
- finish(completion: .success(.finished))
+ finish(completion: .success(true))
} catch {
logger.error(
chainedError: AnyChainedError(error),
diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
index 0bcfa6458b..9723e29863 100644
--- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
@@ -45,21 +45,16 @@ class StartTunnelOperation: ResultOperation<(), TunnelManager.Error> {
finish(completion: .success(()))
case .disconnected, .pendingReconnect:
- RelayCache.Tracker.shared.read { readResult in
- self.dispatchQueue.async {
- switch readResult {
- case .success(let cachedRelays):
- self.didReceiveRelays(
- tunnelSettings: tunnelSettings,
- cachedRelays: cachedRelays
- )
-
- case .failure(let error):
- self.finish(completion: .failure(.readRelays(error)))
- }
- }
+ guard let cachedRelays = RelayCache.Tracker.shared.getCachedRelays() else {
+ finish(completion: .failure(.readRelays))
+ return
}
+ didReceiveRelays(
+ tunnelSettings: tunnelSettings,
+ cachedRelays: cachedRelays
+ )
+
default:
// Do not attempt to start the tunnel in all other cases.
finish(completion: .success(()))
diff --git a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
index 1b7d008f2d..4ca000562d 100644
--- a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
@@ -28,7 +28,7 @@ class StopTunnelOperation: ResultOperation<(), TunnelManager.Error> {
override func main() {
guard let tunnel = state.tunnel else {
- finish(completion: .failure(.unsetAccount))
+ finish(completion: .failure(.unsetTunnel))
return
}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
index 8dfd651062..ff74c6213e 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
@@ -6,7 +6,6 @@
// Copyright © 2019 Mullvad VPN AB. All rights reserved.
//
-import BackgroundTasks
import Foundation
import NetworkExtension
import UIKit
@@ -70,12 +69,20 @@ final class TunnelManager: TunnelManagerStateDelegate {
private let state: TunnelManager.State
private var privateKeyRotationTimer: DispatchSourceTimer?
+ private var lastKeyRotationData: (
+ attempt: Date,
+ completion: OperationCompletion<Bool, TunnelManager.Error>
+ )?
private var isRunningPeriodicPrivateKeyRotation = false
private var tunnelStatusPollTimer: DispatchSourceTimer?
private var isPolling = false
private var lastConnectingDate: Date?
+ var isLoadedConfiguration: Bool {
+ return state.isLoadedConfiguration
+ }
+
var accountNumber: String? {
return state.tunnelSettings?.account.number
}
@@ -103,17 +110,10 @@ final class TunnelManager: TunnelManagerStateDelegate {
private init(accountsProxy: REST.AccountsProxy, devicesProxy: REST.DevicesProxy) {
self.accountsProxy = accountsProxy
self.devicesProxy = devicesProxy
- self.state = TunnelManager.State(queue: stateQueue)
+ self.state = TunnelManager.State(delegateQueue: stateQueue)
self.state.delegate = self
self.operationQueue.name = "TunnelManager.operationQueue"
self.operationQueue.underlyingQueue = stateQueue
-
- NotificationCenter.default.addObserver(
- self,
- selector: #selector(applicationDidBecomeActive),
- name: UIApplication.didBecomeActiveNotification,
- object: nil
- )
}
// MARK: - Periodic private key rotation
@@ -125,7 +125,6 @@ final class TunnelManager: TunnelManagerStateDelegate {
self.logger.debug("Start periodic private key rotation.")
self.isRunningPeriodicPrivateKeyRotation = true
-
self.updatePrivateKeyRotationTimer()
}
}
@@ -137,62 +136,92 @@ final class TunnelManager: TunnelManagerStateDelegate {
self.logger.debug("Stop periodic private key rotation.")
self.isRunningPeriodicPrivateKeyRotation = false
+ self.updatePrivateKeyRotationTimer()
+ }
+ }
- self.privateKeyRotationTimer?.cancel()
- self.privateKeyRotationTimer = nil
+ func getNextKeyRotationDate() -> Date? {
+ return stateQueue.sync {
+ return _getNextKeyRotationDate()
}
}
private func updatePrivateKeyRotationTimer() {
dispatchPrecondition(condition: .onQueue(stateQueue))
+ privateKeyRotationTimer?.cancel()
+ privateKeyRotationTimer = nil
+
guard self.isRunningPeriodicPrivateKeyRotation else { return }
- if let tunnelSettings = state.tunnelSettings {
- let creationDate = tunnelSettings.device.wgKeyData.creationDate
- let scheduleDate = Date(
- timeInterval: TunnelManagerConfiguration.privateKeyRotationInterval,
- since: creationDate
- )
+ guard let scheduleDate = _getNextKeyRotationDate() else { return }
- schedulePrivateKeyRotationTimer(scheduleDate)
- } else {
- privateKeyRotationTimer?.cancel()
- privateKeyRotationTimer = nil
+ let timer = DispatchSource.makeTimerSource(queue: stateQueue)
+
+ timer.setEventHandler { [weak self] in
+ guard let self = self else { return }
+
+ _ = self.rotatePrivateKey(forceRotate: false) { _ in
+ // no-op
+ }
}
+
+ timer.schedule(wallDeadline: .now() + scheduleDate.timeIntervalSinceNow)
+ timer.activate()
+
+ privateKeyRotationTimer = timer
+
+ logger.debug("Schedule next private key rotation at \(scheduleDate.logFormatDate()).")
}
- /// Schedule new private key rotation timer.
- private func schedulePrivateKeyRotationTimer(_ scheduleDate: Date) {
- dispatchPrecondition(condition: .onQueue(stateQueue))
+ private func _getNextKeyRotationDate() -> Date? {
+ guard let tunnelSettings = state.tunnelSettings else {
+ return nil
+ }
- let timer = DispatchSource.makeTimerSource(queue: stateQueue)
+ if case .some(let (lastAttemptDate, completion)) = lastKeyRotationData {
+ // Do not rotate the key when logged out.
+ if case .unsetAccount = completion.error {
+ return nil
+ }
- timer.setEventHandler { [weak self] in
- guard let self = self else { return }
+ // Do not rotate the key if account or device is not found.
+ if case .rotateKey(.unhandledResponse(_, let serverErrorResponse)) = completion.error,
+ serverErrorResponse?.code == .invalidAccount ||
+ serverErrorResponse?.code == .deviceNotFound {
+ return nil
+ }
- _ = self.rotatePrivateKey { completion in
- self.stateQueue.async {
- if let scheduleDate = self.handlePrivateKeyRotationCompletion(completion) {
- guard self.isRunningPeriodicPrivateKeyRotation else { return }
+ // Retry at equal interval if failed or cancelled.
+ if !completion.isSuccess {
+ let date = lastAttemptDate.addingTimeInterval(
+ TunnelManagerConfiguration.privateKeyRotationFailureRetryInterval
+ )
- self.schedulePrivateKeyRotationTimer(scheduleDate)
- }
- }
+ return max(date, Date())
}
}
- // Cancel active timer
- privateKeyRotationTimer?.cancel()
+ // Rotate at long intervals otherwise.
+ let date = tunnelSettings.device.wgKeyData.creationDate
+ .addingTimeInterval(TunnelManagerConfiguration.privateKeyRotationInterval)
- // Assign new timer
- privateKeyRotationTimer = timer
+ return max(date, Date())
+ }
- // Schedule and activate
- timer.schedule(wallDeadline: .now() + scheduleDate.timeIntervalSinceNow)
- timer.activate()
+ private func setFinishedKeyRotation(_ completion: OperationCompletion<Bool, TunnelManager.Error>) {
+ dispatchPrecondition(condition: .onQueue(stateQueue))
+
+ lastKeyRotationData = (Date(), completion)
+ updatePrivateKeyRotationTimer()
+ }
+
+ private func resetKeyRotationData() {
+ dispatchPrecondition(condition: .onQueue(stateQueue))
+
+ lastKeyRotationData = nil
+ updatePrivateKeyRotationTimer()
- logger.debug("Schedule next private key rotation on \(scheduleDate.logFormatDate())")
}
// MARK: - Public methods
@@ -207,7 +236,9 @@ final class TunnelManager: TunnelManagerStateDelegate {
let loadTunnelOperation = LoadTunnelConfigurationOperation(
dispatchQueue: stateQueue,
state: state
- ) { [weak self] completion in
+ )
+ loadTunnelOperation.completionQueue = stateQueue
+ loadTunnelOperation.completionHandler = { [weak self] completion in
guard let self = self else { return }
dispatchPrecondition(condition: .onQueue(self.stateQueue))
@@ -242,6 +273,13 @@ final class TunnelManager: TunnelManagerStateDelegate {
operationQueue.addOperation(groupOperation)
}
+ func refreshTunnelStatus() {
+ stateQueue.async {
+ self.logger.debug("Refresh tunnel status due to application becoming active.")
+ self._refreshTunnelStatus()
+ }
+ }
+
func startTunnel() {
let operation = StartTunnelOperation(
dispatchQueue: stateQueue,
@@ -274,11 +312,7 @@ final class TunnelManager: TunnelManagerStateDelegate {
dispatchQueue: stateQueue,
state: state
) { [weak self] completion in
- guard let self = self else { return }
-
- dispatchPrecondition(condition: .onQueue(self.stateQueue))
-
- guard let error = completion.error else { return }
+ guard let self = self, let error = completion.error else { return }
// Pass tunnel failure to observers
DispatchQueue.main.async {
@@ -294,8 +328,10 @@ final class TunnelManager: TunnelManagerStateDelegate {
operationQueue.addOperation(operation)
}
- func reconnectTunnel(completionHandler: (() -> Void)?) {
- let operation = ReloadTunnelOperation(queue: stateQueue, state: state) { [weak self] completion in
+ func reconnectTunnel(completionHandler: ((OperationCompletion<(), TunnelManager.Error>) -> Void)? = nil) {
+ let operation = ReloadTunnelOperation(dispatchQueue: stateQueue, state: state)
+
+ operation.completionHandler = { [weak self] completion in
guard let self = self else { return }
dispatchPrecondition(condition: .onQueue(self.stateQueue))
@@ -310,16 +346,17 @@ final class TunnelManager: TunnelManagerStateDelegate {
switch self.tunnelState {
case .connecting, .reconnecting:
self.logger.debug("Refresh tunnel status due to reconnect.")
- self.refreshTunnelStatus()
+ self._refreshTunnelStatus()
default:
break
}
DispatchQueue.main.async {
- completionHandler?()
+ completionHandler?(completion)
}
}
+ operation.completionQueue = stateQueue
operation.addObserver(
BackgroundObserver(name: "Reconnect tunnel", cancelUponExpiration: true)
@@ -355,9 +392,7 @@ final class TunnelManager: TunnelManagerStateDelegate {
operation.completionHandler = { [weak self] completion in
guard let self = self else { return }
- dispatchPrecondition(condition: .onQueue(self.stateQueue))
-
- self.updatePrivateKeyRotationTimer()
+ self.resetKeyRotationData()
DispatchQueue.main.async {
completionHandler(completion)
@@ -428,59 +463,29 @@ final class TunnelManager: TunnelManagerStateDelegate {
return operation
}
- func regeneratePrivateKey(completionHandler: ((TunnelManager.Error?) -> Void)? = nil) {
- let operation = RotateKeyOperation(
- dispatchQueue: stateQueue,
- state: state,
- devicesProxy: devicesProxy,
- rotationInterval: nil
- ) { [weak self] completion in
- guard let self = self else { return }
-
- dispatchPrecondition(condition: .onQueue(self.stateQueue))
-
- switch completion {
- case .success:
- self.updatePrivateKeyRotationTimer()
- self.reconnectTunnel(completionHandler: nil)
-
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to regenerate private key.")
-
- case .cancelled:
- break
- }
-
- DispatchQueue.main.async {
- completionHandler?(completion.error)
- }
+ func rotatePrivateKey(
+ forceRotate: Bool,
+ completionHandler: @escaping (OperationCompletion<Bool, TunnelManager.Error>) -> Void
+ ) -> Cancellable {
+ var rotationInterval: TimeInterval?
+ if !forceRotate {
+ rotationInterval = TunnelManagerConfiguration.privateKeyRotationInterval
}
- operation.addObserver(
- BackgroundObserver(name: "Regenerate private key", cancelUponExpiration: true)
- )
-
- operation.addCondition(
- MutuallyExclusive(category: OperationCategory.changeTunnelSettings)
- )
-
- operationQueue.addOperation(operation)
- }
-
- func rotatePrivateKey(completionHandler: @escaping (OperationCompletion<KeyRotationResult, TunnelManager.Error>) -> Void) -> Cancellable {
let operation = RotateKeyOperation(
dispatchQueue: stateQueue,
state: state,
devicesProxy: devicesProxy,
- rotationInterval: TunnelManagerConfiguration.privateKeyRotationInterval
+ rotationInterval: rotationInterval
) { [weak self] completion in
guard let self = self else { return }
dispatchPrecondition(condition: .onQueue(self.stateQueue))
+ self.setFinishedKeyRotation(completion)
switch completion {
case .success:
- self.reconnectTunnel {
+ self.reconnectTunnel { _ in
completionHandler(completion)
}
@@ -547,15 +552,37 @@ final class TunnelManager: TunnelManagerStateDelegate {
// MARK: - TunnelManagerStateDelegate
- func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelSettings newTunnelSettinggs: TunnelSettingsV2?) {
+ func tunnelManagerState(
+ _ state: State,
+ didChangeLoadedConfiguration isLoadedConfiguration: Bool
+ )
+ {
+ DispatchQueue.main.async {
+ self.observerList.forEach { observer in
+ if isLoadedConfiguration {
+ observer.tunnelManagerDidLoadConfiguration(self)
+ }
+ }
+ }
+ }
+
+ func tunnelManagerState(
+ _ state: TunnelManager.State,
+ didChangeTunnelSettings newTunnelSettings: TunnelSettingsV2?
+ )
+ {
DispatchQueue.main.async {
self.observerList.forEach { observer in
- observer.tunnelManager(self, didUpdateTunnelSettings: newTunnelSettinggs)
+ observer.tunnelManager(self, didUpdateTunnelSettings: newTunnelSettings)
}
}
}
- func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelStatus newTunnelStatus: TunnelStatus) {
+ func tunnelManagerState(
+ _ state: TunnelManager.State,
+ didChangeTunnelStatus newTunnelStatus: TunnelStatus
+ )
+ {
logger.info("Status: \(newTunnelStatus).")
switch newTunnelStatus.state {
@@ -576,7 +603,12 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
}
- func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelProvider newTunnelObject: Tunnel?, shouldRefreshTunnelState: Bool) {
+ func tunnelManagerState(
+ _ state: TunnelManager.State,
+ didChangeTunnelProvider newTunnelObject: Tunnel?,
+ shouldRefreshTunnelState: Bool
+ )
+ {
dispatchPrecondition(condition: .onQueue(stateQueue))
// Register for tunnel connection status changes
@@ -589,7 +621,7 @@ final class TunnelManager: TunnelManagerStateDelegate {
// Update the existing state
if shouldRefreshTunnelState {
logger.debug("Refresh tunnel status for new tunnel.")
- refreshTunnelStatus()
+ _refreshTunnelStatus()
}
}
@@ -611,10 +643,10 @@ final class TunnelManager: TunnelManagerStateDelegate {
statusObserver = nil
}
- private func refreshTunnelStatus() {
+ private func _refreshTunnelStatus() {
dispatchPrecondition(condition: .onQueue(stateQueue))
- if let connectionStatus = self.state.tunnel?.status {
+ if let connectionStatus = state.tunnel?.status {
updateTunnelStatus(connectionStatus)
}
}
@@ -637,9 +669,7 @@ final class TunnelManager: TunnelManagerStateDelegate {
self.startTunnel()
}
- operation.addCondition(
- MutuallyExclusive(category: OperationCategory.tunnelStateUpdate)
- )
+ operation.addCondition(MutuallyExclusive(category: OperationCategory.tunnelStateUpdate))
// Cancel last VPN status mapping operation
lastMapConnectionStatusOperation?.cancel()
@@ -648,13 +678,6 @@ final class TunnelManager: TunnelManagerStateDelegate {
operationQueue.addOperation(operation)
}
- @objc private func applicationDidBecomeActive() {
- stateQueue.async {
- self.logger.debug("Refresh tunnel status due to application becoming active.")
- self.refreshTunnelStatus()
- }
- }
-
fileprivate func scheduleTunnelSettingsUpdate(taskName: String, modificationBlock: @escaping (inout TunnelSettingsV2) -> Void, completionHandler: @escaping (TunnelManager.Error?) -> Void) {
let operation = ResultBlockOperation<Void, TunnelManager.Error>(
dispatchQueue: stateQueue
@@ -742,7 +765,7 @@ final class TunnelManager: TunnelManagerStateDelegate {
guard let self = self else { return }
self.logger.debug("Refresh tunnel status (poll).")
- self.refreshTunnelStatus()
+ self._refreshTunnelStatus()
}
timer.schedule(
@@ -769,155 +792,7 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
-extension TunnelManager {
- /// Key rotation result.
- enum KeyRotationResult: CustomStringConvertible {
- /// Request to rotate the key was throttled.
- case throttled(_ lastKeyCreationDate: Date)
-
- /// New key was generated.
- case finished
-
- var description: String {
- switch self {
- case .throttled:
- return "throttled"
- case .finished:
- return "finished"
- }
- }
- }
-}
-
-// 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() throws {
- guard let tunnelSettings = state.tunnelSettings else {
- throw Error.unsetAccount
- }
-
- let creationDate = tunnelSettings.device.wgKeyData.creationDate
- let beginDate = Date(
- timeInterval: TunnelManagerConfiguration.privateKeyRotationInterval,
- since: creationDate
- )
-
- return try submitBackgroundTask(at: beginDate)
- }
-
- /// Create and submit task request to scheduler.
- private func submitBackgroundTask(at beginDate: Date) throws {
- let taskIdentifier = ApplicationConfiguration.privateKeyRotationTaskIdentifier
-
- let request = BGProcessingTaskRequest(identifier: taskIdentifier)
- request.earliestBeginDate = beginDate
- request.requiresNetworkConnectivity = true
-
- try BGTaskScheduler.shared.submit(request)
- }
-
- /// Background task handler.
- private func handleBackgroundTask(_ task: BGProcessingTask) {
- logger.debug("Start private key rotation task")
-
- let cancellableTask = rotatePrivateKey { completion in
- if let scheduleDate = self.handlePrivateKeyRotationCompletion(completion) {
- // Schedule next background task
- do {
- try self.submitBackgroundTask(at: scheduleDate)
-
- self.logger.debug(
- "Scheduled next private key rotation task at \(scheduleDate.logFormatDate())"
- )
- } catch {
- self.logger.error(
- chainedError: AnyChainedError(error),
- message: "Failed to schedule next private key rotation task."
- )
- }
- }
-
- // Complete current task
- task.setTaskCompleted(success: completion.isSuccess)
- }
-
- task.expirationHandler = {
- cancellableTask.cancel()
- }
- }
-}
-
-extension TunnelManager {
- fileprivate func handlePrivateKeyRotationCompletion(_ completion: OperationCompletion<KeyRotationResult, TunnelManager.Error>) -> Date? {
- switch completion {
- case .success(let result):
- switch result {
- case .finished:
- logger.debug("Finished private key rotation.")
- case .throttled:
- logger.debug("Private key was already rotated earlier.")
- }
-
- return nextScheduleDate(result)
-
- case .failure(let error):
- logger.error(chainedError: error, message: "Failed to rotate private key.")
-
- return nextRetryScheduleDate(error)
-
- case .cancelled:
- logger.debug("Private key rotation was cancelled.")
-
- return Date(timeIntervalSinceNow: TunnelManagerConfiguration.privateKeyRotationFailureRetryInterval)
- }
- }
-
- fileprivate func nextScheduleDate(_ result: KeyRotationResult) -> Date {
- switch result {
- case .finished:
- return Date(timeIntervalSinceNow: TunnelManagerConfiguration.privateKeyRotationInterval)
-
- case .throttled(let lastKeyCreationDate):
- return Date(timeInterval: TunnelManagerConfiguration.privateKeyRotationInterval, since: lastKeyCreationDate)
- }
- }
-
- fileprivate func nextRetryScheduleDate(_ error: TunnelManager.Error) -> Date? {
- switch error {
- case .unsetAccount:
- // Do not retry if logged out.
- return nil
-
- case .rotateKey(.unhandledResponse(_, let serverErrorResponse))
- where serverErrorResponse?.code == .invalidAccount ||
- serverErrorResponse?.code == .deviceNotFound:
- // Do not retry if account or device were removed.
- return nil
-
- default:
- return Date(timeIntervalSinceNow: TunnelManagerConfiguration.privateKeyRotationFailureRetryInterval)
- }
- }
-}
+// MARK: - AppStore payment observer
extension TunnelManager: AppStorePaymentObserver {
func appStorePaymentManager(_ manager: AppStorePaymentManager,
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift b/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift
index fb1fd03c7f..48ea2f97b1 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift
@@ -14,6 +14,9 @@ extension TunnelManager {
/// Account is unset.
case unsetAccount
+ /// Tunnel is not set yet.
+ case unsetTunnel
+
/// Failure to start the VPN tunnel via system call.
case startVPNTunnel(Swift.Error)
@@ -39,7 +42,7 @@ extension TunnelManager {
case deleteSettings(Swift.Error)
/// Failure to read relays cache.
- case readRelays(RelayCache.Error)
+ case readRelays
/// Failure to find a relay satisfying the given constraints.
case cannotSatisfyRelayConstraints
@@ -72,6 +75,8 @@ extension TunnelManager {
switch self {
case .unsetAccount:
return "Account is unset."
+ case .unsetTunnel:
+ return "Tunnel is unset."
case .startVPNTunnel:
return "Failed to start the VPN tunnel."
case .loadAllVPNConfigurations:
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift b/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift
index 990e8e482f..002e3e3862 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift
@@ -10,19 +10,37 @@ import Foundation
import NetworkExtension
protocol TunnelManagerStateDelegate: AnyObject {
- func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelSettings newTunnelSettings: TunnelSettingsV2?)
- func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelStatus newTunnelStatus: TunnelStatus)
- func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelProvider newTunnelObject: Tunnel?, shouldRefreshTunnelState: Bool)
+
+ func tunnelManagerState(
+ _ state: TunnelManager.State,
+ didChangeLoadedConfiguration isLoadedConfiguration: Bool
+ )
+
+ func tunnelManagerState(
+ _ state: TunnelManager.State,
+ didChangeTunnelSettings newTunnelSettings: TunnelSettingsV2?
+ )
+
+ func tunnelManagerState(
+ _ state: TunnelManager.State,
+ didChangeTunnelStatus newTunnelStatus: TunnelStatus
+ )
+
+ func tunnelManagerState(
+ _ state: TunnelManager.State,
+ didChangeTunnelProvider newTunnelObject: Tunnel?,
+ shouldRefreshTunnelState: Bool
+ )
}
extension TunnelManager {
class State {
- let queue: DispatchQueue
weak var delegate: TunnelManagerStateDelegate?
+ let delegateQueue: DispatchQueue
- private let queueMarkerKey = DispatchSpecificKey<Bool>()
-
+ private let nslock = NSLock()
+ private var _isLoadedConfiguration = false
private var _tunnelSettings: TunnelSettingsV2?
private var _tunnelObject: Tunnel?
private var _tunnelStatus = TunnelStatus(
@@ -31,73 +49,97 @@ extension TunnelManager {
state: .disconnected
)
- var tunnelSettings: TunnelSettingsV2? {
+ var isLoadedConfiguration: Bool {
get {
- return performBlock {
- return _tunnelSettings
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ return _isLoadedConfiguration
+ }
+ set {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ guard _isLoadedConfiguration != newValue else { return }
+
+ _isLoadedConfiguration = newValue
+
+ delegateQueue.async {
+ self.delegate?.tunnelManagerState(
+ self,
+ didChangeLoadedConfiguration: newValue
+ )
}
}
+ }
+
+ var tunnelSettings: TunnelSettingsV2? {
+ get {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ return _tunnelSettings
+ }
set {
- performBlock {
- if _tunnelSettings != newValue {
- _tunnelSettings = newValue
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ guard _tunnelSettings != newValue else { return }
+
+ _tunnelSettings = newValue
- delegate?.tunnelManagerState(self, didChangeTunnelSettings: newValue)
- }
+ delegateQueue.async {
+ self.delegate?.tunnelManagerState(self, didChangeTunnelSettings: newValue)
}
}
}
var tunnel: Tunnel? {
- return performBlock {
- return _tunnelObject
- }
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ return _tunnelObject
}
var tunnelStatus: TunnelStatus {
get {
- return performBlock {
- return _tunnelStatus
- }
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ return _tunnelStatus
}
set {
- performBlock {
- if _tunnelStatus != newValue {
- _tunnelStatus = newValue
+ nslock.lock()
+ defer { nslock.unlock() }
- delegate?.tunnelManagerState(self, didChangeTunnelStatus: newValue)
- }
- }
- }
- }
+ guard _tunnelStatus != newValue else { return }
- init(queue: DispatchQueue) {
- self.queue = queue
+ _tunnelStatus = newValue
- queue.setSpecific(key: queueMarkerKey, value: true)
+ delegateQueue.async {
+ self.delegate?.tunnelManagerState(self, didChangeTunnelStatus: newValue)
+ }
+ }
}
- deinit {
- queue.setSpecific(key: queueMarkerKey, value: nil)
+ init(delegateQueue: DispatchQueue) {
+ self.delegateQueue = delegateQueue
}
func setTunnel(_ newTunnelObject: Tunnel?, shouldRefreshTunnelState: Bool) {
- performBlock {
- if _tunnelObject != newTunnelObject {
- _tunnelObject = newTunnelObject
+ nslock.lock()
+ defer { nslock.unlock() }
- delegate?.tunnelManagerState(self, didChangeTunnelProvider: newTunnelObject, shouldRefreshTunnelState: shouldRefreshTunnelState)
- }
- }
- }
+ guard _tunnelObject != newTunnelObject else { return }
- private func performBlock<T>(_ block: () -> T) -> T {
- let isTargetQueue = DispatchQueue.getSpecific(key: queueMarkerKey) ?? false
+ _tunnelObject = newTunnelObject
- if isTargetQueue {
- return block()
- } else {
- return queue.sync(execute: block)
+ delegateQueue.async {
+ self.delegate?.tunnelManagerState(
+ self,
+ didChangeTunnelProvider: newTunnelObject,
+ shouldRefreshTunnelState: shouldRefreshTunnelState
+ )
}
}
}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelObserver.swift b/ios/MullvadVPN/TunnelManager/TunnelObserver.swift
index 6e1934e30e..93ab9d8849 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelObserver.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelObserver.swift
@@ -9,6 +9,7 @@
import Foundation
protocol TunnelObserver: AnyObject {
+ func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager)
func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState)
func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?)
func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error)
diff --git a/ios/MullvadVPN/WireguardKeysViewController.swift b/ios/MullvadVPN/WireguardKeysViewController.swift
index 655d665b8f..e772290cd2 100644
--- a/ios/MullvadVPN/WireguardKeysViewController.swift
+++ b/ios/MullvadVPN/WireguardKeysViewController.swift
@@ -104,6 +104,10 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
// MARK: - TunnelObserver
+ func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) {
+ // no-op
+ }
+
func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) {
// no-op
}
@@ -252,8 +256,8 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
private func regeneratePrivateKey() {
self.updateViewState(.regeneratingKey)
- TunnelManager.shared.regeneratePrivateKey { [weak self] error in
- if let error = error {
+ _ = TunnelManager.shared.rotatePrivateKey(forceRotate: true) { [weak self] completion in
+ if let error = completion.error {
self?.showKeyRegenerationFailureAlert(error)
self?.updateViewState(.regeneratedKey(false))
} else {