summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2023-10-30 15:03:38 +0100
committerBug Magnet <marco.nikic@mullvad.net>2023-10-30 15:03:38 +0100
commit87ce1ee93ec7b58f350b87a3fd0c61c91c45165e (patch)
treea267c3ec3ce80e8663c1fa4c4c190d3b2a50c1bf
parenta182908fc283312d8244ad982ffcc3da6fe87f15 (diff)
parent199000054df33c3fcd73ce2197579e764fb6a3a9 (diff)
downloadmullvadvpn-87ce1ee93ec7b58f350b87a3fd0c61c91c45165e.tar.xz
mullvadvpn-87ce1ee93ec7b58f350b87a3fd0c61c91c45165e.zip
Merge branch 'create-wireguard-obfuscation-settings-ios-356'
-rw-r--r--ios/MullvadREST/RESTAuthorization.swift4
-rw-r--r--ios/MullvadREST/RESTProxy.swift4
-rw-r--r--ios/MullvadSettings/MigrationManager.swift84
-rw-r--r--ios/MullvadSettings/SettingsManager.swift17
-rw-r--r--ios/MullvadSettings/StoredWgKeyData.swift37
-rw-r--r--ios/MullvadSettings/TunnelSettings.swift29
-rw-r--r--ios/MullvadSettings/TunnelSettingsV1.swift7
-rw-r--r--ios/MullvadSettings/TunnelSettingsV2.swift38
-rw-r--r--ios/MullvadSettings/TunnelSettingsV3.swift36
-rw-r--r--ios/MullvadSettings/WireGuardObfuscationSettings.swift31
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj32
-rw-r--r--ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift2
-rw-r--r--ios/MullvadVPN/SceneDelegate.swift14
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift2
-rw-r--r--ios/MullvadVPNTests/InMemorySettingsStore.swift31
-rw-r--r--ios/MullvadVPNTests/MigrationManagerTests.swift189
16 files changed, 460 insertions, 97 deletions
diff --git a/ios/MullvadREST/RESTAuthorization.swift b/ios/MullvadREST/RESTAuthorization.swift
index 7e81b1f8cb..340bd63f5c 100644
--- a/ios/MullvadREST/RESTAuthorization.swift
+++ b/ios/MullvadREST/RESTAuthorization.swift
@@ -19,10 +19,10 @@ extension REST {
typealias Authorization = String
struct AccessTokenProvider: RESTAuthorizationProvider {
- private let accessTokenManager: AccessTokenManager
+ private let accessTokenManager: RESTAccessTokenManagement
private let accountNumber: String
- init(accessTokenManager: AccessTokenManager, accountNumber: String) {
+ init(accessTokenManager: RESTAccessTokenManagement, accountNumber: String) {
self.accessTokenManager = accessTokenManager
self.accountNumber = accountNumber
}
diff --git a/ios/MullvadREST/RESTProxy.swift b/ios/MullvadREST/RESTProxy.swift
index 6ca64d85ee..3ca5c9630e 100644
--- a/ios/MullvadREST/RESTProxy.swift
+++ b/ios/MullvadREST/RESTProxy.swift
@@ -160,11 +160,11 @@ extension REST {
}
public class AuthProxyConfiguration: ProxyConfiguration {
- public let accessTokenManager: AccessTokenManager
+ public let accessTokenManager: RESTAccessTokenManagement
public init(
proxyConfiguration: ProxyConfiguration,
- accessTokenManager: AccessTokenManager
+ accessTokenManager: RESTAccessTokenManagement
) {
self.accessTokenManager = accessTokenManager
diff --git a/ios/MullvadSettings/MigrationManager.swift b/ios/MullvadSettings/MigrationManager.swift
index 6348cf9471..8667885823 100644
--- a/ios/MullvadSettings/MigrationManager.swift
+++ b/ios/MullvadSettings/MigrationManager.swift
@@ -29,14 +29,18 @@ public struct MigrationManager {
/// Migrate settings store if needed.
///
- /// The following types of error are expected to be returned by this method:
- /// `SettingsMigrationError`, `UnsupportedSettingsVersionError`, `ReadSettingsVersionError`.
+ /// Reads the current settings, upgrades them to the latest version if needed
+ /// and writes back to `store` when settings are updated.
+ /// - Parameters:
+ /// - store: The store to from which settings are read and written to.
+ /// - proxyFactory: Factory used for migrations that involve API calls.
+ /// - migrationCompleted: Completion handler called with a migration result.
public func migrateSettings(
store: SettingsStore,
proxyFactory: REST.ProxyFactory,
migrationCompleted: @escaping (SettingsMigrationResult) -> Void
) {
- let handleCompletion = { (result: SettingsMigrationResult) in
+ let resetStoreHandler = { (result: SettingsMigrationResult) in
// Reset store upon failure to migrate settings.
if case .failure = result {
SettingsManager.resetStore()
@@ -45,56 +49,52 @@ public struct MigrationManager {
}
do {
- try checkLatestSettingsVersion(in: store)
- handleCompletion(.nothing)
+ try upgradeSettingsToLatestVersion(
+ store: store,
+ proxyFactory: proxyFactory,
+ migrationCompleted: migrationCompleted
+ )
+ } catch .itemNotFound as KeychainError {
+ migrationCompleted(.nothing)
} catch {
- handleCompletion(.failure(error))
+ resetStoreHandler(.failure(error))
}
}
- private func checkLatestSettingsVersion(in store: SettingsStore) throws {
- let settingsVersion: Int
- do {
- let parser = SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder())
- let settingsData = try store.read(key: SettingsKey.settings)
- settingsVersion = try parser.parseVersion(data: settingsData)
- } catch .itemNotFound as KeychainError {
- return
- } catch {
- throw ReadSettingsVersionError(underlyingError: error)
- }
+ private func upgradeSettingsToLatestVersion(
+ store: SettingsStore,
+ proxyFactory: REST.ProxyFactory,
+ migrationCompleted: @escaping (SettingsMigrationResult) -> Void
+ ) throws {
+ let parser = SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder())
+ let settingsData = try store.read(key: SettingsKey.settings)
+ let settingsVersion = try parser.parseVersion(data: settingsData)
guard settingsVersion != SchemaVersion.current.rawValue else {
+ migrationCompleted(.nothing)
return
}
- let error = UnsupportedSettingsVersionError(
- storedVersion: settingsVersion,
- currentVersion: SchemaVersion.current
- )
-
- logger.error(error: error, message: "Encountered an unknown version.")
-
- throw error
- }
-}
-
-/// A wrapper type for errors returned by concrete migrations.
-public struct SettingsMigrationError: LocalizedError, WrappingError {
- private let inner: Error
- public let sourceVersion, targetVersion: SchemaVersion
+ // Corrupted settings version (i.e. negative values, or downgrade from a future version) should fail
+ guard var savedSchema = SchemaVersion(rawValue: settingsVersion) else {
+ migrationCompleted(.failure(UnsupportedSettingsVersionError(
+ storedVersion: settingsVersion,
+ currentVersion: SchemaVersion.current
+ )))
+ return
+ }
- public var underlyingError: Error? {
- inner
- }
+ var savedSettings = try parser.parsePayload(as: savedSchema.settingsType, from: settingsData)
- public var errorDescription: String? {
- "Failed to migrate settings from \(sourceVersion) to \(targetVersion)."
- }
+ repeat {
+ let upgradedVersion = savedSettings.upgradeToNextVersion()
+ savedSchema = savedSchema.nextVersion
+ savedSettings = upgradedVersion
+ } while savedSchema.rawValue < SchemaVersion.current.rawValue
- public init(sourceVersion: SchemaVersion, targetVersion: SchemaVersion, underlyingError: Error) {
- self.sourceVersion = sourceVersion
- self.targetVersion = targetVersion
- inner = underlyingError
+ // Write the latest settings back to the store
+ let latestVersionPayload = try parser.producePayload(savedSettings, version: SchemaVersion.current.rawValue)
+ try store.write(latestVersionPayload, for: .settings)
+ migrationCompleted(.success)
}
}
diff --git a/ios/MullvadSettings/SettingsManager.swift b/ios/MullvadSettings/SettingsManager.swift
index 953c052beb..8de7cc1a9d 100644
--- a/ios/MullvadSettings/SettingsManager.swift
+++ b/ios/MullvadSettings/SettingsManager.swift
@@ -18,11 +18,28 @@ private let accountExpiryKey = "accountExpiry"
public enum SettingsManager {
private static let logger = Logger(label: "SettingsManager")
+ #if DEBUG
+ private static var _store = KeychainSettingsStore(
+ serviceName: keychainServiceName,
+ accessGroup: ApplicationConfiguration.securityGroupIdentifier
+ )
+
+ /// Alternative store used for tests.
+ internal static var unitTestStore: SettingsStore?
+
+ public static var store: SettingsStore {
+ if let unitTestStore { return unitTestStore }
+ return _store
+ }
+
+ #else
public static let store: SettingsStore = KeychainSettingsStore(
serviceName: keychainServiceName,
accessGroup: ApplicationConfiguration.securityGroupIdentifier
)
+ #endif
+
private static func makeParser() -> SettingsParser {
SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder())
}
diff --git a/ios/MullvadSettings/StoredWgKeyData.swift b/ios/MullvadSettings/StoredWgKeyData.swift
new file mode 100644
index 0000000000..df07e0e8c9
--- /dev/null
+++ b/ios/MullvadSettings/StoredWgKeyData.swift
@@ -0,0 +1,37 @@
+//
+// StoredWgKeyData.swift
+// MullvadSettings
+//
+// Created by Marco Nikic on 2023-10-23.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import WireGuardKitTypes
+
+public struct StoredWgKeyData: Codable, Equatable {
+ /// Private key creation date.
+ public var creationDate: Date
+
+ /// Last date a rotation was attempted. Nil if last attempt was successful.
+ public var lastRotationAttemptDate: Date?
+
+ /// Private key.
+ public var privateKey: PrivateKey
+
+ /// Next private key we're trying to rotate to.
+ /// Added in 2023.3
+ public var nextPrivateKey: PrivateKey?
+
+ public init(
+ creationDate: Date,
+ lastRotationAttemptDate: Date? = nil,
+ privateKey: PrivateKey,
+ nextPrivateKey: PrivateKey? = nil
+ ) {
+ self.creationDate = creationDate
+ self.lastRotationAttemptDate = lastRotationAttemptDate
+ self.privateKey = privateKey
+ self.nextPrivateKey = nextPrivateKey
+ }
+}
diff --git a/ios/MullvadSettings/TunnelSettings.swift b/ios/MullvadSettings/TunnelSettings.swift
index f58c88c47c..9d27b0a375 100644
--- a/ios/MullvadSettings/TunnelSettings.swift
+++ b/ios/MullvadSettings/TunnelSettings.swift
@@ -7,9 +7,15 @@
//
import Foundation
+import MullvadREST
/// Alias to the latest version of the `TunnelSettings`.
-public typealias LatestTunnelSettings = TunnelSettingsV2
+public typealias LatestTunnelSettings = TunnelSettingsV3
+
+/// Protocol all TunnelSettings must adhere to, for upgrade purposes.
+public protocol TunnelSettings: Codable {
+ func upgradeToNextVersion() -> any TunnelSettings
+}
/// Settings and device state schema versions.
public enum SchemaVersion: Int, Equatable {
@@ -19,6 +25,25 @@ public enum SchemaVersion: Int, Equatable {
/// New settings format, stored as `TunnelSettingsV2`.
case v2 = 2
+ /// V2 format with WireGuard obfuscation options, stored as `TunnelSettingsV3`.
+ case v3 = 3
+
+ var settingsType: any TunnelSettings.Type {
+ switch self {
+ case .v1: return TunnelSettingsV1.self
+ case .v2: return TunnelSettingsV2.self
+ case .v3: return TunnelSettingsV3.self
+ }
+ }
+
+ var nextVersion: Self {
+ switch self {
+ case .v1: return .v2
+ case .v2: return .v3
+ case .v3: return .v3
+ }
+ }
+
/// Current schema version.
- public static let current = SchemaVersion.v2
+ public static let current = SchemaVersion.v3
}
diff --git a/ios/MullvadSettings/TunnelSettingsV1.swift b/ios/MullvadSettings/TunnelSettingsV1.swift
index 5148266e21..1d7301a910 100644
--- a/ios/MullvadSettings/TunnelSettingsV1.swift
+++ b/ios/MullvadSettings/TunnelSettingsV1.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import MullvadREST
import MullvadTypes
import struct Network.IPv4Address
import struct WireGuardKitTypes.IPAddressRange
@@ -14,9 +15,13 @@ import class WireGuardKitTypes.PrivateKey
import class WireGuardKitTypes.PublicKey
/// A struct that holds the configuration passed via `NETunnelProviderProtocol`.
-public struct TunnelSettingsV1: Codable, Equatable {
+public struct TunnelSettingsV1: Codable, Equatable, TunnelSettings {
public var relayConstraints = RelayConstraints()
public var interface = InterfaceSettings()
+
+ public func upgradeToNextVersion() -> any TunnelSettings {
+ TunnelSettingsV2(relayConstraints: relayConstraints, dnsSettings: interface.dnsSettings)
+ }
}
/// A struct that holds a tun interface configuration.
diff --git a/ios/MullvadSettings/TunnelSettingsV2.swift b/ios/MullvadSettings/TunnelSettingsV2.swift
index 001cc3adb6..c5d39ca0e6 100644
--- a/ios/MullvadSettings/TunnelSettingsV2.swift
+++ b/ios/MullvadSettings/TunnelSettingsV2.swift
@@ -7,13 +7,10 @@
//
import Foundation
+import MullvadREST
import MullvadTypes
-import struct Network.IPv4Address
-import struct WireGuardKitTypes.IPAddressRange
-import class WireGuardKitTypes.PrivateKey
-import class WireGuardKitTypes.PublicKey
-public struct TunnelSettingsV2: Codable, Equatable {
+public struct TunnelSettingsV2: Codable, Equatable, TunnelSettings {
/// Relay constraints.
public var relayConstraints: RelayConstraints
@@ -27,31 +24,12 @@ public struct TunnelSettingsV2: Codable, Equatable {
self.relayConstraints = relayConstraints
self.dnsSettings = dnsSettings
}
-}
-
-public struct StoredWgKeyData: Codable, Equatable {
- /// Private key creation date.
- public var creationDate: Date
-
- /// Last date a rotation was attempted. Nil if last attempt was successful.
- public var lastRotationAttemptDate: Date?
-
- /// Private key.
- public var privateKey: PrivateKey
- /// Next private key we're trying to rotate to.
- /// Added in 2023.3
- public var nextPrivateKey: PrivateKey?
-
- public init(
- creationDate: Date,
- lastRotationAttemptDate: Date? = nil,
- privateKey: PrivateKey,
- nextPrivateKey: PrivateKey? = nil
- ) {
- self.creationDate = creationDate
- self.lastRotationAttemptDate = lastRotationAttemptDate
- self.privateKey = privateKey
- self.nextPrivateKey = nextPrivateKey
+ public func upgradeToNextVersion() -> any TunnelSettings {
+ TunnelSettingsV3(
+ relayConstraints: relayConstraints,
+ dnsSettings: dnsSettings,
+ wireGuardObfuscation: WireGuardObfuscationSettings()
+ )
}
}
diff --git a/ios/MullvadSettings/TunnelSettingsV3.swift b/ios/MullvadSettings/TunnelSettingsV3.swift
new file mode 100644
index 0000000000..98d9f4bc71
--- /dev/null
+++ b/ios/MullvadSettings/TunnelSettingsV3.swift
@@ -0,0 +1,36 @@
+//
+// TunnelSettingsV3.swift
+// MullvadVPN
+//
+// Created by Marco Nikic on 2023-10-17.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadREST
+import MullvadTypes
+
+public struct TunnelSettingsV3: Codable, Equatable, TunnelSettings {
+ /// Relay constraints.
+ public var relayConstraints: RelayConstraints
+
+ /// DNS settings.
+ public var dnsSettings: DNSSettings
+
+ /// WireGuard obfuscation settings
+ public var wireGuardObfuscation: WireGuardObfuscationSettings
+
+ public init(
+ relayConstraints: RelayConstraints = RelayConstraints(),
+ dnsSettings: DNSSettings = DNSSettings(),
+ wireGuardObfuscation: WireGuardObfuscationSettings = WireGuardObfuscationSettings()
+ ) {
+ self.relayConstraints = relayConstraints
+ self.dnsSettings = dnsSettings
+ self.wireGuardObfuscation = wireGuardObfuscation
+ }
+
+ public func upgradeToNextVersion() -> any TunnelSettings {
+ self
+ }
+}
diff --git a/ios/MullvadSettings/WireGuardObfuscationSettings.swift b/ios/MullvadSettings/WireGuardObfuscationSettings.swift
new file mode 100644
index 0000000000..1d5404b94b
--- /dev/null
+++ b/ios/MullvadSettings/WireGuardObfuscationSettings.swift
@@ -0,0 +1,31 @@
+//
+// WireGuardObfuscationSettings.swift
+// MullvadVPN
+//
+// Created by Marco Nikic on 2023-10-17.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+public enum WireGuardObfuscationState: Codable {
+ case automatic
+ case on
+ case off
+}
+
+public enum WireGuardObfuscationPort: Codable {
+ case automatic
+ case port80
+ case port5001
+}
+
+public struct WireGuardObfuscationSettings: Codable, Equatable {
+ let state: WireGuardObfuscationState
+ let port: WireGuardObfuscationPort
+
+ public init(state: WireGuardObfuscationState = .automatic, port: WireGuardObfuscationPort = .automatic) {
+ self.state = state
+ self.port = port
+ }
+}
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 34a2bb66ad..61a08e84e5 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -503,12 +503,16 @@
A94D691B2ABAD66700413DD4 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58FE25E72AA7399D003D1918 /* WireGuardKitTypes */; };
A95F86B72A1F53BA00245DAC /* URLSessionTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67C28F83CA50033DD93 /* URLSessionTransport.swift */; };
A95F86B82A1F547000245DAC /* ShadowsocksProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F1FF1B29F06124007083C3 /* ShadowsocksProxy.swift */; };
+ A97D30172AE6B5E90045C0E4 /* StoredWgKeyData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97D30162AE6B5E90045C0E4 /* StoredWgKeyData.swift */; };
A97F1F442A1F4E1A00ECEFDE /* MullvadTransport.h in Headers */ = {isa = PBXBuildFile; fileRef = A97F1F432A1F4E1A00ECEFDE /* MullvadTransport.h */; settings = {ATTRIBUTES = (Public, ); }; };
A97F1F472A1F4E1A00ECEFDE /* MullvadTransport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A97F1F412A1F4E1A00ECEFDE /* MullvadTransport.framework */; };
A97F1F482A1F4E1A00ECEFDE /* MullvadTransport.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A97F1F412A1F4E1A00ECEFDE /* MullvadTransport.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
A97FF5502A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */; };
A988DF212ADD293D00D807EF /* RESTTransportStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A1DE782AD5708E0073F689 /* RESTTransportStrategy.swift */; };
A988DF242ADD307200D807EF /* libRelaySelector.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5898D29829017DAC00EB5EBA /* libRelaySelector.a */; };
+ A988DF262ADE86ED00D807EF /* WireGuardObfuscationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988DF252ADE86ED00D807EF /* WireGuardObfuscationSettings.swift */; };
+ A988DF272ADE86ED00D807EF /* WireGuardObfuscationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988DF252ADE86ED00D807EF /* WireGuardObfuscationSettings.swift */; };
+ A988DF2A2ADE880300D807EF /* TunnelSettingsV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = A988DF282ADE880300D807EF /* TunnelSettingsV3.swift */; };
A9A1DE792AD5708E0073F689 /* RESTTransportStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A1DE782AD5708E0073F689 /* RESTTransportStrategy.swift */; };
A9A5F9E12ACB05160083449F /* AddressCacheTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06AC114028F841390037AF9A /* AddressCacheTracker.swift */; };
A9A5F9E22ACB05160083449F /* BackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C76A0A2A338E4300100D75 /* BackgroundTask.swift */; };
@@ -611,6 +615,9 @@
A9A5FA432ACB05F20083449F /* UIColor+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */; };
A9A8A8EB2A262AB30086D569 /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A8A8EA2A262AB30086D569 /* FileCache.swift */; };
A9B2CF722A1F64CD0013CC6C /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; };
+ A9B6AC182ADE8F4300F7802A /* MigrationManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B6AC172ADE8F4300F7802A /* MigrationManagerTests.swift */; };
+ A9B6AC1A2ADE8FBB00F7802A /* InMemorySettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B6AC192ADE8FBB00F7802A /* InMemorySettingsStore.swift */; };
+ A9B6AC1B2ADEA3AD00F7802A /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB9C2A98F69E00F578F2 /* MemoryCache.swift */; };
A9C342C12ACC37E30045F00E /* TunnelStatusBlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E0317D2ACC32920095D843 /* TunnelStatusBlockObserver.swift */; };
A9C342C32ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C342C22ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift */; };
A9C342C52ACC42130045F00E /* ServerRelaysResponse+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C342C42ACC42130045F00E /* ServerRelaysResponse+Stubs.swift */; };
@@ -1585,12 +1592,17 @@
A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheTests.swift; sourceTree = "<group>"; };
A9467E872A2DCD57000DC21F /* ShadowsocksConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksConfiguration.swift; sourceTree = "<group>"; };
A9467E8A2A2E0317000DC21F /* ShadowsocksConfigurationCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksConfigurationCache.swift; sourceTree = "<group>"; };
+ A97D30162AE6B5E90045C0E4 /* StoredWgKeyData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredWgKeyData.swift; sourceTree = "<group>"; };
A97F1F412A1F4E1A00ECEFDE /* MullvadTransport.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MullvadTransport.framework; sourceTree = BUILT_PRODUCTS_DIR; };
A97F1F432A1F4E1A00ECEFDE /* MullvadTransport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MullvadTransport.h; sourceTree = "<group>"; };
A97FF54F2A0D2FFC00900996 /* NSFileCoordinator+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSFileCoordinator+Extensions.swift"; sourceTree = "<group>"; };
+ A988DF252ADE86ED00D807EF /* WireGuardObfuscationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardObfuscationSettings.swift; sourceTree = "<group>"; };
+ A988DF282ADE880300D807EF /* TunnelSettingsV3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV3.swift; sourceTree = "<group>"; };
A9A1DE782AD5708E0073F689 /* RESTTransportStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTTransportStrategy.swift; sourceTree = "<group>"; };
A9A5F9A12ACB003D0083449F /* TunnelManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerTests.swift; sourceTree = "<group>"; };
A9A8A8EA2A262AB30086D569 /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = "<group>"; };
+ A9B6AC172ADE8F4300F7802A /* MigrationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationManagerTests.swift; sourceTree = "<group>"; };
+ A9B6AC192ADE8FBB00F7802A /* InMemorySettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemorySettingsStore.swift; sourceTree = "<group>"; };
A9C342C22ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RelayCacheTracker+Stubs.swift"; sourceTree = "<group>"; };
A9C342C42ACC42130045F00E /* ServerRelaysResponse+Stubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ServerRelaysResponse+Stubs.swift"; sourceTree = "<group>"; };
A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCacheTests.swift; sourceTree = "<group>"; };
@@ -2471,7 +2483,9 @@
58C3FA672A385C89006A450A /* FileCacheTests.swift */,
582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */,
58B0A2A4238EE67E00BC001D /* Info.plist */,
+ A9B6AC192ADE8FBB00F7802A /* InMemorySettingsStore.swift */,
F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */,
+ A9B6AC172ADE8F4300F7802A /* MigrationManagerTests.swift */,
58C3FA652A38549D006A450A /* MockFileCache.swift */,
A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */,
A9C342C22ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift */,
@@ -2518,20 +2532,23 @@
58B2FDD42AA71D2A003EB5C6 /* MullvadSettings */ = {
isa = PBXGroup;
children = (
- 58B2FDD52AA71D2A003EB5C6 /* MullvadSettings.h */,
+ A92ECC2B2A7803A50052F1B1 /* DeviceState.swift */,
580F8B8528197958002E0998 /* DNSSettings.swift */,
06410DFD292CE18F00AFC18C /* KeychainSettingsStore.swift */,
068CE5732927B7A400A068BB /* Migration.swift */,
+ A9D96B192A8247C100A5C673 /* MigrationManager.swift */,
+ 58B2FDD52AA71D2A003EB5C6 /* MullvadSettings.h */,
58FF2C02281BDE02009EF542 /* SettingsManager.swift */,
06410E03292D0F7100AFC18C /* SettingsParser.swift */,
06410E06292D108E00AFC18C /* SettingsStore.swift */,
A92ECC232A7802520052F1B1 /* StoredAccountData.swift */,
A92ECC272A7802AB0052F1B1 /* StoredDeviceData.swift */,
+ A97D30162AE6B5E90045C0E4 /* StoredWgKeyData.swift */,
+ A92ECC202A77FFAF0052F1B1 /* TunnelSettings.swift */,
587AD7C523421D7000E93A53 /* TunnelSettingsV1.swift */,
580F8B8228197881002E0998 /* TunnelSettingsV2.swift */,
- A92ECC202A77FFAF0052F1B1 /* TunnelSettings.swift */,
- A92ECC2B2A7803A50052F1B1 /* DeviceState.swift */,
- A9D96B192A8247C100A5C673 /* MigrationManager.swift */,
+ A988DF282ADE880300D807EF /* TunnelSettingsV3.swift */,
+ A988DF252ADE86ED00D807EF /* WireGuardObfuscationSettings.swift */,
);
path = MullvadSettings;
sourceTree = "<group>";
@@ -4069,6 +4086,7 @@
A9A5F9E72ACB05160083449F /* FirstTimeLaunch.swift in Sources */,
A9A5F9E82ACB05160083449F /* MarkdownStylingOptions.swift in Sources */,
A9A5F9E92ACB05160083449F /* ObserverList.swift in Sources */,
+ A9B6AC1B2ADEA3AD00F7802A /* MemoryCache.swift in Sources */,
A9A5F9EA2ACB05160083449F /* Bundle+ProductVersion.swift in Sources */,
A9A5F9EB2ACB05160083449F /* CharacterSet+IPAddress.swift in Sources */,
A9A5F9EC2ACB05160083449F /* CodingErrors+CustomErrorDescription.swift in Sources */,
@@ -4115,6 +4133,7 @@
A9A5FA102ACB05160083449F /* PacketTunnelTransport.swift in Sources */,
A9A5FA112ACB05160083449F /* TransportMonitor.swift in Sources */,
A9A5FA122ACB05160083449F /* DeleteAccountOperation.swift in Sources */,
+ A9B6AC1A2ADE8FBB00F7802A /* InMemorySettingsStore.swift in Sources */,
A9A5FA132ACB05160083449F /* LoadTunnelConfigurationOperation.swift in Sources */,
A9A5FA142ACB05160083449F /* MapConnectionStatusOperation.swift in Sources */,
A9A5FA152ACB05160083449F /* RedeemVoucherOperation.swift in Sources */,
@@ -4140,6 +4159,7 @@
A9A5FA272ACB05160083449F /* VPNConnectionProtocol.swift in Sources */,
A9A5FA282ACB05160083449F /* WgKeyRotation.swift in Sources */,
A9A5FA292ACB05160083449F /* AddressCacheTests.swift in Sources */,
+ A9B6AC182ADE8F4300F7802A /* MigrationManagerTests.swift in Sources */,
A9A5FA2A2ACB05160083449F /* CoordinatesTests.swift in Sources */,
A9A5FA2B2ACB05160083449F /* CustomDateComponentsFormattingTests.swift in Sources */,
A9A5FA2C2ACB05160083449F /* DeviceCheckOperationTests.swift in Sources */,
@@ -4162,13 +4182,16 @@
files = (
58B2FDEE2AA72098003EB5C6 /* ApplicationConfiguration.swift in Sources */,
58B2FDE52AA71D5C003EB5C6 /* TunnelSettingsV2.swift in Sources */,
+ A97D30172AE6B5E90045C0E4 /* StoredWgKeyData.swift in Sources */,
58B2FDE32AA71D5C003EB5C6 /* StoredDeviceData.swift in Sources */,
58B2FDDF2AA71D5C003EB5C6 /* DNSSettings.swift in Sources */,
58B2FDE02AA71D5C003EB5C6 /* TunnelSettings.swift in Sources */,
+ A988DF2A2ADE880300D807EF /* TunnelSettingsV3.swift in Sources */,
58B2FDE42AA71D5C003EB5C6 /* SettingsManager.swift in Sources */,
58B2FDE62AA71D5C003EB5C6 /* DeviceState.swift in Sources */,
58FE25BF2AA72311003D1918 /* MigrationManager.swift in Sources */,
58B2FDEF2AA720C4003EB5C6 /* ApplicationTarget.swift in Sources */,
+ A988DF272ADE86ED00D807EF /* WireGuardObfuscationSettings.swift in Sources */,
58B2FDDE2AA71D5C003EB5C6 /* Migration.swift in Sources */,
58B2FDE12AA71D5C003EB5C6 /* TunnelSettingsV1.swift in Sources */,
58B2FDE72AA71D5C003EB5C6 /* SettingsStore.swift in Sources */,
@@ -4273,6 +4296,7 @@
5896CEF226972DEB00B0FAE8 /* AccountContentView.swift in Sources */,
7A3353932AAA089000F0A71C /* SimulatorTunnelInfo.swift in Sources */,
5867771429097BCD006F721F /* PaymentState.swift in Sources */,
+ A988DF262ADE86ED00D807EF /* WireGuardObfuscationSettings.swift in Sources */,
F0EF50D32A8FA47E0031E8DF /* ChangeLogInteractor.swift in Sources */,
7AC8A3AF2ABC71D600DC4939 /* TermsOfServiceCoordinator.swift in Sources */,
F0C2AEFD2A0BB5CC00986207 /* NotificationProviderIdentifier.swift in Sources */,
diff --git a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift
index 8e4fcf2897..300de34ad7 100644
--- a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift
+++ b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift
@@ -226,7 +226,7 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific
switch error {
case .outdatedSchema:
- errorString = "Unable to start tunnel connection after update. Please send a problem report."
+ errorString = "Unable to start tunnel connection after update. Please disconnect and reconnect."
case .noRelaysSatisfyingConstraints:
errorString = "No servers match your settings, try changing server or other settings."
case .invalidAccount:
diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift
index 7a7f9f84da..1547ab0f50 100644
--- a/ios/MullvadVPN/SceneDelegate.swift
+++ b/ios/MullvadVPN/SceneDelegate.swift
@@ -223,18 +223,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, SettingsMigrationUIHand
"NEWER_STORED_SETTINGS_ERROR",
tableName: "SettingsMigrationUI",
value: """
- The version of settings stored on device is from a newer app than is currently \
- running. Settings will be reset to defaults and device logged out.
- """,
- comment: ""
- )
- } else if let error = error as? SettingsMigrationError,
- error.underlyingError is REST.Error {
- return NSLocalizedString(
- "NETWORK_ERROR",
- tableName: "SettingsMigrationUI",
- value: """
- Network error occurred. Settings will be reset to defaults and device logged out.
+ The version of settings stored on device is unrecognized.\
+ Settings will be reset to defaults and the device will be logged out.
""",
comment: ""
)
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
index 0f32bf0512..415f7be2ea 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
@@ -1117,7 +1117,7 @@ final class TunnelManager: StorePaymentObserver {
}
private func unsetTunnelConfiguration(completion: @escaping () -> Void) {
- setSettings(TunnelSettingsV2(), persist: true)
+ setSettings(LatestTunnelSettings(), persist: true)
// Tell the caller to unsubscribe from VPN status notifications.
prepareForVPNConfigurationDeletion()
diff --git a/ios/MullvadVPNTests/InMemorySettingsStore.swift b/ios/MullvadVPNTests/InMemorySettingsStore.swift
new file mode 100644
index 0000000000..1f19b6429c
--- /dev/null
+++ b/ios/MullvadVPNTests/InMemorySettingsStore.swift
@@ -0,0 +1,31 @@
+//
+// InMemorySettingsStore.swift
+// MullvadVPNTests
+//
+// Created by Marco Nikic on 2023-10-17.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadSettings
+
+protocol Instantiable {
+ init()
+}
+
+class InMemorySettingsStore<ThrownError: Error>: SettingsStore where ThrownError: Instantiable {
+ private var settings = [SettingsKey: Data]()
+
+ func read(key: SettingsKey) throws -> Data {
+ guard settings.keys.contains(key), let value = settings[key] else { throw ThrownError() }
+ return value
+ }
+
+ func write(_ data: Data, for key: SettingsKey) throws {
+ settings[key] = data
+ }
+
+ func delete(key: SettingsKey) throws {
+ settings.removeValue(forKey: key)
+ }
+}
diff --git a/ios/MullvadVPNTests/MigrationManagerTests.swift b/ios/MullvadVPNTests/MigrationManagerTests.swift
new file mode 100644
index 0000000000..1b0bc35bad
--- /dev/null
+++ b/ios/MullvadVPNTests/MigrationManagerTests.swift
@@ -0,0 +1,189 @@
+//
+// MigrationManagerTests.swift
+// MullvadVPNTests
+//
+// Created by Marco Nikic on 2023-10-17.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+@testable import MullvadREST
+@testable import MullvadSettings
+@testable import MullvadTypes
+import XCTest
+
+final class MigrationManagerTests: XCTestCase {
+ static let store = InMemorySettingsStore<SettingNotFound>()
+
+ var manager: MigrationManager!
+ var proxyFactory: REST.ProxyFactory!
+ override class func setUp() {
+ SettingsManager.unitTestStore = store
+ }
+
+ override class func tearDown() {
+ SettingsManager.unitTestStore = nil
+ }
+
+ override func setUpWithError() throws {
+ let transportProvider = REST.AnyTransportProvider { nil }
+ let addressCache = REST.AddressCache(canWriteToCache: false, fileCache: MemoryCache())
+ let proxyConfiguration = REST.ProxyConfiguration(
+ transportProvider: transportProvider,
+ addressCacheStore: addressCache
+ )
+ let authProxy = REST.AuthProxyConfiguration(
+ proxyConfiguration: proxyConfiguration,
+ accessTokenManager: AccessTokenManagerStub()
+ )
+
+ proxyFactory = REST.ProxyFactory(configuration: authProxy)
+ manager = MigrationManager()
+ }
+
+ func testNothingToMigrate() throws {
+ let store = Self.store
+ let settings = LatestTunnelSettings()
+ try SettingsManager.writeSettings(settings)
+
+ let nothingToMigrateExpectation = expectation(description: "No migration")
+ manager.migrateSettings(store: store, proxyFactory: proxyFactory) { result in
+ if case .nothing = result {
+ nothingToMigrateExpectation.fulfill()
+ }
+ }
+ wait(for: [nothingToMigrateExpectation], timeout: 1)
+ }
+
+ func testNothingToMigrateWhenSettingsAreNotFound() throws {
+ let store = InMemorySettingsStore<KeychainError>()
+ SettingsManager.unitTestStore = store
+
+ let nothingToMigrateExpectation = expectation(description: "No migration")
+ manager.migrateSettings(store: store, proxyFactory: proxyFactory) { result in
+ if case .nothing = result {
+ nothingToMigrateExpectation.fulfill()
+ }
+ }
+ wait(for: [nothingToMigrateExpectation], timeout: 1)
+
+ // Reset the `SettingsManager` unit test store to avoid affecting other tests
+ // since it's a globally shared instance
+ SettingsManager.unitTestStore = Self.store
+ }
+
+ func testFailedMigration() throws {
+ let store = Self.store
+ let failedMigrationExpectation = expectation(description: "Failed migration")
+ manager.migrateSettings(store: store, proxyFactory: proxyFactory) { result in
+ if case .failure = result {
+ failedMigrationExpectation.fulfill()
+ }
+ }
+ wait(for: [failedMigrationExpectation], timeout: 1)
+ }
+
+ func testFailedMigrationResetsSettings() throws {
+ let store = Self.store
+ let data = try XCTUnwrap("Migration test".data(using: .utf8))
+ try store.write(data, for: .settings)
+ try store.write(data, for: .deviceState)
+
+ // Failed migration should reset settings and device state keys
+ manager.migrateSettings(store: store, proxyFactory: proxyFactory) { _ in }
+
+ let assertDeletionFor: (SettingsKey) throws -> Void = { key in
+ try XCTAssertThrowsError(store.read(key: key)) { thrownError in
+ XCTAssertTrue(thrownError is SettingNotFound)
+ }
+ }
+
+ try assertDeletionFor(.deviceState)
+ try assertDeletionFor(.lastUsedAccount)
+ }
+
+ func testFailedMigrationIfRecordedSettingsVersionHigherThanLatestSettings() throws {
+ let store = Self.store
+ let settings = FutureVersionSettings()
+ try write(settings: settings, version: Int.max - 1, in: store)
+
+ manager.migrateSettings(store: store, proxyFactory: proxyFactory) { _ in }
+
+ let assertDeletionFor: (SettingsKey) throws -> Void = { key in
+ try XCTAssertThrowsError(store.read(key: key)) { thrownError in
+ XCTAssertTrue(thrownError is SettingNotFound)
+ }
+ }
+
+ try assertDeletionFor(.deviceState)
+ try assertDeletionFor(.lastUsedAccount)
+ }
+
+ func testFailedMigrationCorruptedSchemaResetsSettings() throws {
+ let store = Self.store
+ let settings = FutureVersionSettings()
+ try write(settings: settings, version: -42, in: store)
+
+ let failedMigrationExpectation = expectation(description: "Failed migration")
+ manager.migrateSettings(store: store, proxyFactory: proxyFactory) { result in
+ if case .failure = result {
+ failedMigrationExpectation.fulfill()
+ }
+ }
+ wait(for: [failedMigrationExpectation], timeout: 1)
+ }
+
+ func testSuccessfulMigrationFromV2ToLatest() throws {
+ var settingsV2 = TunnelSettingsV2()
+ let osakaRelayConstraints: RelayConstraints = .init(location: .only(.city("jp", "osa")))
+ settingsV2.relayConstraints = osakaRelayConstraints
+
+ try migrateToLatest(settingsV2, version: .v2)
+
+ let latestSettings = try SettingsManager.readSettings()
+ XCTAssertEqual(osakaRelayConstraints, latestSettings.relayConstraints)
+ }
+
+ func testSuccessfulMigrationFromV1ToLatest() throws {
+ var settingsV1 = TunnelSettingsV1()
+ let osakaRelayConstraints: RelayConstraints = .init(location: .only(.city("jp", "osa")))
+ settingsV1.relayConstraints = osakaRelayConstraints
+
+ try migrateToLatest(settingsV1, version: .v1)
+
+ // Once the migration is done, settings should have been updated to the latest available version
+ // Verify that the old settings are still valid
+ let latestSettings = try SettingsManager.readSettings()
+ XCTAssertEqual(osakaRelayConstraints, latestSettings.relayConstraints)
+ }
+
+ private func migrateToLatest(_ settings: any TunnelSettings, version: SchemaVersion) throws {
+ let store = Self.store
+ try write(settings: settings, version: version.rawValue, in: store)
+
+ let successfulMigrationExpectation = expectation(description: "Successful migration")
+ manager.migrateSettings(store: store, proxyFactory: proxyFactory) { result in
+ if case .success = result {
+ successfulMigrationExpectation.fulfill()
+ }
+ }
+ wait(for: [successfulMigrationExpectation], timeout: 1)
+ }
+
+ private func write(settings: any TunnelSettings, version: Int, in store: SettingsStore) throws {
+ let parser = SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder())
+ let payload = try parser.producePayload(settings, version: version)
+ try store.write(payload, for: .settings)
+ }
+}
+
+private struct FutureVersionSettings: TunnelSettings {
+ func upgradeToNextVersion() -> TunnelSettings { self }
+}
+
+struct SettingNotFound: Error, Instantiable {}
+
+extension KeychainError: Instantiable {
+ init() {
+ self = KeychainError.itemNotFound
+ }
+}