summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-05-30 15:00:28 +0200
committerAndrej Mihajlov <and@mullvad.net>2022-05-30 15:00:28 +0200
commit5eb10755ed05d7d64e21978281976b74736574b5 (patch)
tree6d4c83af1d0b559f8bb94b1b44cf8191a4e34bd7
parent07e672f7585e7f89ae1e833405c76ea16b82787f (diff)
parent01290fbb01050b24e3b49fc7ecc6db4a2fd93a06 (diff)
downloadmullvadvpn-5eb10755ed05d7d64e21978281976b74736574b5.tar.xz
mullvadvpn-5eb10755ed05d7d64e21978281976b74736574b5.zip
Merge branch 'integrate-device-api'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj152
-rw-r--r--ios/MullvadVPN/Account.swift292
-rw-r--r--ios/MullvadVPN/AccountContentView.swift12
-rw-r--r--ios/MullvadVPN/AccountExpiry.swift34
-rw-r--r--ios/MullvadVPN/AccountViewController.swift45
-rw-r--r--ios/MullvadVPN/AppDelegate.swift127
-rw-r--r--ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift12
-rw-r--r--ios/MullvadVPN/ConnectViewController.swift25
-rw-r--r--ios/MullvadVPN/ConsolidatedApplicationLog.swift17
-rw-r--r--ios/MullvadVPN/DNSSettings.swift (renamed from ios/MullvadVPN/TunnelSettings.swift)61
-rw-r--r--ios/MullvadVPN/DisplayChainedError.swift175
-rw-r--r--ios/MullvadVPN/Keychain/Keychain.swift154
-rw-r--r--ios/MullvadVPN/Keychain/KeychainAttributes.swift139
-rw-r--r--ios/MullvadVPN/Keychain/KeychainClass.swift50
-rw-r--r--ios/MullvadVPN/Keychain/KeychainError.swift32
-rw-r--r--ios/MullvadVPN/Keychain/KeychainMatchLimit.swift48
-rw-r--r--ios/MullvadVPN/Keychain/KeychainReturn.swift57
-rw-r--r--ios/MullvadVPN/KeychainError.swift25
-rw-r--r--ios/MullvadVPN/Logging/ChainedError+Logger.swift29
-rw-r--r--ios/MullvadVPN/Logging/LogRotation.swift45
-rw-r--r--ios/MullvadVPN/Logging/Logging.swift18
-rw-r--r--ios/MullvadVPN/LoginViewController.swift69
-rw-r--r--ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift32
-rw-r--r--ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift2
-rw-r--r--ios/MullvadVPN/PreferencesViewController.swift6
-rw-r--r--ios/MullvadVPN/PrivateKeyWithMetadata.swift100
-rw-r--r--ios/MullvadVPN/ProblemReportViewController.swift2
-rw-r--r--ios/MullvadVPN/REST/RESTAPIProxy.swift247
-rw-r--r--ios/MullvadVPN/REST/RESTError.swift1
-rw-r--r--ios/MullvadVPN/REST/RESTRetryStrategy.swift7
-rw-r--r--ios/MullvadVPN/RelayCache/RelayCacheError.swift3
-rw-r--r--ios/MullvadVPN/RelayCache/RelayCacheIO.swift87
-rw-r--r--ios/MullvadVPN/RelayCache/RelayCacheTracker.swift159
-rw-r--r--ios/MullvadVPN/SettingsAccountCell.swift59
-rw-r--r--ios/MullvadVPN/SettingsDataSource.swift45
-rw-r--r--ios/MullvadVPN/SettingsManager/SettingsManager.swift256
-rw-r--r--ios/MullvadVPN/SettingsManager/TunnelSettingsV1.swift98
-rw-r--r--ios/MullvadVPN/SettingsManager/TunnelSettingsV2+REST.swift21
-rw-r--r--ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift69
-rw-r--r--ios/MullvadVPN/SettingsNavigationController.swift2
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProviderHost.swift37
-rw-r--r--ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift128
-rw-r--r--ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift249
-rw-r--r--ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift204
-rw-r--r--ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift132
-rw-r--r--ios/MullvadVPN/TunnelManager/SetAccountOperation.swift404
-rw-r--r--ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift70
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelInfo.swift18
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift369
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManagerError.swift104
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManagerState.swift14
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelObserver.swift2
-rw-r--r--ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift87
-rw-r--r--ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift96
-rw-r--r--ios/MullvadVPN/TunnelSettingsManager.swift258
-rw-r--r--ios/MullvadVPN/WireguardKeysViewController.swift94
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider.swift170
-rw-r--r--ios/PacketTunnel/Pinger.swift22
-rw-r--r--ios/PacketTunnel/TunnelMonitor.swift27
59 files changed, 2482 insertions, 2817 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index bf644e9dc1..0e1af93ed2 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -8,13 +8,7 @@
/* Begin PBXBuildFile section */
5801C9A527A14B2A0031566A /* TunnelManagerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5801C9A427A14B2A0031566A /* TunnelManagerState.swift */; };
- 5806766D27048E5500C858CB /* KeychainMatchLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */; };
- 5806766E27048E5600C858CB /* KeychainMatchLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */; };
- 5806767927048E8800C858CB /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDF6245088E100CB0F5B /* Keychain.swift */; };
- 5806767A27048E8800C858CB /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDF6245088E100CB0F5B /* Keychain.swift */; };
- 5806767B27048E8900C858CB /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDF6245088E100CB0F5B /* Keychain.swift */; };
5806767C27048E9B00C858CB /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CE5E7B224146470008646E /* PacketTunnelProvider.swift */; };
- 5806768127048EE000C858CB /* KeychainMatchLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */; };
5807483B27DB8A980020ECBF /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 5807483A27DB8A980020ECBF /* WireGuardKitTypes */; };
5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; };
5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2C1243203D000F5FF30 /* StringTests.swift */; };
@@ -28,6 +22,10 @@
58095C592762155700890776 /* RESTRetryStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58095C582762155700890776 /* RESTRetryStrategy.swift */; };
580EE20624B3222200F9D8A1 /* ExclusivityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE20524B3222200F9D8A1 /* ExclusivityController.swift */; };
580EE22424B3243100F9D8A1 /* AsyncBlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */; };
+ 580F8B8328197881002E0998 /* TunnelSettingsV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */; };
+ 580F8B8428197884002E0998 /* TunnelSettingsV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */; };
+ 580F8B8628197958002E0998 /* DNSSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8528197958002E0998 /* DNSSettings.swift */; };
+ 580F8B872819795C002E0998 /* DNSSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580F8B8528197958002E0998 /* DNSSettings.swift */; };
5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */; };
5815039724D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815039624D6ECAE00C9C50E /* CustomFormatLogHandler.swift */; };
5815039824D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815039624D6ECAE00C9C50E /* CustomFormatLogHandler.swift */; };
@@ -39,6 +37,7 @@
581503A424D6F1EC00C9C50E /* ChainedError+Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581503A224D6F1EC00C9C50E /* ChainedError+Logger.swift */; };
581503A624D6F4AE00C9C50E /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581503A524D6F4AE00C9C50E /* Logging.swift */; };
581503A724D6F4AE00C9C50E /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581503A524D6F4AE00C9C50E /* Logging.swift */; };
+ 58161C9C28352F850028ECFD /* MigrateSettingsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58161C9B28352F850028ECFD /* MigrateSettingsOperation.swift */; };
5819C2142726CC8D00D6EC38 /* DataSourceSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5819C2132726CC8D00D6EC38 /* DataSourceSnapshotTests.swift */; };
5819C2152726CC9400D6EC38 /* DataSourceSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587EB66F27143B6500123C75 /* DataSourceSnapshot.swift */; };
5819C2172729595500D6EC38 /* SettingsAddDNSEntryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */; };
@@ -69,7 +68,6 @@
582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1AE229566420055B6EF /* SettingsCell.swift */; };
582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B0229569620055B6EF /* CustomNavigationBar.swift */; };
582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */; };
- 582BB1B52295780F0055B6EF /* AccountExpiry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B42295780F0055B6EF /* AccountExpiry.swift */; };
582CFEE726945FC30072883A /* AppStoreSubscriptions.strings in Resources */ = {isa = PBXBuildFile; fileRef = 582CFEE526945FC30072883A /* AppStoreSubscriptions.strings */; };
582CFEEA269463B80072883A /* Settings.strings in Resources */ = {isa = PBXBuildFile; fileRef = 582CFEE8269463B80072883A /* Settings.strings */; };
5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5835B7CB233B76CB0096D79F /* TunnelManager.swift */; };
@@ -81,6 +79,9 @@
5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */; };
5840BE35279EDB16002836BA /* OperationCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840BE34279EDB16002836BA /* OperationCompletion.swift */; };
5842102E282D3FC200F24E46 /* ResultBlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5842102D282D3FC200F24E46 /* ResultBlockOperation.swift */; };
+ 58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */; };
+ 58421032282E42B000F24E46 /* UpdateDeviceDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */; };
+ 58421034282E4B1500F24E46 /* TunnelSettingsV2+REST.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58421033282E4B1500F24E46 /* TunnelSettingsV2+REST.swift */; };
584592612639B4A200EF967F /* ConsentContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584592602639B4A200EF967F /* ConsentContentView.swift */; };
5846226526E0D9630035F7C2 /* ProductsRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */; };
5846227126E229F20035F7C2 /* AppStoreSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227026E229F20035F7C2 /* AppStoreSubscription.swift */; };
@@ -102,8 +103,6 @@
584EBDBD2747C98F00A0C9FD /* NSAttributedString+Markdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584EBDBC2747C98F00A0C9FD /* NSAttributedString+Markdown.swift */; };
5850366825A47AC700A43E93 /* IPAddressRange+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */; };
5850367F25A481D800A43E93 /* IPAddressRange+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */; };
- 5850368C25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */; };
- 5850368D25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */; };
58554F73280AFA5A00013055 /* RESTAuthenticationProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F72280AFA5A00013055 /* RESTAuthenticationProxy.swift */; };
58554F77280AFD5C00013055 /* RESTTaskIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F76280AFD5C00013055 /* RESTTaskIdentifier.swift */; };
58554F79280B037400013055 /* RESTAccessTokenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58554F78280B037400013055 /* RESTAccessTokenManager.swift */; };
@@ -158,14 +157,14 @@
5875960A26F371FC00BF6711 /* TunnelIPCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5875960926F371FC00BF6711 /* TunnelIPCSession.swift */; };
5875960B26F3723000BF6711 /* TunnelIPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5845F841236CBACD00B2D93C /* TunnelIPC.swift */; };
5877153023981F7B001F8237 /* WireguardKeysViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5877152F23981F7B001F8237 /* WireguardKeysViewController.swift */; };
+ 5877D70F282137E8002FCFC7 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF2C02281BDE02009EF542 /* SettingsManager.swift */; };
58781CC922AE7CA8009B9D8E /* RelayConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */; };
58781CCE22AE8918009B9D8E /* RelayConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */; };
58781CD522AFBA39009B9D8E /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CD422AFBA39009B9D8E /* RelaySelector.swift */; };
5878BA1426DD0B01004147D7 /* OSLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA4F26CA690600283BF8 /* OSLogHandler.swift */; };
587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */; };
- 587AD7C623421D7000E93A53 /* TunnelSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelSettings.swift */; };
- 587AD7C723421D8600E93A53 /* TunnelSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelSettings.swift */; };
- 587AD7CA2342283900E93A53 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C92342283900E93A53 /* Account.swift */; };
+ 587AD7C623421D7000E93A53 /* TunnelSettingsV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelSettingsV1.swift */; };
+ 587AD7C723421D8600E93A53 /* TunnelSettingsV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelSettingsV1.swift */; };
587B7536266528A200DEF7E9 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B7535266528A200DEF7E9 /* NotificationManager.swift */; };
587B753B2666467500DEF7E9 /* NotificationBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B753A2666467500DEF7E9 /* NotificationBannerView.swift */; };
587B753D2666468F00DEF7E9 /* NotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B753C2666468F00DEF7E9 /* NotificationController.swift */; };
@@ -179,9 +178,8 @@
587EB672271451E300123C75 /* PreferencesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587EB671271451E300123C75 /* PreferencesViewModel.swift */; };
587EB6742714520600123C75 /* PreferencesDataSourceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587EB6732714520600123C75 /* PreferencesDataSourceDelegate.swift */; };
5883A09E266A5AF7003EFFCB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 587B7543266922BF00DEF7E9 /* Localizable.strings */; };
- 588527B2276B3F0700BAA373 /* LoadTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588527B1276B3F0700BAA373 /* LoadTunnelOperation.swift */; };
+ 588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588527B1276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift */; };
588527B4276B4F2F00BAA373 /* SetAccountOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588527B3276B4F2F00BAA373 /* SetAccountOperation.swift */; };
- 588527B6276B58B300BAA373 /* SetTunnelSettingsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588527B5276B58B300BAA373 /* SetTunnelSettingsOperation.swift */; };
58871D1E25D535A3002297FA /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 58871D1D25D535A3002297FA /* WireGuardKit */; };
58871D2325D535D2002297FA /* IPAddressRange+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */; };
5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD82227B11080051EB06 /* SelectLocationCell.swift */; };
@@ -195,9 +193,6 @@
5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */; };
5891BF5125E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */; };
5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */; };
- 5896AE7E246ACE65005B36CB /* KeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */; };
- 5896AE80246ACE79005B36CB /* KeychainClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */; };
- 5896AE82246ACE84005B36CB /* KeychainReturn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */; };
5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; };
5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */; };
5896AE88246D7FAF005B36CB /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; };
@@ -212,8 +207,6 @@
58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */; };
58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; };
58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; };
- 58AEEF6B2344A46200C9BBD5 /* TunnelSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */; };
- 58AEEF6C2344A49D00C9BBD5 /* TunnelSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */; };
58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584B26F3237434D00073B10E /* RelaySelectorTests.swift */; };
58B0A2A9238EE6A100BC001D /* RelayConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */; };
58B0A2AA238EE6A900BC001D /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CD422AFBA39009B9D8E /* RelaySelector.swift */; };
@@ -263,7 +256,7 @@
58F2E144276A13F300A79513 /* StartTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E143276A13F300A79513 /* StartTunnelOperation.swift */; };
58F2E146276A2C9900A79513 /* StopTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */; };
58F2E148276A307400A79513 /* MapConnectionStatusOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */; };
- 58F2E14C276A61C000A79513 /* ReplaceKeyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E14B276A61C000A79513 /* ReplaceKeyOperation.swift */; };
+ 58F2E14C276A61C000A79513 /* RotateKeyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E14B276A61C000A79513 /* RotateKeyOperation.swift */; };
58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F3C0A3249CB069003E76BE /* HeaderBarView.swift */; };
58F3C0A624A50157003E76BE /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 58F3C0A524A50155003E76BE /* relays.json */; };
58F3C0A724A50C02003E76BE /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 58F3C0A524A50155003E76BE /* relays.json */; };
@@ -291,18 +284,11 @@
58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */; };
58F97A1B280EEBC00050C2FC /* RESTProxyFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F97A1A280EEBC00050C2FC /* RESTProxyFactory.swift */; };
58F97A1E280FDE230050C2FC /* RESTRequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F97A1D280FDE230050C2FC /* RESTRequestHandler.swift */; };
- 58FAEDEF245069C700CB0F5B /* KeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */; };
- 58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */; };
58FAEDF4245088B300CB0F5B /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; };
- 58FAEDFF24533A7000CB0F5B /* KeychainReturn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */; };
- 58FAEE0124533A9C00CB0F5B /* KeychainClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */; };
- 58FAEE0324533ABE00CB0F5B /* KeychainReturn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */; };
- 58FAEE0424533AC000CB0F5B /* KeychainClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */; };
58FB865526E8BF3100F188BC /* AppStorePaymentManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865426E8BF3100F188BC /* AppStorePaymentManagerError.swift */; };
58FB865A26EA214400F188BC /* RelayCacheObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865926EA214400F188BC /* RelayCacheObserver.swift */; };
58FB865E26EA284E00F188BC /* LogFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865D26EA284E00F188BC /* LogFormatting.swift */; };
58FB865F26EA2E6D00F188BC /* LogFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865D26EA284E00F188BC /* LogFormatting.swift */; };
- 58FB866126EB678000F188BC /* TunnelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB866026EB677F00F188BC /* TunnelInfo.swift */; };
58FC040A27B3EE03001C21F0 /* TunnelMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FC040927B3EE03001C21F0 /* TunnelMonitor.swift */; };
58FD5BE724192A2C00112C88 /* AppStoreReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BE624192A2B00112C88 /* AppStoreReceipt.swift */; };
58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */; };
@@ -311,6 +297,7 @@
58FEAFB92750DA2F003C1625 /* AddressCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEAFB82750DA2F003C1625 /* AddressCache.swift */; };
58FEEB46260A028D00A621A8 /* GeoJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB45260A028D00A621A8 /* GeoJSON.swift */; };
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */; };
+ 58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FF2C02281BDE02009EF542 /* SettingsManager.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -371,12 +358,15 @@
58095C582762155700890776 /* RESTRetryStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTRetryStrategy.swift; sourceTree = "<group>"; };
580EE20524B3222200F9D8A1 /* ExclusivityController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExclusivityController.swift; sourceTree = "<group>"; };
580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncBlockOperation.swift; sourceTree = "<group>"; };
+ 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV2.swift; sourceTree = "<group>"; };
+ 580F8B8528197958002E0998 /* DNSSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSSettings.swift; sourceTree = "<group>"; };
5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEVPNStatus+Debug.swift"; sourceTree = "<group>"; };
5815039324D6EB7200C9C50E /* LogRotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogRotation.swift; sourceTree = "<group>"; };
5815039624D6ECAE00C9C50E /* CustomFormatLogHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFormatLogHandler.swift; sourceTree = "<group>"; };
5815039C24D6ECE600C9C50E /* TextFileOutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFileOutputStream.swift; sourceTree = "<group>"; };
581503A224D6F1EC00C9C50E /* ChainedError+Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChainedError+Logger.swift"; sourceTree = "<group>"; };
581503A524D6F4AE00C9C50E /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; };
+ 58161C9B28352F850028ECFD /* MigrateSettingsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateSettingsOperation.swift; sourceTree = "<group>"; };
5819C2132726CC8D00D6EC38 /* DataSourceSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSourceSnapshotTests.swift; sourceTree = "<group>"; };
5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAddDNSEntryCell.swift; sourceTree = "<group>"; };
581FC4F92695ACE100AA97BA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Account.strings; sourceTree = "<group>"; };
@@ -398,7 +388,6 @@
582BB1AE229566420055B6EF /* SettingsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = "<group>"; };
582BB1B0229569620055B6EF /* CustomNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNavigationBar.swift; sourceTree = "<group>"; };
582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountCell.swift; sourceTree = "<group>"; };
- 582BB1B42295780F0055B6EF /* AccountExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiry.swift; sourceTree = "<group>"; };
582CFEE626945FC30072883A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AppStoreSubscriptions.strings; sourceTree = "<group>"; };
582CFEE9269463B80072883A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Settings.strings; sourceTree = "<group>"; };
5835B7CB233B76CB0096D79F /* TunnelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManager.swift; sourceTree = "<group>"; };
@@ -408,6 +397,9 @@
5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadEndpoint.swift; sourceTree = "<group>"; };
5840BE34279EDB16002836BA /* OperationCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationCompletion.swift; sourceTree = "<group>"; };
5842102D282D3FC200F24E46 /* ResultBlockOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultBlockOperation.swift; sourceTree = "<group>"; };
+ 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateAccountDataOperation.swift; sourceTree = "<group>"; };
+ 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateDeviceDataOperation.swift; sourceTree = "<group>"; };
+ 58421033282E4B1500F24E46 /* TunnelSettingsV2+REST.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelSettingsV2+REST.swift"; sourceTree = "<group>"; };
584592602639B4A200EF967F /* ConsentContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentContentView.swift; sourceTree = "<group>"; };
5845F841236CBACD00B2D93C /* TunnelIPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelIPC.swift; sourceTree = "<group>"; };
5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsRequestOperation.swift; sourceTree = "<group>"; };
@@ -463,8 +455,7 @@
58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConstraints.swift; sourceTree = "<group>"; };
58781CD422AFBA39009B9D8E /* RelaySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelector.swift; sourceTree = "<group>"; };
587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelProviderHost.swift; sourceTree = "<group>"; };
- 587AD7C523421D7000E93A53 /* TunnelSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelSettings.swift; sourceTree = "<group>"; };
- 587AD7C92342283900E93A53 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
+ 587AD7C523421D7000E93A53 /* TunnelSettingsV1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV1.swift; sourceTree = "<group>"; };
587B7535266528A200DEF7E9 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
587B753A2666467500DEF7E9 /* NotificationBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationBannerView.swift; sourceTree = "<group>"; };
587B753C2666468F00DEF7E9 /* NotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationController.swift; sourceTree = "<group>"; };
@@ -477,9 +468,8 @@
587EB66F27143B6500123C75 /* DataSourceSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSourceSnapshot.swift; sourceTree = "<group>"; };
587EB671271451E300123C75 /* PreferencesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewModel.swift; sourceTree = "<group>"; };
587EB6732714520600123C75 /* PreferencesDataSourceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesDataSourceDelegate.swift; sourceTree = "<group>"; };
- 588527B1276B3F0700BAA373 /* LoadTunnelOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadTunnelOperation.swift; sourceTree = "<group>"; };
+ 588527B1276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadTunnelConfigurationOperation.swift; sourceTree = "<group>"; };
588527B3276B4F2F00BAA373 /* SetAccountOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetAccountOperation.swift; sourceTree = "<group>"; };
- 588527B5276B58B300BAA373 /* SetTunnelSettingsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetTunnelSettingsOperation.swift; sourceTree = "<group>"; };
5888AD82227B11080051EB06 /* SelectLocationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationCell.swift; sourceTree = "<group>"; };
5888AD86227B17950051EB06 /* SelectLocationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationViewController.swift; sourceTree = "<group>"; };
588BCF23280FE43D009ADCEC /* RESTDevicesProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTDevicesProxy.swift; sourceTree = "<group>"; };
@@ -502,7 +492,6 @@
58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSwitch.swift; sourceTree = "<group>"; };
58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSwitchContainer.swift; sourceTree = "<group>"; };
58AEEF642344A36000C9BBD5 /* KeychainError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainError.swift; sourceTree = "<group>"; };
- 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsManager.swift; sourceTree = "<group>"; };
58B0A2A0238EE67E00BC001D /* MullvadVPNTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MullvadVPNTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
58B0A2A4238EE67E00BC001D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
58B3F30E2742708B00A2DD38 /* HeaderBarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderBarButton.swift; sourceTree = "<group>"; };
@@ -517,7 +506,6 @@
58BFA5C522A7C97F00A6173D /* RelayCacheTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheTracker.swift; sourceTree = "<group>"; };
58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationConfiguration.swift; sourceTree = "<group>"; };
58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInputGroupView.swift; sourceTree = "<group>"; };
- 58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateKeyWithMetadata.swift; sourceTree = "<group>"; };
58CB0EDF24B86751001EF0D8 /* RESTAPIProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTAPIProxy.swift; sourceTree = "<group>"; };
58CC40EE24A601900019D96E /* ObserverList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObserverList.swift; sourceTree = "<group>"; };
58CCA00F224249A1004F3011 /* ConnectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewController.swift; sourceTree = "<group>"; };
@@ -554,7 +542,7 @@
58F2E143276A13F300A79513 /* StartTunnelOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartTunnelOperation.swift; sourceTree = "<group>"; };
58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopTunnelOperation.swift; sourceTree = "<group>"; };
58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapConnectionStatusOperation.swift; sourceTree = "<group>"; };
- 58F2E14B276A61C000A79513 /* ReplaceKeyOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplaceKeyOperation.swift; sourceTree = "<group>"; };
+ 58F2E14B276A61C000A79513 /* RotateKeyOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotateKeyOperation.swift; sourceTree = "<group>"; };
58F3C0A3249CB069003E76BE /* HeaderBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderBarView.swift; sourceTree = "<group>"; };
58F3C0A524A50155003E76BE /* relays.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = relays.json; sourceTree = "<group>"; };
58F558DC2695B85E00F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Consent.strings; sourceTree = "<group>"; };
@@ -580,15 +568,9 @@
58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportReviewViewController.swift; sourceTree = "<group>"; };
58F97A1A280EEBC00050C2FC /* RESTProxyFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTProxyFactory.swift; sourceTree = "<group>"; };
58F97A1D280FDE230050C2FC /* RESTRequestHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTRequestHandler.swift; sourceTree = "<group>"; };
- 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainAttributes.swift; sourceTree = "<group>"; };
- 58FAEDF6245088E100CB0F5B /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
- 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainMatchLimit.swift; sourceTree = "<group>"; };
- 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainReturn.swift; sourceTree = "<group>"; };
- 58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainClass.swift; sourceTree = "<group>"; };
58FB865426E8BF3100F188BC /* AppStorePaymentManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorePaymentManagerError.swift; sourceTree = "<group>"; };
58FB865926EA214400F188BC /* RelayCacheObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheObserver.swift; sourceTree = "<group>"; };
58FB865D26EA284E00F188BC /* LogFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogFormatting.swift; sourceTree = "<group>"; };
- 58FB866026EB677F00F188BC /* TunnelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelInfo.swift; sourceTree = "<group>"; };
58FC040927B3EE03001C21F0 /* TunnelMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelMonitor.swift; sourceTree = "<group>"; };
58FD5BE624192A2B00112C88 /* AppStoreReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreReceipt.swift; sourceTree = "<group>"; };
58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProduct+Formatting.swift"; sourceTree = "<group>"; };
@@ -597,6 +579,7 @@
58FEAFB82750DA2F003C1625 /* AddressCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCache.swift; sourceTree = "<group>"; };
58FEEB45260A028D00A621A8 /* GeoJSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoJSON.swift; sourceTree = "<group>"; };
58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticKeyboardResponder.swift; sourceTree = "<group>"; };
+ 58FF2C02281BDE02009EF542 /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -671,6 +654,17 @@
path = Operations;
sourceTree = "<group>";
};
+ 580F8B88281A79A7002E0998 /* SettingsManager */ = {
+ isa = PBXGroup;
+ children = (
+ 58FF2C02281BDE02009EF542 /* SettingsManager.swift */,
+ 587AD7C523421D7000E93A53 /* TunnelSettingsV1.swift */,
+ 580F8B8228197881002E0998 /* TunnelSettingsV2.swift */,
+ 58421033282E4B1500F24E46 /* TunnelSettingsV2+REST.swift */,
+ );
+ path = SettingsManager;
+ sourceTree = "<group>";
+ };
5815039F24D6ECF200C9C50E /* Logging */ = {
isa = PBXGroup;
children = (
@@ -689,20 +683,21 @@
isa = PBXGroup;
children = (
587C575226D2615F005EF767 /* PacketTunnelOptions.swift */,
- 58FB866026EB677F00F188BC /* TunnelInfo.swift */,
5835B7CB233B76CB0096D79F /* TunnelManager.swift */,
5801C9A427A14B2A0031566A /* TunnelManagerState.swift */,
5820676326E771DB00655B05 /* TunnelManagerError.swift */,
5823FA5326CE49F600283BF8 /* TunnelObserver.swift */,
58B93A1226C3F13600A55733 /* TunnelState.swift */,
- 588527B1276B3F0700BAA373 /* LoadTunnelOperation.swift */,
+ 588527B1276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift */,
584B17AA27637DE40057F3B8 /* ReloadTunnelOperation.swift */,
588527B3276B4F2F00BAA373 /* SetAccountOperation.swift */,
58F2E143276A13F300A79513 /* StartTunnelOperation.swift */,
58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */,
58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */,
- 58F2E14B276A61C000A79513 /* ReplaceKeyOperation.swift */,
- 588527B5276B58B300BAA373 /* SetTunnelSettingsOperation.swift */,
+ 58F2E14B276A61C000A79513 /* RotateKeyOperation.swift */,
+ 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */,
+ 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */,
+ 58161C9B28352F850028ECFD /* MigrateSettingsOperation.swift */,
);
path = TunnelManager;
sourceTree = "<group>";
@@ -865,9 +860,7 @@
58CE5E62224146200008646E /* MullvadVPN */ = {
isa = PBXGroup;
children = (
- 587AD7C92342283900E93A53 /* Account.swift */,
5896CEF126972DEB00B0FAE8 /* AccountContentView.swift */,
- 582BB1B42295780F0055B6EF /* AccountExpiry.swift */,
58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */,
58CCA01D2242787B004F3011 /* AccountTextField.swift */,
582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */,
@@ -904,6 +897,7 @@
587EB66F27143B6500123C75 /* DataSourceSnapshot.swift */,
58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */,
58B9EB142489139B00095626 /* DisplayChainedError.swift */,
+ 580F8B8528197958002E0998 /* DNSSettings.swift */,
5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */,
58FEEB45260A028D00A621A8 /* GeoJSON.swift */,
58B3F30E2742708B00A2DD38 /* HeaderBarButton.swift */,
@@ -913,7 +907,7 @@
5840250022B1124600E4CFEC /* IPAddress+Codable.swift */,
5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */,
58561C98239A5D1500BD6B5E /* IPEndpoint.swift */,
- 58FB865626E8C06800F188BC /* Keychain */,
+ 58AEEF642344A36000C9BBD5 /* KeychainError.swift */,
58727282265D173C00F315B2 /* LaunchScreen.storyboard */,
58E20770274672CA00DE5D77 /* LaunchViewController.swift */,
58A1AA8623F43901009F7EA6 /* Location.swift */,
@@ -938,7 +932,6 @@
587EB6732714520600123C75 /* PreferencesDataSourceDelegate.swift */,
58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */,
587EB671271451E300123C75 /* PreferencesViewModel.swift */,
- 58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */,
58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */,
58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */,
58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */,
@@ -959,6 +952,7 @@
58EE2E38272FF814003BFF93 /* SettingsDataSource.swift */,
58EE2E39272FF814003BFF93 /* SettingsDataSourceDelegate.swift */,
584D26C5270C8741004EA533 /* SettingsDNSTextCell.swift */,
+ 580F8B88281A79A7002E0998 /* SettingsManager */,
58E6771E24ADFE7800AA26E7 /* SettingsNavigationController.swift */,
584D26C1270C8542004EA533 /* SettingsStaticTextFooterView.swift */,
58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */,
@@ -974,8 +968,6 @@
58E0A98727C8F46300FE6BDD /* Tunnel.swift */,
585DA88D26B031D100B8C587 /* TunnelIPC */,
5823FA5726CE4A4100283BF8 /* TunnelManager */,
- 587AD7C523421D7000E93A53 /* TunnelSettings.swift */,
- 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */,
5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */,
587CBFE222807F530028DED3 /* UIColor+Helpers.swift */,
58CCA0152242560B004F3011 /* UIColor+Palette.swift */,
@@ -1029,19 +1021,6 @@
path = Assets;
sourceTree = "<group>";
};
- 58FB865626E8C06800F188BC /* Keychain */ = {
- isa = PBXGroup;
- children = (
- 58FAEDF6245088E100CB0F5B /* Keychain.swift */,
- 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */,
- 58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */,
- 58AEEF642344A36000C9BBD5 /* KeychainError.swift */,
- 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */,
- 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */,
- );
- path = Keychain;
- sourceTree = "<group>";
- };
/* End PBXGroup section */
/* Begin PBXLegacyTarget section */
@@ -1280,15 +1259,12 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 5896AE80246ACE79005B36CB /* KeychainClass.swift in Sources */,
582AE3132440CA2700E6733A /* AccountTokenInput.swift in Sources */,
58CAF4EF26025954007C5886 /* SimulatorTunnelProvider.swift in Sources */,
58B0A2AA238EE6A900BC001D /* RelaySelector.swift in Sources */,
5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */,
5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */,
- 5896AE82246ACE84005B36CB /* KeychainReturn.swift in Sources */,
58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */,
- 5806766D27048E5500C858CB /* KeychainMatchLimit.swift in Sources */,
5819C2152726CC9400D6EC38 /* DataSourceSnapshot.swift in Sources */,
584E96BE240FD4DB00D3334F /* Location.swift in Sources */,
5857F23424C8443700CF6F47 /* AsyncOperation.swift in Sources */,
@@ -1300,7 +1276,6 @@
5857F23824C8446700CF6F47 /* AsyncBlockOperation.swift in Sources */,
582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */,
585DA8A526B14EE000B8C587 /* PacketTunnelStatus.swift in Sources */,
- 5896AE7E246ACE65005B36CB /* KeychainAttributes.swift in Sources */,
58B0A2A9238EE6A100BC001D /* RelayConstraints.swift in Sources */,
5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */,
5819C2142726CC8D00D6EC38 /* DataSourceSnapshotTests.swift in Sources */,
@@ -1311,7 +1286,6 @@
58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */,
5846227A26E24F1F0035F7C2 /* ExclusivityController.swift in Sources */,
58871D2325D535D2002297FA /* IPAddressRange+Codable.swift in Sources */,
- 5806767B27048E8900C858CB /* Keychain.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1328,8 +1302,10 @@
5842102E282D3FC200F24E46 /* ResultBlockOperation.swift in Sources */,
5857F24724C882D700CF6F47 /* SelectLocationNavigationController.swift in Sources */,
5846227126E229F20035F7C2 /* AppStoreSubscription.swift in Sources */,
+ 58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */,
5820675B26E6576800655B05 /* RelayCache.swift in Sources */,
5846226526E0D9630035F7C2 /* ProductsRequestOperation.swift in Sources */,
+ 58FF2C03281BDE02009EF542 /* SettingsManager.swift in Sources */,
587EB672271451E300123C75 /* PreferencesViewModel.swift in Sources */,
58095C512760BBB500890776 /* AddressCacheTracker.swift in Sources */,
584D26C6270C8741004EA533 /* SettingsDNSTextCell.swift in Sources */,
@@ -1344,9 +1320,8 @@
58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */,
58E6771F24ADFE7800AA26E7 /* SettingsNavigationController.swift in Sources */,
58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */,
- 582BB1B52295780F0055B6EF /* AccountExpiry.swift in Sources */,
582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */,
- 588527B2276B3F0700BAA373 /* LoadTunnelOperation.swift in Sources */,
+ 588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */,
58F1311527E0B2AB007AC5BC /* Result+Extensions.swift in Sources */,
585DA88426B0270700B8C587 /* ServerRelaysResponse.swift in Sources */,
5875960726F36B3A00BF6711 /* TunnelIPCError.swift in Sources */,
@@ -1358,6 +1333,7 @@
582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */,
58B3F30F2742708B00A2DD38 /* HeaderBarButton.swift in Sources */,
584789E026529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */,
+ 58161C9C28352F850028ECFD /* MigrateSettingsOperation.swift in Sources */,
58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */,
588D2FE3248AC27F00E313F7 /* AsyncOperation.swift in Sources */,
5820675026E6514100655B05 /* HTTP.swift in Sources */,
@@ -1369,11 +1345,10 @@
58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */,
58655DCE27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */,
5850367F25A481D800A43E93 /* IPAddressRange+Codable.swift in Sources */,
- 58F2E14C276A61C000A79513 /* ReplaceKeyOperation.swift in Sources */,
+ 58F2E14C276A61C000A79513 /* RotateKeyOperation.swift in Sources */,
5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */,
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */,
5846227326E22A160035F7C2 /* AppStorePaymentObserver.swift in Sources */,
- 58FAEDEF245069C700CB0F5B /* KeychainAttributes.swift in Sources */,
58F2E146276A2C9900A79513 /* StopTunnelOperation.swift in Sources */,
585DA87A26B024F900B8C587 /* RelayCacheError.swift in Sources */,
5856D13727450A8A00DFD627 /* UIImage+TintColor.swift in Sources */,
@@ -1392,12 +1367,9 @@
58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */,
58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */,
58A99ED3240014A0006599E9 /* ConsentViewController.swift in Sources */,
- 58FAEE0124533A9C00CB0F5B /* KeychainClass.swift in Sources */,
58CCA0162242560B004F3011 /* UIColor+Palette.swift in Sources */,
58095C4F2760BA9100890776 /* AddressCacheStore.swift in Sources */,
- 58AEEF6B2344A46200C9BBD5 /* TunnelSettingsManager.swift in Sources */,
587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */,
- 5806767927048E8800C858CB /* Keychain.swift in Sources */,
588527B4276B4F2F00BAA373 /* SetAccountOperation.swift in Sources */,
585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */,
58F97A1B280EEBC00050C2FC /* RESTProxyFactory.swift in Sources */,
@@ -1420,7 +1392,6 @@
584B17AB27637DE40057F3B8 /* ReloadTunnelOperation.swift in Sources */,
5820676426E771DB00655B05 /* TunnelManagerError.swift in Sources */,
585B4B8726D9098900555C4C /* TunnelErrorNotificationProvider.swift in Sources */,
- 5850368C25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */,
58FEAFB92750DA2F003C1625 /* AddressCache.swift in Sources */,
58B67B482602079E008EF58E /* RelaySelector.swift in Sources */,
58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */,
@@ -1446,7 +1417,6 @@
58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */,
58FD5BE724192A2C00112C88 /* AppStoreReceipt.swift in Sources */,
5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */,
- 58FB866126EB678000F188BC /* TunnelInfo.swift in Sources */,
5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */,
58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */,
58FEEB46260A028D00A621A8 /* GeoJSON.swift in Sources */,
@@ -1460,20 +1430,21 @@
5857F24324C8662600CF6F47 /* SelectLocationHeaderView.swift in Sources */,
5840BE35279EDB16002836BA /* OperationCompletion.swift in Sources */,
58EE2E3A272FF814003BFF93 /* SettingsDataSource.swift in Sources */,
+ 58421032282E42B000F24E46 /* UpdateDeviceDataOperation.swift in Sources */,
58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */,
581503A624D6F4AE00C9C50E /* Logging.swift in Sources */,
58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */,
+ 580F8B8628197958002E0998 /* DNSSettings.swift in Sources */,
58FB865526E8BF3100F188BC /* AppStorePaymentManagerError.swift in Sources */,
58FD5BF42428C67600112C88 /* InAppPurchaseButton.swift in Sources */,
580EE22424B3243100F9D8A1 /* AsyncBlockOperation.swift in Sources */,
- 588527B6276B58B300BAA373 /* SetTunnelSettingsOperation.swift in Sources */,
58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */,
58B9EB152489139B00095626 /* DisplayChainedError.swift in Sources */,
587B753F2668E5A700DEF7E9 /* NotificationContainerView.swift in Sources */,
58554F79280B037400013055 /* RESTAccessTokenManager.swift in Sources */,
+ 58421034282E4B1500F24E46 /* TunnelSettingsV2+REST.swift in Sources */,
58F2E144276A13F300A79513 /* StartTunnelOperation.swift in Sources */,
5868BD33261DCD2600E6027F /* CustomSplitViewController.swift in Sources */,
- 5806766E27048E5600C858CB /* KeychainMatchLimit.swift in Sources */,
58CCA01E2242787B004F3011 /* AccountTextField.swift in Sources */,
586E54FB27A2DF6D0029B88B /* TunnelIPCRequestOperation.swift in Sources */,
584592612639B4A200EF967F /* ConsentContentView.swift in Sources */,
@@ -1481,7 +1452,6 @@
584EBDBD2747C98F00A0C9FD /* NSAttributedString+Markdown.swift in Sources */,
5875960A26F371FC00BF6711 /* TunnelIPCSession.swift in Sources */,
58FB865E26EA284E00F188BC /* LogFormatting.swift in Sources */,
- 587AD7CA2342283900E93A53 /* Account.swift in Sources */,
585DA88726B0277200B8C587 /* RESTError.swift in Sources */,
58293FB725138B88005D0BB5 /* CustomNavigationController.swift in Sources */,
587425C12299833500CA2045 /* RootContainerViewController.swift in Sources */,
@@ -1489,7 +1459,7 @@
585DA89B26B146B300B8C587 /* TunnelIPCCoding.swift in Sources */,
5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */,
585DA89626B0328000B8C587 /* TunnelIPCResponse.swift in Sources */,
- 587AD7C623421D7000E93A53 /* TunnelSettings.swift in Sources */,
+ 587AD7C623421D7000E93A53 /* TunnelSettingsV1.swift in Sources */,
581503A324D6F1EC00C9C50E /* ChainedError+Logger.swift in Sources */,
58E20771274672CA00DE5D77 /* LaunchViewController.swift in Sources */,
584D26C4270C855B004EA533 /* PreferencesDataSource.swift in Sources */,
@@ -1502,11 +1472,11 @@
5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */,
58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */,
58F840B22464491D0044E708 /* ChainedError.swift in Sources */,
- 58FAEDFF24533A7000CB0F5B /* KeychainReturn.swift in Sources */,
588BCF24280FE43D009ADCEC /* RESTDevicesProxy.swift in Sources */,
58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */,
587EB67027143B6500123C75 /* DataSourceSnapshot.swift in Sources */,
585DA88A26B027A300B8C587 /* RESTCoding.swift in Sources */,
+ 580F8B8328197881002E0998 /* TunnelSettingsV2.swift in Sources */,
587B753D2666468F00DEF7E9 /* NotificationController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1517,20 +1487,16 @@
files = (
5850366825A47AC700A43E93 /* IPAddressRange+Codable.swift in Sources */,
58FB865F26EA2E6D00F188BC /* LogFormatting.swift in Sources */,
- 5806767A27048E8800C858CB /* Keychain.swift in Sources */,
585DA89726B0328000B8C587 /* TunnelIPCResponse.swift in Sources */,
- 5806768127048EE000C858CB /* KeychainMatchLimit.swift in Sources */,
587C575426D2615F005EF767 /* PacketTunnelOptions.swift in Sources */,
- 58FAEE0324533ABE00CB0F5B /* KeychainReturn.swift in Sources */,
58BFA5CD22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */,
- 5850368D25A49E2200A43E93 /* PrivateKeyWithMetadata.swift in Sources */,
5820675826E652AF00655B05 /* RelayCacheIO.swift in Sources */,
584D26C0270C550E004EA533 /* AnyIPAddress.swift in Sources */,
5820675726E652A600655B05 /* REST.swift in Sources */,
585DA88F26B031E200B8C587 /* TunnelIPCCoding.swift in Sources */,
5806767C27048E9B00C858CB /* PacketTunnelProvider.swift in Sources */,
585DA89426B0323E00B8C587 /* TunnelIPCRequest.swift in Sources */,
- 587AD7C723421D8600E93A53 /* TunnelSettings.swift in Sources */,
+ 587AD7C723421D8600E93A53 /* TunnelSettingsV1.swift in Sources */,
5875960B26F3723000BF6711 /* TunnelIPC.swift in Sources */,
58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */,
582AD44127BE6178002A6BFC /* CodingErrors+ChainedError.swift in Sources */,
@@ -1538,16 +1504,15 @@
58FC040A27B3EE03001C21F0 /* TunnelMonitor.swift in Sources */,
5838318B27C40A3900000571 /* Pinger.swift in Sources */,
5820675C26E6576800655B05 /* RelayCache.swift in Sources */,
- 58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */,
585DA89A26B0329200B8C587 /* PacketTunnelStatus.swift in Sources */,
585DA88526B0270700B8C587 /* ServerRelaysResponse.swift in Sources */,
581503A724D6F4AE00C9C50E /* Logging.swift in Sources */,
- 58FAEE0424533AC000CB0F5B /* KeychainClass.swift in Sources */,
- 58AEEF6C2344A49D00C9BBD5 /* TunnelSettingsManager.swift in Sources */,
+ 580F8B8428197884002E0998 /* TunnelSettingsV2.swift in Sources */,
581503A424D6F1EC00C9C50E /* ChainedError+Logger.swift in Sources */,
5815039824D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */,
5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */,
58906DE02445C7A5002F0673 /* NEProviderStopReason+Debug.swift in Sources */,
+ 580F8B872819795C002E0998 /* DNSSettings.swift in Sources */,
58655DCF27DA0A5D00911834 /* TunnelMonitorConfiguration.swift in Sources */,
5815039E24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */,
585DA87826B024A900B8C587 /* CachedRelays.swift in Sources */,
@@ -1560,6 +1525,7 @@
58781CCE22AE8918009B9D8E /* RelayConstraints.swift in Sources */,
581503A024D6F01E00C9C50E /* LogRotation.swift in Sources */,
58781CD522AFBA39009B9D8E /* RelaySelector.swift in Sources */,
+ 5877D70F282137E8002FCFC7 /* SettingsManager.swift in Sources */,
5820675926E652BE00655B05 /* RESTCoding.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -2165,8 +2131,8 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mullvad/wireguard-apple.git";
requirement = {
- branch = "mullvad-master";
- kind = branch;
+ kind = revision;
+ revision = eeb980058f5bff593868e34b718b4519a3236dcc;
};
};
/* End XCRemoteSwiftPackageReference section */
diff --git a/ios/MullvadVPN/Account.swift b/ios/MullvadVPN/Account.swift
deleted file mode 100644
index c3ba572a5b..0000000000
--- a/ios/MullvadVPN/Account.swift
+++ /dev/null
@@ -1,292 +0,0 @@
-//
-// Account.swift
-// MullvadVPN
-//
-// Created by pronebird on 16/05/2019.
-// Copyright © 2019 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import StoreKit
-import Logging
-
-/// A enum holding the `UserDefaults` string keys
-private enum UserDefaultsKeys: String {
- case isAgreedToTermsOfService = "isAgreedToTermsOfService"
- case accountToken = "accountToken"
- case accountExpiry = "accountExpiry"
-}
-
-protocol AccountObserver: AnyObject {
- func account(_ account: Account, didUpdateExpiry expiry: Date)
- func account(_ account: Account, didLoginWithToken token: String, expiry: Date)
- func accountDidLogout(_ account: Account)
-}
-
-/// A class that groups the account related operations
-class Account {
-
- enum Error: ChainedError {
- /// A failure to create the new account token
- case createAccount(REST.Error)
-
- /// A failure to verify the account token
- case verifyAccount(REST.Error)
-
- /// A failure to configure a tunnel
- case tunnelConfiguration(TunnelManager.Error)
-
- var errorDescription: String? {
- switch self {
- case .createAccount:
- return "Failure to create new account."
- case .verifyAccount:
- return "Failure to verify account."
- case .tunnelConfiguration:
- return "Failure to configure the tunnel."
- }
- }
- }
-
- /// A shared instance of `Account`
- static let shared = Account()
-
- private let logger = Logger(label: "Account")
- private var observerList = ObserverList<AccountObserver>()
-
- private let operationQueue: OperationQueue = {
- let operationQueue = OperationQueue()
- operationQueue.maxConcurrentOperationCount = 1
- return operationQueue
- }()
-
- private let apiProxy = REST.ProxyFactory.shared.createAPIProxy()
-
- /// Returns true if user agreed to terms of service, otherwise false
- var isAgreedToTermsOfService: Bool {
- return UserDefaults.standard.bool(forKey: UserDefaultsKeys.isAgreedToTermsOfService.rawValue)
- }
-
- /// Returns the currently used account token
- private(set) var token: String? {
- set {
- UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.accountToken.rawValue)
- }
- get {
- return UserDefaults.standard.string(forKey: UserDefaultsKeys.accountToken.rawValue)
- }
- }
-
- var formattedToken: String? {
- return token?.split(every: 4).joined(separator: " ")
- }
-
- /// Returns the account expiry for the currently used account token
- private(set) var expiry: Date? {
- set {
- UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.accountExpiry.rawValue)
- }
- get {
- return UserDefaults.standard.object(forKey: UserDefaultsKeys.accountExpiry.rawValue) as? Date
- }
- }
-
- var isLoggedIn: Bool {
- return token != nil
- }
-
- /// Save the boolean flag in preferences indicating that the user agreed to terms of service.
- func agreeToTermsOfService() {
- UserDefaults.standard.set(true, forKey: UserDefaultsKeys.isAgreedToTermsOfService.rawValue)
- }
-
- func loginWithNewAccount(completionHandler: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void) {
- let operation = AsyncBlockOperation(dispatchQueue: .main) { operation in
- _ = self.apiProxy.createAccount(retryStrategy: .noRetry) { result in
- switch result {
- case .success(let response):
- self.setupTunnel(accountToken: response.token, expiry: response.expires) { error in
- if let error = error {
- completionHandler(.failure(error))
- } else {
- self.observerList.forEach { observer in
- observer.account(self, didLoginWithToken: response.token, expiry: response.expires)
- }
-
- completionHandler(.success(response))
- }
-
- operation.finish()
- }
-
- case .failure(let error):
- completionHandler(.failure(.createAccount(error)))
-
- operation.finish()
-
- case .cancelled:
- operation.finish()
- }
- }
- }
-
- operationQueue.addOperation(operation)
- }
-
- /// Perform the login and save the account token along with expiry (if available) to the
- /// application preferences.
- func login(accountToken: String, completionHandler: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void) {
- let operation = AsyncBlockOperation(dispatchQueue: .main) { operation in
- _ = self.apiProxy.getAccountExpiry(accountNumber: accountToken, retryStrategy: .default) { result in
- switch result {
- case .success(let response):
- self.setupTunnel(accountToken: response.token, expiry: response.expires) { error in
- if let error = error {
- completionHandler(.failure(error))
- } else {
- self.observerList.forEach { observer in
- observer.account(self, didLoginWithToken: response.token, expiry: response.expires)
- }
- completionHandler(.success(response))
- }
- operation.finish()
- }
-
- case .failure(let error):
- completionHandler(.failure(.verifyAccount(error)))
- operation.finish()
-
- case .cancelled:
- operation.finish()
- }
- }
- }
-
- operationQueue.addOperation(operation)
- }
-
- /// Perform the logout by erasing the account token and expiry from the application preferences.
- func logout(completionHandler: @escaping () -> Void) {
- let operation = AsyncBlockOperation(dispatchQueue: .main) { operation in
- TunnelManager.shared.unsetAccount {
- self.removeFromPreferences()
- self.observerList.forEach { (observer) in
- observer.accountDidLogout(self)
- }
-
- completionHandler()
- operation.finish()
- }
- }
-
- operationQueue.addOperation(operation)
- }
-
- /// Forget that user was logged in, but do not attempt to unset account in `TunnelManager`.
- /// This function is used in cases where the tunnel or tunnel settings are corrupt.
- func forget(completionHandler: @escaping () -> Void) {
- let operation = AsyncBlockOperation(dispatchQueue: .main) { operation in
- self.removeFromPreferences()
- self.observerList.forEach { (observer) in
- observer.accountDidLogout(self)
- }
-
- completionHandler()
- operation.finish()
- }
-
- operationQueue.addOperation(operation)
- }
-
- func updateAccountExpiry() {
- let operation = AsyncBlockOperation(dispatchQueue: .main) { operation in
- guard let token = self.token else {
- operation.finish()
- return
- }
-
- _ = self.apiProxy.getAccountExpiry(accountNumber: token, retryStrategy: .default) { completion in
- switch completion {
- case .success(let response):
- if self.expiry != response.expires {
- self.expiry = response.expires
- self.observerList.forEach { (observer) in
- observer.account(self, didUpdateExpiry: response.expires)
- }
- }
-
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to update account expiry.")
-
- case .cancelled:
- self.logger.debug("Account expiry update was cancelled.")
- }
-
- operation.finish()
- }
- }
-
- operationQueue.addOperation(operation)
- }
-
- private func setupTunnel(accountToken: String, expiry: Date, completionHandler: @escaping (Account.Error?) -> Void) {
- TunnelManager.shared.setAccount(accountToken: accountToken) { error in
- dispatchPrecondition(condition: .onQueue(.main))
-
- if let error = error {
- completionHandler(.tunnelConfiguration(error))
- } else {
- self.token = accountToken
- self.expiry = expiry
-
- completionHandler(nil)
- }
- }
- }
-
- private func removeFromPreferences() {
- let preferences = UserDefaults.standard
-
- preferences.removeObject(forKey: UserDefaultsKeys.accountToken.rawValue)
- preferences.removeObject(forKey: UserDefaultsKeys.accountExpiry.rawValue)
- }
-
- // MARK: - Account observation
-
- func addObserver(_ observer: AccountObserver) {
- observerList.append(observer)
- }
-
- func removeObserver(_ observer: AccountObserver) {
- observerList.remove(observer)
- }
-}
-
-extension Account: AppStorePaymentObserver {
-
- func startPaymentMonitoring(with paymentManager: AppStorePaymentManager) {
- paymentManager.addPaymentObserver(self)
- }
-
- func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction?, payment: SKPayment, accountToken: String?, didFailWithError error: AppStorePaymentManager.Error) {
- // no-op
- }
-
- func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction, accountToken: String, didFinishWithResponse response: REST.CreateApplePaymentResponse) {
- let operation = AsyncBlockOperation(dispatchQueue: .main) { operation in
- let newExpiry = response.newExpiry
-
- // Make sure that payment corresponds to the active account token
- if self.token == accountToken, self.expiry != newExpiry {
- self.expiry = newExpiry
- self.observerList.forEach { (observer) in
- observer.account(self, didUpdateExpiry: newExpiry)
- }
- }
-
- operation.finish()
- }
-
- operationQueue.addOperation(operation)
- }
-}
diff --git a/ios/MullvadVPN/AccountContentView.swift b/ios/MullvadVPN/AccountContentView.swift
index 93fb6d3186..da3a8fa85f 100644
--- a/ios/MullvadVPN/AccountContentView.swift
+++ b/ios/MullvadVPN/AccountContentView.swift
@@ -172,9 +172,9 @@ class AccountExpiryRow: UIView {
var value: Date? {
didSet {
- let expiry = value.flatMap { AccountExpiry(date: $0) }
+ let expiry = value
- if let expiry = expiry, expiry.isExpired {
+ if let expiry = expiry, expiry <= Date() {
let localizedString = NSLocalizedString(
"ACCOUNT_OUT_OF_TIME_LABEL",
tableName: "Account",
@@ -187,7 +187,13 @@ class AccountExpiryRow: UIView {
valueLabel.textColor = .dangerColor
} else {
- let formattedDate = expiry?.formattedDate
+ let formattedDate = expiry.map { date in
+ return DateFormatter.localizedString(
+ from: date,
+ dateStyle: .medium,
+ timeStyle: .short
+ )
+ }
valueLabel.text = formattedDate
accessibilityValue = formattedDate
diff --git a/ios/MullvadVPN/AccountExpiry.swift b/ios/MullvadVPN/AccountExpiry.swift
deleted file mode 100644
index 4618e736ae..0000000000
--- a/ios/MullvadVPN/AccountExpiry.swift
+++ /dev/null
@@ -1,34 +0,0 @@
-//
-// AccountExpiry.swift
-// MullvadVPN
-//
-// Created by pronebird on 22/05/2019.
-// Copyright © 2019 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-
-class AccountExpiry {
- let date: Date
-
- init(date: Date) {
- self.date = date
- }
-
- var isExpired: Bool {
- return date <= Date()
- }
-
- var formattedRemainingTime: String? {
- return CustomDateComponentsFormatting.localizedString(
- from: Date(),
- to: date,
- unitsStyle: .full
- )
- }
-
- var formattedDate: String {
- return DateFormatter.localizedString(from: date, dateStyle: .medium, timeStyle: .short)
- }
-
-}
diff --git a/ios/MullvadVPN/AccountViewController.swift b/ios/MullvadVPN/AccountViewController.swift
index 88a34167f5..164985e316 100644
--- a/ios/MullvadVPN/AccountViewController.swift
+++ b/ios/MullvadVPN/AccountViewController.swift
@@ -14,8 +14,7 @@ protocol AccountViewControllerDelegate: AnyObject {
func accountViewControllerDidLogout(_ controller: AccountViewController)
}
-class AccountViewController: UIViewController, AppStorePaymentObserver, AccountObserver {
-
+class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelObserver {
private let contentView: AccountContentView = {
let contentView = AccountContentView()
contentView.translatesAutoresizingMaskIntoConstraints = false
@@ -84,7 +83,9 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO
comment: "Navigation title"
)
- contentView.accountTokenRowView.value = Account.shared.formattedToken
+ contentView.accountTokenRowView.value = TunnelManager.shared.accountNumber.map { string in
+ return formatAccountNumber(string)
+ }
contentView.accountTokenRowView.actionHandler = { [weak self] in
self?.copyAccountToken()
}
@@ -94,9 +95,9 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO
contentView.logoutButton.addTarget(self, action: #selector(doLogout), for: .touchUpInside)
AppStorePaymentManager.shared.addPaymentObserver(self)
- Account.shared.addObserver(self)
+ TunnelManager.shared.addObserver(self)
- updateAccountExpiry(expiryDate: Account.shared.expiry)
+ updateAccountExpiry(expiryDate: TunnelManager.shared.accountExpiry)
// Make sure to disable IAPs when payments are restricted
if AppStorePaymentManager.canMakePayments {
@@ -293,7 +294,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO
)
alertPresenter.enqueue(alertController, presentingController: self) {
- Account.shared.logout {
+ TunnelManager.shared.unsetAccount {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
alertController.dismiss(animated: true) {
self.delegate?.accountViewControllerDidLogout(self)
@@ -303,18 +304,18 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO
}
}
- // MARK: - AccountObserver
+ // MARK: - TunnelObserver
- func account(_ account: Account, didUpdateExpiry expiry: Date) {
- updateAccountExpiry(expiryDate: expiry)
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) {
+ // no-op
}
- func account(_ account: Account, didLoginWithToken token: String, expiry: Date) {
+ func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) {
// no-op
}
- func accountDidLogout(_ account: Account) {
- // no-op
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) {
+ updateAccountExpiry(expiryDate: tunnelSettings?.account.expiry)
}
// MARK: - AppStorePaymentObserver
@@ -351,7 +352,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO
func appStorePaymentManager(_ manager: AppStorePaymentManager, transaction: SKPaymentTransaction, accountToken: String, didFinishWithResponse response: REST.CreateApplePaymentResponse) {
showTimeAddedConfirmationAlert(with: response, context: .purchase)
- if transaction.payment == self.pendingPayment {
+ if transaction.payment == pendingPayment {
compoundInteractionRestriction.decrease(animated: true)
}
}
@@ -368,7 +369,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO
}
private func copyAccountToken() {
- UIPasteboard.general.string = Account.shared.token
+ UIPasteboard.general.string = TunnelManager.shared.accountNumber
contentView.accountTokenRowView.value = NSLocalizedString(
"COPIED_TO_PASTEBOARD_LABEL",
@@ -377,9 +378,9 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO
)
let workItem = DispatchWorkItem { [weak self] in
- guard let formattedToken = Account.shared.formattedToken else { return }
+ guard let accountNumber = TunnelManager.shared.accountNumber else { return }
- self?.contentView.accountTokenRowView.value = formattedToken
+ self?.contentView.accountTokenRowView.value = self?.formatAccountNumber(accountNumber)
}
copyToPasteboardWork?.cancel()
@@ -389,22 +390,22 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO
}
@objc private func doPurchase() {
- guard let product = product, let accountToken = Account.shared.token else { return }
+ guard let product = product, let accountNumber = TunnelManager.shared.accountNumber else { return }
let payment = SKPayment(product: product)
pendingPayment = payment
compoundInteractionRestriction.increase(animated: true)
- AppStorePaymentManager.shared.addPayment(payment, for: accountToken)
+ AppStorePaymentManager.shared.addPayment(payment, for: accountNumber)
}
@objc private func restorePurchases() {
- guard let accountToken = Account.shared.token else { return }
+ guard let accountNumber = TunnelManager.shared.accountNumber else { return }
compoundInteractionRestriction.increase(animated: true)
- _ = AppStorePaymentManager.shared.restorePurchases(for: accountToken) { completion in
+ _ = AppStorePaymentManager.shared.restorePurchases(for: accountNumber) { completion in
switch completion {
case .success(let response):
self.showTimeAddedConfirmationAlert(with: response, context: .restoration)
@@ -438,6 +439,10 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO
}
}
+ private func formatAccountNumber(_ string: String) -> String {
+ return string.split(every: 4).joined(separator: " ")
+ }
+
}
private extension REST.CreateApplePaymentResponse {
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index bba086e75d..c5d4d9e9e4 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -103,29 +103,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
// Load tunnels
- TunnelManager.shared.loadTunnel(accountToken: Account.shared.token) { error in
+ TunnelManager.shared.loadConfiguration { error in
dispatchPrecondition(condition: .onQueue(.main))
if let error = error {
self.logger?.error(chainedError: error, message: "Failed to load tunnels")
- switch error {
- case .loadAllVPNConfigurations(_), .removeInconsistentVPNConfiguration(_):
- // TODO: avoid throwing fatal error and show the problem report UI instead.
- fatalError(error.displayChain(message: "Failed to load tunnels"))
-
- case .migrateTunnelSettings(_), .readTunnelSettings(_):
- // Forget that user was logged in since tunnel settings are likely corrupt
- // or missing.
- Account.shared.forget {
- self.didFinishInitialization()
- }
-
- default:
- fatalError("Unexpected error coming from loadTunnel()")
- }
+ // 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.tunnelInfo?.tunnelSettings.relayConstraints
+ self.relayConstraints = TunnelManager.shared.tunnelSettings?.relayConstraints
self.didFinishInitialization()
}
}
@@ -263,18 +250,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
@available(iOS 13.0, *)
private func scheduleBackgroundTasks() {
- switch RelayCache.Tracker.shared.scheduleAppRefreshTask() {
- case .success:
- self.logger?.debug("Scheduled app refresh task.")
- case .failure(let error):
- self.logger?.error(chainedError: error, message: "Could not schedule app refresh task.")
+ do {
+ try RelayCache.Tracker.shared.scheduleAppRefreshTask()
+
+ logger?.debug("Scheduled app refresh task.")
+ } catch {
+ logger?.error(
+ chainedError: AnyChainedError(error),
+ message: "Could not schedule app refresh task."
+ )
}
- switch TunnelManager.shared.scheduleBackgroundTask() {
- case .success:
- self.logger?.debug("Scheduled private key rotation task")
- case .failure(let error):
- self.logger?.error(chainedError: error, message: "Could not schedule private key rotation task.")
+ do {
+ try TunnelManager.shared.scheduleBackgroundTask()
+
+ logger?.debug("Scheduled private key rotation task.")
+ } catch {
+ logger?.error(
+ chainedError: AnyChainedError(error),
+ message: "Could not schedule private key rotation task."
+ )
}
do {
@@ -282,9 +277,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
self.logger?.debug("Scheduled address cache update task.")
} catch {
- self.logger?.error(chainedError: AnyChainedError(error), message: "Could not schedule address cache update task.")
+ self.logger?.error(
+ chainedError: AnyChainedError(error),
+ message: "Could not schedule address cache update task."
+ )
}
-
}
private func didFinishInitialization() {
@@ -317,8 +314,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
private func startPaymentQueueHandling() {
let paymentManager = AppStorePaymentManager.shared
paymentManager.delegate = self
-
- Account.shared.startPaymentMonitoring(with: paymentManager)
+ paymentManager.addPaymentObserver(TunnelManager.shared)
paymentManager.startPaymentQueueMonitoring()
}
@@ -339,15 +335,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
self.connectController = connectController
self.rootContainer?.setViewControllers([splitViewController], animated: false)
- showSplitViewMaster(Account.shared.isLoggedIn, animated: false)
+ showSplitViewMaster(TunnelManager.shared.isAccountSet, animated: false)
let rootContainerWrapper = makeLoginContainerController()
- if !Account.shared.isAgreedToTermsOfService {
+ if !isAgreedToTermsOfService() {
let consentViewController = self.makeConsentController { [weak self] (viewController) in
guard let self = self else { return }
- if Account.shared.isLoggedIn {
+ if TunnelManager.shared.isAccountSet {
rootContainerWrapper.dismiss(animated: true) {
self.showAccountSettingsControllerIfAccountExpired()
}
@@ -357,7 +353,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
rootContainerWrapper.setViewControllers([consentViewController], animated: false)
self.rootContainer?.present(rootContainerWrapper, animated: false)
- } else if !Account.shared.isLoggedIn {
+ } else if !TunnelManager.shared.isAccountSet {
rootContainerWrapper.setViewControllers([makeLoginController()], animated: false)
self.rootContainer?.present(rootContainerWrapper, animated: false)
} else {
@@ -372,7 +368,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
let loginViewController = self.makeLoginController()
var viewControllers: [UIViewController] = [loginViewController]
- if Account.shared.isLoggedIn {
+ if TunnelManager.shared.isAccountSet {
let connectController = self.makeConnectViewController()
viewControllers.append(connectController)
self.connectController = connectController
@@ -383,7 +379,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
- if Account.shared.isAgreedToTermsOfService {
+ if isAgreedToTermsOfService() {
showNextController(false)
} else {
let consentViewController = self.makeConsentController { (consentController) in
@@ -428,7 +424,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
consentViewController.completionHandler = { (consentViewController) in
- Account.shared.agreeToTermsOfService()
+ setAgreedToTermsOfService()
completion(consentViewController)
}
@@ -478,7 +474,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
private func showAccountSettingsControllerIfAccountExpired() {
- guard let accountExpiry = Account.shared.expiry, AccountExpiry(date: accountExpiry).isExpired else { return }
+ guard let accountExpiry = TunnelManager.shared.accountExpiry, accountExpiry <= Date() else { return }
rootContainer?.showSettings(navigateTo: .account, animated: true)
}
@@ -534,7 +530,7 @@ extension AppDelegate: RootContainerViewControllerDelegate {
}
func rootContainerViewAccessibilityPerformMagicTap(_ controller: RootContainerViewController) -> Bool {
- guard Account.shared.isLoggedIn else { return false }
+ guard TunnelManager.shared.isAccountSet else { return false }
switch TunnelManager.shared.tunnelState {
case .connected, .connecting, .reconnecting:
@@ -552,39 +548,45 @@ extension AppDelegate: RootContainerViewControllerDelegate {
extension AppDelegate: LoginViewControllerDelegate {
- func loginViewController(_ controller: LoginViewController, loginWithAccountToken accountToken: String, completion: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void) {
+ func loginViewController(_ controller: LoginViewController, loginWithAccountToken accountNumber: String, completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void) {
self.rootContainer?.setEnableSettingsButton(false)
- Account.shared.login(accountToken: accountToken) { result in
- switch result {
+ TunnelManager.shared.setAccount(action: .existing(accountNumber)) { operationCompletion in
+ switch operationCompletion {
case .success:
- self.logger?.debug("Logged in with existing token")
+ 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:
self.rootContainer?.setEnableSettingsButton(true)
}
- completion(result)
+ completion(operationCompletion)
}
}
- func loginViewControllerLoginWithNewAccount(_ controller: LoginViewController, completion: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void) {
+ func loginViewControllerLoginWithNewAccount(_ controller: LoginViewController, completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void) {
self.rootContainer?.setEnableSettingsButton(false)
- Account.shared.loginWithNewAccount { result in
- switch result {
+ TunnelManager.shared.setAccount(action: .new) { operationCompletion in
+ switch operationCompletion {
case .success:
- self.logger?.debug("Logged in with new account token")
+ 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:
self.rootContainer?.setEnableSettingsButton(true)
}
- completion(result)
+ completion(operationCompletion)
}
}
@@ -594,7 +596,7 @@ extension AppDelegate: LoginViewControllerDelegate {
// Move the settings button back into header bar
self.rootContainer?.removeSettingsButtonFromPresentationContainer()
- self.relayConstraints = TunnelManager.shared.tunnelInfo?.tunnelSettings.relayConstraints
+ self.relayConstraints = TunnelManager.shared.tunnelSettings?.relayConstraints
self.selectLocationViewController?.setSelectedRelayLocation(relayConstraints?.location.value, animated: false, scrollPosition: .middle)
switch UIDevice.current.userInterfaceIdiom {
@@ -788,7 +790,7 @@ extension AppDelegate: AppStorePaymentManagerDelegate {
{
// Since we do not persist the relation between the payment and account token between the
// app launches, we assume that all successful purchases belong to the active account token.
- return Account.shared.token
+ return TunnelManager.shared.tunnelSettings?.account.number
}
}
@@ -823,7 +825,7 @@ extension AppDelegate: UISplitViewControllerDelegate {
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
- if response.notification.request.identifier == kAccountExpiryNotificationIdentifier,
+ if response.notification.request.identifier == accountExpiryNotificationIdentifier,
response.actionIdentifier == UNNotificationDefaultActionIdentifier {
rootContainer?.showSettings(navigateTo: .account, animated: true)
}
@@ -840,3 +842,18 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
}
}
+
+// MARK: -
+
+/// A enum holding the `UserDefaults` string keys
+private let isAgreedToTermsOfServiceKey = "isAgreedToTermsOfService"
+
+/// Returns true if user agreed to terms of service, otherwise false.
+func isAgreedToTermsOfService() -> Bool {
+ return UserDefaults.standard.bool(forKey: isAgreedToTermsOfServiceKey)
+}
+
+/// Save the boolean flag in preferences indicating that the user agreed to terms of service.
+func setAgreedToTermsOfService() {
+ UserDefaults.standard.set(true, forKey: isAgreedToTermsOfServiceKey)
+}
diff --git a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift
index 885ad3cbad..e94a6e906b 100644
--- a/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift
+++ b/ios/MullvadVPN/AppStorePaymentManager/AppStorePaymentManager.swift
@@ -29,6 +29,7 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver {
}()
private let apiProxy = REST.ProxyFactory.shared.createAPIProxy()
+ private let accountsProxy = REST.ProxyFactory.shared.createAccountsProxy()
private let exclusivityController = ExclusivityController()
@@ -111,16 +112,19 @@ class AppStorePaymentManager: NSObject, SKPaymentTransactionObserver {
}
func addPayment(_ payment: SKPayment, for accountToken: String) {
- var cancellableTask: Cancellable?
+ var task: Cancellable?
let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Validate account token") {
- cancellableTask?.cancel()
+ task?.cancel()
}
// Validate account token before adding new payment to the queue.
- cancellableTask = apiProxy.getAccountExpiry(accountNumber: accountToken, retryStrategy: .default) { result in
+ task = accountsProxy.getAccountData(
+ accountNumber: accountToken,
+ retryStrategy: .default
+ ) { completion in
dispatchPrecondition(condition: .onQueue(.main))
- switch result {
+ switch completion {
case .success:
self.associateAccountToken(accountToken, and: payment)
self.paymentQueue.add(payment)
diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift
index 1c6401bf63..3e2f6926ba 100644
--- a/ios/MullvadVPN/ConnectViewController.swift
+++ b/ios/MullvadVPN/ConnectViewController.swift
@@ -22,7 +22,7 @@ protocol ConnectViewControllerDelegate: AnyObject {
func connectViewControllerShouldShowSelectLocationPicker(_ controller: ConnectViewController)
}
-class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainment, TunnelObserver, AccountObserver {
+class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainment, TunnelObserver {
weak var delegate: ConnectViewControllerDelegate?
@@ -46,9 +46,10 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen
}
var preferredHeaderBarPresentation: HeaderBarPresentation {
- if !Account.shared.isLoggedIn {
+ guard TunnelManager.shared.isAccountSet else {
return HeaderBarPresentation(style: .default, showsDivider: true)
}
+
switch tunnelState {
case .connecting, .reconnecting, .connected:
return HeaderBarPresentation(style: .secured, showsDivider: false)
@@ -94,7 +95,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen
updateLocation(animated: false)
addNotificationController()
- Account.shared.addObserver(self)
+ TunnelManager.shared.addObserver(self)
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
@@ -128,24 +129,10 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen
])
}
- // MARK: - AccountObserver
-
- func account(_ account: Account, didLoginWithToken token: String, expiry: Date) {
- setNeedsHeaderBarStyleAppearanceUpdate()
- }
-
- func account(_ account: Account, didUpdateExpiry expiry: Date) {
- // no-op
- }
-
- func accountDidLogout(_ account: Account) {
- setNeedsHeaderBarStyleAppearanceUpdate()
- }
-
// MARK: - TunnelObserver
- func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelInfo: TunnelInfo?) {
- // no-op
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) {
+ setNeedsHeaderBarStyleAppearanceUpdate()
}
func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) {
diff --git a/ios/MullvadVPN/ConsolidatedApplicationLog.swift b/ios/MullvadVPN/ConsolidatedApplicationLog.swift
index e31b6b6371..bd11fac1c9 100644
--- a/ios/MullvadVPN/ConsolidatedApplicationLog.swift
+++ b/ios/MullvadVPN/ConsolidatedApplicationLog.swift
@@ -15,7 +15,6 @@ private let kRedactedAccountPlaceholder = "[REDACTED ACCOUNT NUMBER]"
private let kRedactedContainerPlaceholder = "[REDACTED CONTAINER PATH]"
class ConsolidatedApplicationLog: TextOutputStreamable {
-
typealias Metadata = KeyValuePairs<MetadataKey, String>
enum MetadataKey: String {
@@ -108,13 +107,13 @@ class ConsolidatedApplicationLog: TextOutputStreamable {
let path = fileURL.path
let redactedPath = redact(string: path)
- switch Self.readFileLossy(path: path, maxBytes: kLogMaxReadBytes) {
- case .success(let lossyString):
+ do {
+ let lossyString = try Self.readFileLossy(path: path, maxBytes: kLogMaxReadBytes)
let redactedString = redact(string: lossyString)
- logs.append(LogAttachment(label: redactedPath, content: redactedString))
- case .failure(let error):
- addError(message: redactedPath, error: error)
+ logs.append(LogAttachment(label: redactedPath, content: redactedString))
+ } catch {
+ addError(message: redactedPath, error: AnyChainedError(error))
}
}
@@ -129,9 +128,9 @@ class ConsolidatedApplicationLog: TextOutputStreamable {
]
}
- private static func readFileLossy(path: String, maxBytes: UInt64) -> Result<String, Error> {
+ private static func readFileLossy(path: String, maxBytes: UInt64) throws -> String {
guard let fileHandle = FileHandle(forReadingAtPath: path) else {
- return .failure(.logFileDoesNotExist(path))
+ throw Error.logFileDoesNotExist(path)
}
let endOfFileOffset = fileHandle.seekToEndOfFile()
@@ -149,7 +148,7 @@ class ConsolidatedApplicationLog: TextOutputStreamable {
return ch == replacementCharacter
})
- return .success(lossyString)
+ return lossyString
}
private func redactCustomStrings(string: String) -> String {
diff --git a/ios/MullvadVPN/TunnelSettings.swift b/ios/MullvadVPN/DNSSettings.swift
index a9d8c5b063..bee5b1d7f1 100644
--- a/ios/MullvadVPN/TunnelSettings.swift
+++ b/ios/MullvadVPN/DNSSettings.swift
@@ -1,68 +1,13 @@
//
-// TunnelSettings.swift
+// DNSSettings.swift
// MullvadVPN
//
-// Created by pronebird on 19/06/2019.
-// Copyright © 2019 Mullvad VPN AB. All rights reserved.
+// Created by pronebird on 27/04/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
//
import Foundation
import struct Network.IPv4Address
-import class WireGuardKitTypes.PublicKey
-import struct WireGuardKitTypes.IPAddressRange
-
-/// A struct that holds a tun interface configuration.
-struct InterfaceSettings: Codable, Equatable {
- var privateKey: PrivateKeyWithMetadata
- var nextPrivateKey: PrivateKeyWithMetadata?
-
- var addresses: [IPAddressRange]
- var dnsSettings: DNSSettings
-
- var publicKey: PublicKey {
- return privateKey.publicKeyWithMetadata.publicKey
- }
-
- private enum CodingKeys: String, CodingKey {
- case privateKey, nextPrivateKey, addresses, dnsSettings
- }
-
- init(privateKey: PrivateKeyWithMetadata = PrivateKeyWithMetadata(), nextPrivateKey: PrivateKeyWithMetadata? = nil, addresses: [IPAddressRange] = [], dnsSettings: DNSSettings = DNSSettings()) {
- self.privateKey = privateKey
- self.nextPrivateKey = nextPrivateKey
- self.addresses = addresses
- self.dnsSettings = dnsSettings
- }
-
- init(from decoder: Decoder) throws {
- let container = try decoder.container(keyedBy: CodingKeys.self)
-
- privateKey = try container.decode(PrivateKeyWithMetadata.self, forKey: .privateKey)
- addresses = try container.decode([IPAddressRange].self, forKey: .addresses)
-
- // Added in 2022.1
- nextPrivateKey = try container.decodeIfPresent(PrivateKeyWithMetadata.self, forKey: .nextPrivateKey)
-
- // Provide default value, since `dnsSettings` key does not exist in <= 2021.2
- dnsSettings = try container.decodeIfPresent(DNSSettings.self, forKey: .dnsSettings)
- ?? DNSSettings()
- }
-
- func encode(to encoder: Encoder) throws {
- var container = encoder.container(keyedBy: CodingKeys.self)
-
- try container.encode(privateKey, forKey: .privateKey)
- try container.encode(nextPrivateKey, forKey: .nextPrivateKey)
- try container.encode(addresses, forKey: .addresses)
- try container.encode(dnsSettings, forKey: .dnsSettings)
- }
-}
-
-/// A struct that holds the configuration passed via `NETunnelProviderProtocol`.
-struct TunnelSettings: Codable, Equatable {
- var relayConstraints = RelayConstraints()
- var interface = InterfaceSettings()
-}
/// A struct describing Mullvad DNS blocking options.
struct DNSBlockingOptions: OptionSet, Codable {
diff --git a/ios/MullvadVPN/DisplayChainedError.swift b/ios/MullvadVPN/DisplayChainedError.swift
index aa7ec26b2d..d5955e3aae 100644
--- a/ios/MullvadVPN/DisplayChainedError.swift
+++ b/ios/MullvadVPN/DisplayChainedError.swift
@@ -68,7 +68,6 @@ extension TunnelManager.Error: DisplayChainedError {
),
systemError.localizedDescription
)
-
case .reloadVPNConfiguration(let systemError):
return String(
format: NSLocalizedString(
@@ -79,7 +78,6 @@ extension TunnelManager.Error: DisplayChainedError {
),
systemError.localizedDescription
)
-
case .saveVPNConfiguration(let systemError):
return String(
format: NSLocalizedString(
@@ -90,15 +88,6 @@ extension TunnelManager.Error: DisplayChainedError {
),
systemError.localizedDescription
)
-
- case .obtainPersistentKeychainReference(_):
- return NSLocalizedString(
- "OBTAIN_PERSISTENT_KEYCHAIN_REFERENCE_ERROR",
- tableName: "TunnelManager",
- value: "Failed to obtain the persistent keychain reference for the VPN configuration",
- comment: ""
- )
-
case .startVPNTunnel(let systemError):
return String(
format: NSLocalizedString(
@@ -109,7 +98,6 @@ extension TunnelManager.Error: DisplayChainedError {
),
systemError.localizedDescription
)
-
case .removeVPNConfiguration(let systemError):
return String(
format: NSLocalizedString(
@@ -120,121 +108,79 @@ extension TunnelManager.Error: DisplayChainedError {
),
systemError.localizedDescription
)
-
- case .removeInconsistentVPNConfiguration(let systemError):
- return String(
- format: NSLocalizedString(
- "REMOVE_INCONSISTENT_VPN_CONFIGURATION",
- tableName: "TunnelManager",
- value: "Failed to remove the outdated system VPN configuration: %@",
- comment: ""
- ),
- systemError.localizedDescription
- )
-
- case .readTunnelSettings(_):
+ case .readSettings:
return NSLocalizedString(
"READ_TUNNEL_SETTINGS_ERROR",
tableName: "TunnelManager",
- value: "Failed to read tunnel settings",
+ value: "Failed to read settings",
comment: ""
)
-
- case .addTunnelSettings(_):
+ case .writeSettings:
return NSLocalizedString(
- "ADD_TUNNEL_SETTINGS_ERROR",
+ "WRITE_TUNNEL_SETTINGS_ERROR",
tableName: "TunnelManager",
- value: "Failed to add tunnel settings",
+ value: "Failed to write settings",
comment: ""
)
-
- case .updateTunnelSettings(_):
+ case .deleteSettings:
return NSLocalizedString(
- "UPDATE_TUNNEL_SETTINGS_ERROR",
+ "DELETE_TUNNEL_SETTINGS_ERROR",
tableName: "TunnelManager",
- value: "Failed to update tunnel settings",
+ value: "Failed to delete settings",
comment: ""
)
-
- case .removeTunnelSettings(_):
- return NSLocalizedString(
- "REMOVE_TUNNEL_SETTINGS_ERROR",
- tableName: "TunnelManager",
- value: "Failed to remove tunnel settings",
- comment: ""
+ case .deleteDevice(let restError):
+ return String(
+ format: NSLocalizedString(
+ "DELETE_DEVICE_ERROR",
+ tableName: "TunnelManager",
+ value: "Failed to create a device: %@",
+ comment: ""
+ ),
+ restError.errorChainDescription ?? ""
)
-
- case .migrateTunnelSettings(_):
+ case .getDevice(let restError):
+ return String(
+ format: NSLocalizedString(
+ "CREATE_DEVICE_ERROR",
+ tableName: "TunnelManager",
+ value: "Failed to obtain device data: %@",
+ comment: ""
+ ),
+ restError.errorChainDescription ?? ""
+ )
+ case .deviceRevoked:
return NSLocalizedString(
- "MIGRATE_TUNNEL_SETTINGS_ERROR",
+ "DEVICE_REVOKED_ERROR",
tableName: "TunnelManager",
- value: "Failed to migrate tunnel settings",
+ value: "Device is revoked.",
comment: ""
)
-
- case .pushWireguardKey(let restError):
- let reason = restError.errorChainDescription ?? ""
- var message = String(
+ case .createDevice(let restError):
+ return String(
format: NSLocalizedString(
- "PUSH_WIREGUARD_KEY_ERROR",
+ "CREATE_DEVICE_ERROR",
tableName: "TunnelManager",
- value: "Failed to send the WireGuard key to server: %@",
+ value: "Failed to create a device: %@",
comment: ""
),
- reason
+ restError.errorChainDescription ?? ""
)
-
- if case .unhandledResponse(_, let serverErrorResponse) = restError,
- serverErrorResponse?.code == .keyLimitReached
- {
- // TODO: maybe use `restError.recoverySuggestion` instead?
- message.append("\n\n")
- message.append(NSLocalizedString(
- "PUSH_WIREGUARD_KEY_RECOVERY_SUGGESTION",
- tableName: "TunnelManager",
- value: "Remove unused WireGuard keys and try again",
- comment: ""
- ))
- }
-
- return message
-
- case .replaceWireguardKey(let restError):
- let reason = restError.errorChainDescription ?? ""
- var message = String(
+ case .rotateKey(let restError):
+ return String(
format: NSLocalizedString(
- "REPLACE_WIREGUARD_KEY_ERROR",
+ "ROTATE_KY_ERROR",
tableName: "TunnelManager",
- value: "Failed to replace the WireGuard key on server: %@",
+ value: "Failed to rotate WireGuard key: %@",
comment: ""
),
- reason
+ restError.errorChainDescription ?? ""
)
-
- if case .unhandledResponse(_, let serverErrorResponse) = restError,
- serverErrorResponse?.code == .keyLimitReached
- {
- // TODO: maybe use `restError.recoverySuggestion` instead?
- message.append("\n\n")
- message.append(NSLocalizedString(
- "REPLACE_WIREGUARD_KEY_RECOVERY_SUGGESTION",
- tableName: "TunnelManager",
- value: "Remove unused WireGuard keys and try again",
- comment: "")
- )
- }
-
- return message
-
- case .removeWireguardKey:
- // This error is never displayed anywhere
- return nil
-
case .unsetAccount:
return NSLocalizedString(
- "MISSING_ACCOUNT_INTERNAL_ERROR",
+ "UNSET_ACCOUNT_ERROR",
tableName: "TunnelManager",
- value: "Internal error: missing account",
+ value: "Internal error: account is unset",
comment: ""
)
case .readRelays:
@@ -251,11 +197,6 @@ extension TunnelManager.Error: DisplayChainedError {
value: "Failed to satisfy relay constraints.",
comment: ""
)
-
- case .backgroundTaskScheduler:
- // This error is never displayed anywhere
- return nil
-
case .reloadTunnel(let error):
return String(
format: NSLocalizedString(
@@ -266,18 +207,26 @@ extension TunnelManager.Error: DisplayChainedError {
),
error.localizedDescription
)
- }
- }
-}
-
-extension Account.Error: DisplayChainedError {
- var errorChainDescription: String? {
- switch self {
- case .createAccount(let restError), .verifyAccount(let restError):
- return restError.errorChainDescription
-
- case .tunnelConfiguration(let tunnelError):
- return tunnelError.errorChainDescription
+ case .getAccountData(let restError):
+ return String(
+ format: NSLocalizedString(
+ "GET_ACCOUNT_DATA_ERROR",
+ tableName: "TunnelManager",
+ value: "Failed to obtain account data: %@",
+ comment: ""
+ ),
+ restError.errorChainDescription ?? ""
+ )
+ case .createAccount(let restError):
+ return String(
+ format: NSLocalizedString(
+ "CREATE_ACCOUNT_ERROR",
+ tableName: "TunnelManager",
+ value: "Failed to create new account: %@",
+ comment: ""
+ ),
+ restError.errorChainDescription ?? ""
+ )
}
}
}
diff --git a/ios/MullvadVPN/Keychain/Keychain.swift b/ios/MullvadVPN/Keychain/Keychain.swift
deleted file mode 100644
index 383219990a..0000000000
--- a/ios/MullvadVPN/Keychain/Keychain.swift
+++ /dev/null
@@ -1,154 +0,0 @@
-//
-// Keychain.swift
-// MullvadVPN
-//
-// Created by pronebird on 22/04/2020.
-// Copyright © 2020 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import Security
-
-protocol KeychainAttributeDecodable {
- init?(attributes: [CFString: Any])
-}
-
-protocol KeychainAttributeEncodable {
- func keychainRepresentation() -> [CFString: Any]
- func updateKeychainAttributes(in attributes: inout [CFString: Any])
-}
-
-extension KeychainAttributeEncodable {
- func keychainRepresentation() -> [CFString: Any] {
- var attributes = [CFString: Any]()
- updateKeychainAttributes(in: &attributes)
- return attributes
- }
-}
-
-enum Keychain {}
-
-extension Keychain {
-
- /// A Keychain Result type
- typealias Result<T> = Swift.Result<T, Keychain.Error>
-
- static func add(_ attributes: Keychain.Attributes) -> Result<Keychain.Attributes?> {
- var result: CFTypeRef?
- let status = SecItemAdd(attributes.keychainRepresentation() as CFDictionary, &result)
-
- return mapSecResultAndReturnValue(
- status: status,
- value: result,
- returnSet: attributes.return ?? [],
- limit: .one)
- .map { $0.first }
- }
-
- static func update(query: Keychain.Attributes, update: Keychain.Attributes) -> Result<()> {
- let queryAttributes = query.keychainRepresentation() as CFDictionary
- let updateAttributes = update.keychainRepresentation() as CFDictionary
-
- let status = SecItemUpdate(queryAttributes, updateAttributes)
-
- return mapSecResult(status: status) {
- return ()
- }
- }
-
- static func delete(query: Keychain.Attributes) -> Result<()> {
- let status = SecItemDelete(query.keychainRepresentation() as CFDictionary)
-
- return mapSecResult(status: status) {
- return ()
- }
- }
-
- static func findFirst(query: Keychain.Attributes) -> Result<Keychain.Attributes?> {
- return find(query: query).map { $0.first }
- }
-
- static func find(query: Keychain.Attributes) -> Result<[Keychain.Attributes]> {
- let attributes = query.keychainRepresentation()
-
- var result: CFTypeRef?
- let status = SecItemCopyMatching(attributes as CFDictionary, &result)
-
- return mapSecResultAndReturnValue(
- status: status,
- value: result,
- returnSet: query.return ?? [],
- limit: query.matchLimit ?? .one
- )
- }
-
- static private func mapSecResultAndReturnValue(
- status: OSStatus,
- value: CFTypeRef?,
- returnSet: Set<Keychain.Return>,
- limit: Keychain.MatchLimit) -> Result<[Keychain.Attributes]>
- {
- return mapSecResult(status: status) { () -> [Keychain.Attributes] in
- return value.map { parseReturnValue(value: $0, returnSet: returnSet, limit: limit) }
- ?? []
- }
- }
-
- static private func parseReturnValue(
- value: CFTypeRef,
- returnSet: Set<Keychain.Return>,
- limit: Keychain.MatchLimit) -> [Keychain.Attributes]
- {
- switch returnSet {
- case []:
- return []
-
- case [.data]:
- let values: [Data] = unsafelyCastReturnValue(value: value, limit: limit)
-
- return values.map { (data) -> Keychain.Attributes in
- var attributes = Keychain.Attributes()
- attributes.valueData = data
- return attributes
- }
-
- case [.persistentReference]:
- let values: [Data] = unsafelyCastReturnValue(value: value, limit: limit)
-
- return values.map { (persistentReference) -> Keychain.Attributes in
- var attributes = Keychain.Attributes()
- attributes.valuePersistentReference = persistentReference
- return attributes
- }
-
- default:
- let rawAttributeList: [[CFString: Any]] =
- unsafelyCastReturnValue(value: value, limit: limit)
-
- return rawAttributeList.map { Keychain.Attributes(attributes: $0) }
- }
- }
-
- /// A private helper that casts and normalizes the return value from Keychain to produce
- /// an array even when a single item is expected to be returned.
- static private func unsafelyCastReturnValue<T>(
- value: CFTypeRef,
- limit: Keychain.MatchLimit) -> [T]
- {
- switch limit {
- case .one:
- return [value as! T]
- case .all:
- return value as! [T]
- }
- }
-
- /// A private helper that verifies the given `status` and executes `body` on success
- static private func mapSecResult<T>(status: OSStatus, body: () -> T) -> Result<T> {
- if status == errSecSuccess {
- return .success(body())
- } else {
- return .failure(Keychain.Error(code: status))
- }
- }
-}
diff --git a/ios/MullvadVPN/Keychain/KeychainAttributes.swift b/ios/MullvadVPN/Keychain/KeychainAttributes.swift
deleted file mode 100644
index 397620f37b..0000000000
--- a/ios/MullvadVPN/Keychain/KeychainAttributes.swift
+++ /dev/null
@@ -1,139 +0,0 @@
-//
-// KeychainAttributes.swift
-// MullvadVPN
-//
-// Created by pronebird on 22/04/2020.
-// Copyright © 2020 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import Security
-
-extension Keychain {
-
- enum Accessible: RawRepresentable, CaseIterable, KeychainAttributeDecodable, KeychainAttributeEncodable {
-
- case whenPasscodeSetThisDeviceOnly
- case whenUnlocked
- case whenUnlockedThisDeviceOnly
- case afterFirstUnlock
- case afterFirstUnlockThisDeviceOnly
-
- var rawValue: CFString {
- switch self {
- case .whenPasscodeSetThisDeviceOnly:
- return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
- case .whenUnlocked:
- return kSecAttrAccessibleWhenUnlocked
- case .whenUnlockedThisDeviceOnly:
- return kSecAttrAccessibleWhenUnlockedThisDeviceOnly
- case .afterFirstUnlock:
- return kSecAttrAccessibleAfterFirstUnlock
- case .afterFirstUnlockThisDeviceOnly:
- return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
- }
- }
-
- init?(rawValue: CFString) {
- let maybeCase = Self.allCases.first { $0.rawValue == rawValue }
-
- if let maybeCase = maybeCase {
- self = maybeCase
- } else {
- return nil
- }
- }
-
- init?(attributes: [CFString: Any]) {
- if let rawValue = attributes[kSecAttrAccessible] as? String {
- self.init(rawValue: rawValue as CFString)
- } else {
- return nil
- }
- }
-
- func updateKeychainAttributes(in attributes: inout [CFString : Any]) {
- attributes[kSecAttrAccessible] = rawValue
- }
-
- }
-
- struct Attributes: KeychainAttributeEncodable, KeychainAttributeDecodable {
- var `class`: KeychainClass?
- var service: String?
- var account: String?
- var accessGroup: String?
- var accessible: Accessible?
- var creationDate: Date?
- var modificationDate: Date?
- var generic: Data?
-
- var valueData: Data?
- var valuePersistentReference: Data?
-
- var `return`: Set<Keychain.Return>?
- var matchLimit: Keychain.MatchLimit?
-
- init() {}
-
- init(attributes: [CFString: Any]) {
- `class` = KeychainClass(attributes: attributes)
- service = attributes[kSecAttrService] as? String
- account = attributes[kSecAttrAccount] as? String
- accessGroup = attributes[kSecAttrAccessGroup] as? String
- accessible = Accessible(attributes: attributes)
- creationDate = attributes[kSecAttrCreationDate] as? Date
- modificationDate = attributes[kSecAttrModificationDate] as? Date
- generic = attributes[kSecAttrGeneric] as? Data
-
- valueData = attributes[kSecValueData] as? Data
- valuePersistentReference = attributes[kSecValuePersistentRef] as? Data
-
- `return` = Set(attributes: attributes)
- matchLimit = Keychain.MatchLimit(attributes: attributes)
- }
-
- func updateKeychainAttributes(in attributes: inout [CFString: Any]) {
- `class`?.updateKeychainAttributes(in: &attributes)
-
- if let service = service {
- attributes[kSecAttrService] = service
- }
-
- if let account = account {
- attributes[kSecAttrAccount] = account
- }
-
- if let accessGroup = accessGroup {
- attributes[kSecAttrAccessGroup] = accessGroup
- }
-
- accessible?.updateKeychainAttributes(in: &attributes)
-
- if let creationDate = creationDate {
- attributes[kSecAttrCreationDate] = creationDate
- }
-
- if let modificationDate = modificationDate {
- attributes[kSecAttrModificationDate] = modificationDate
- }
-
- if let generic = generic {
- attributes[kSecAttrGeneric] = generic
- }
-
- if let valueData = valueData {
- attributes[kSecValueData] = valueData
- }
-
- if let valuePersistentReference = valuePersistentReference {
- attributes[kSecValuePersistentRef] = valuePersistentReference
- }
-
- `return`?.updateKeychainAttributes(in: &attributes)
- matchLimit?.updateKeychainAttributes(in: &attributes)
- }
-
- }
-
-}
diff --git a/ios/MullvadVPN/Keychain/KeychainClass.swift b/ios/MullvadVPN/Keychain/KeychainClass.swift
deleted file mode 100644
index 86e13f2bd6..0000000000
--- a/ios/MullvadVPN/Keychain/KeychainClass.swift
+++ /dev/null
@@ -1,50 +0,0 @@
-//
-// KeychainClass.swift
-// MullvadVPN
-//
-// Created by pronebird on 24/04/2020.
-// Copyright © 2020 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import Security
-
-extension Keychain {
-
- enum KeychainClass: RawRepresentable, CaseIterable, KeychainAttributeDecodable, KeychainAttributeEncodable {
- case genericPassword
- case internetPassword
-
- var rawValue: CFString {
- switch self {
- case .genericPassword:
- return kSecClassGenericPassword
- case .internetPassword:
- return kSecClassInternetPassword
- }
- }
-
- init?(rawValue: CFString) {
- let maybeCase = Self.allCases.first { $0.rawValue == rawValue }
-
- if let maybeCase = maybeCase {
- self = maybeCase
- } else {
- return nil
- }
- }
-
- init?(attributes: [CFString: Any]) {
- if let rawValue = attributes[kSecClass] as? String {
- self.init(rawValue: rawValue as CFString)
- } else {
- return nil
- }
- }
-
- func updateKeychainAttributes(in attributes: inout [CFString : Any]) {
- attributes[kSecClass] = rawValue
- }
- }
-
-}
diff --git a/ios/MullvadVPN/Keychain/KeychainError.swift b/ios/MullvadVPN/Keychain/KeychainError.swift
deleted file mode 100644
index 4758d08b66..0000000000
--- a/ios/MullvadVPN/Keychain/KeychainError.swift
+++ /dev/null
@@ -1,32 +0,0 @@
-//
-// KeychainError.swift
-// MullvadVPN
-//
-// Created by pronebird on 02/10/2019.
-// Copyright © 2019 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import Security
-
-extension Keychain {
- struct Error: Swift.Error, LocalizedError {
- let code: OSStatus
-
- var errorDescription: String? {
- return SecCopyErrorMessageString(code, nil) as String?
- }
- }
-}
-
-
-extension Keychain.Error {
-
- static let duplicateItem = Keychain.Error(code: errSecDuplicateItem)
- static let itemNotFound = Keychain.Error(code: errSecItemNotFound)
-
- static func ~= (lhs: Keychain.Error, rhs: Swift.Error) -> Bool {
- guard let rhsError = rhs as? Keychain.Error else { return false }
- return lhs.code == rhsError.code
- }
-}
diff --git a/ios/MullvadVPN/Keychain/KeychainMatchLimit.swift b/ios/MullvadVPN/Keychain/KeychainMatchLimit.swift
deleted file mode 100644
index f86b010532..0000000000
--- a/ios/MullvadVPN/Keychain/KeychainMatchLimit.swift
+++ /dev/null
@@ -1,48 +0,0 @@
-//
-// KeychainMatchLimit.swift
-// MullvadVPN
-//
-// Created by pronebird on 24/04/2020.
-// Copyright © 2020 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import Security
-
-extension Keychain {
- enum MatchLimit: RawRepresentable, CaseIterable, KeychainAttributeDecodable, KeychainAttributeEncodable {
- case one
- case all
-
- var rawValue: CFString {
- switch self {
- case .one:
- return kSecMatchLimitOne
- case .all:
- return kSecMatchLimitAll
- }
- }
-
- init?(rawValue: CFString) {
- let maybeCase = Self.allCases.first { $0.rawValue == rawValue }
-
- if let maybeCase = maybeCase {
- self = maybeCase
- } else {
- return nil
- }
- }
-
- init?(attributes: [CFString : Any]) {
- if let rawValue = attributes[kSecMatchLimit] as? String {
- self.init(rawValue: rawValue as CFString)
- } else {
- return nil
- }
- }
-
- func updateKeychainAttributes(in attributes: inout [CFString : Any]) {
- attributes[kSecMatchLimit] = rawValue
- }
- }
-}
diff --git a/ios/MullvadVPN/Keychain/KeychainReturn.swift b/ios/MullvadVPN/Keychain/KeychainReturn.swift
deleted file mode 100644
index 7216ffbd80..0000000000
--- a/ios/MullvadVPN/Keychain/KeychainReturn.swift
+++ /dev/null
@@ -1,57 +0,0 @@
-//
-// KeychainReturn.swift
-// MullvadVPN
-//
-// Created by pronebird on 24/04/2020.
-// Copyright © 2020 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import Security
-
-extension Keychain {
- enum Return: KeychainAttributeEncodable, CaseIterable {
- case data
- case attributes
- case persistentReference
-
- fileprivate var attributeKey: CFString {
- switch self {
- case .attributes:
- return kSecReturnAttributes
- case .data:
- return kSecReturnData
- case .persistentReference:
- return kSecReturnPersistentRef
- }
- }
-
- func updateKeychainAttributes(in attributes: inout [CFString: Any]) {
- attributes[attributeKey] = true
- }
- }
-}
-
-extension Set: KeychainAttributeDecodable, KeychainAttributeEncodable
- where Element == Keychain.Return
-{
- init?(attributes: [CFString: Any]) {
- let items = Keychain.Return.allCases.filter { (returnType) -> Bool in
- return attributes[returnType.attributeKey] as? Bool == .some(true)
- }
-
- if items.isEmpty {
- return nil
- } else {
- self.init(items)
- }
- }
-
- func updateKeychainAttributes(in attributes: inout [CFString : Any]) {
- Keychain.Return.allCases.forEach { (returnType) in
- attributes.removeValue(forKey: returnType.attributeKey)
- }
-
- forEach { $0.updateKeychainAttributes(in: &attributes) }
- }
-}
diff --git a/ios/MullvadVPN/KeychainError.swift b/ios/MullvadVPN/KeychainError.swift
new file mode 100644
index 0000000000..5ee831c83f
--- /dev/null
+++ b/ios/MullvadVPN/KeychainError.swift
@@ -0,0 +1,25 @@
+//
+// KeychainError.swift
+// MullvadVPN
+//
+// Created by pronebird on 02/10/2019.
+// Copyright © 2019 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Security
+
+struct KeychainError: LocalizedError, Equatable {
+ let code: OSStatus
+
+ var errorDescription: String? {
+ return SecCopyErrorMessageString(code, nil) as String?
+ }
+
+ static let duplicateItem = KeychainError(code: errSecDuplicateItem)
+ static let itemNotFound = KeychainError(code: errSecItemNotFound)
+
+ static func == (lhs: KeychainError, rhs: KeychainError) -> Bool {
+ return lhs.code == rhs.code
+ }
+}
diff --git a/ios/MullvadVPN/Logging/ChainedError+Logger.swift b/ios/MullvadVPN/Logging/ChainedError+Logger.swift
index a0f396767a..2d221c7132 100644
--- a/ios/MullvadVPN/Logging/ChainedError+Logger.swift
+++ b/ios/MullvadVPN/Logging/ChainedError+Logger.swift
@@ -10,15 +10,26 @@ import Foundation
import Logging
extension Logger {
-
- func error<T: ChainedError>(chainedError: T,
- message: @autoclosure () -> String? = nil,
- metadata: @autoclosure () -> Logger.Metadata? = nil,
- source: @autoclosure () -> String? = nil,
- file: String = #file, function: String = #function, line: UInt = #line)
+ func error<T: ChainedError>(
+ chainedError: T,
+ message: @autoclosure () -> String? = nil,
+ metadata: @autoclosure () -> Logger.Metadata? = nil,
+ source: @autoclosure () -> String? = nil,
+ file: String = #file,
+ function: String = #function,
+ line: UInt = #line
+ )
{
- let s = Message(stringLiteral: chainedError.displayChain(message: message()))
- log(level: .error, s, metadata: metadata(), source: source(), file: file, function: function, line: line)
+ log(
+ level: .error,
+ Message(
+ stringLiteral: chainedError.displayChain(message: message())
+ ),
+ metadata: metadata(),
+ source: source(),
+ file: file,
+ function: function,
+ line: line
+ )
}
-
}
diff --git a/ios/MullvadVPN/Logging/LogRotation.swift b/ios/MullvadVPN/Logging/LogRotation.swift
index 696f7c4a41..16d48b84df 100644
--- a/ios/MullvadVPN/Logging/LogRotation.swift
+++ b/ios/MullvadVPN/Logging/LogRotation.swift
@@ -24,36 +24,37 @@ enum LogRotation {
}
}
- static func rotateLog(logsDirectory: URL, logFileName: String) -> Result<(), Error> {
+ static func rotateLog(logsDirectory: URL, logFileName: String) throws {
let fileManager = FileManager.default
let source = logsDirectory.appendingPathComponent(logFileName)
let backup = source.deletingPathExtension().appendingPathExtension("old.log")
- return Result { _ = try fileManager.replaceItemAt(backup, withItemAt: source) }
- .mapError { (error) -> Error in
- // FileManager returns a very obscure error chain so we need to traverse it to find
- // the root cause of the error.
- var errorCursor: Swift.Error? = error
- let cocoaErrorIterator = AnyIterator { () -> CocoaError? in
- if let cocoaError = errorCursor as? CocoaError {
- errorCursor = cocoaError.underlying
- return cocoaError
- } else {
- errorCursor = nil
- return nil
- }
+ do {
+ _ = try fileManager.replaceItemAt(backup, withItemAt: source)
+ } catch {
+ // FileManager returns a very obscure error chain so we need to traverse it to find
+ // the root cause of the error.
+ var errorCursor: Swift.Error? = error
+ let cocoaErrorIterator = AnyIterator { () -> CocoaError? in
+ if let cocoaError = errorCursor as? CocoaError {
+ errorCursor = cocoaError.underlying
+ return cocoaError
+ } else {
+ errorCursor = nil
+ return nil
}
+ }
- while let fileError = cocoaErrorIterator.next() {
- // .fileNoSuchFile is returned when both backup and source log files do not exist
- // .fileReadNoSuchFile is returned when backup exists but source log file does not
- if fileError.code == .fileNoSuchFile || fileError.code == .fileReadNoSuchFile,
- fileError.url == source {
- return .noSourceLogFile
- }
+ while let fileError = cocoaErrorIterator.next() {
+ // .fileNoSuchFile is returned when both backup and source log files do not exist
+ // .fileReadNoSuchFile is returned when backup exists but source log file does not
+ if fileError.code == .fileNoSuchFile || fileError.code == .fileReadNoSuchFile,
+ fileError.url == source {
+ throw Error.noSourceLogFile
}
+ }
- return .moveSourceLogFile(error)
+ throw Error.moveSourceLogFile(error)
}
}
}
diff --git a/ios/MullvadVPN/Logging/Logging.swift b/ios/MullvadVPN/Logging/Logging.swift
index fe5b65b433..55451b677b 100644
--- a/ios/MullvadVPN/Logging/Logging.swift
+++ b/ios/MullvadVPN/Logging/Logging.swift
@@ -19,7 +19,15 @@ func initLoggingSystem(bundleIdentifier: String, metadata: Logger.Metadata? = ni
try? FileManager.default.createDirectory(at: logsDirectoryURL, withIntermediateDirectories: false, attributes: nil)
// Rotate log
- let logRotationResult = LogRotation.rotateLog(logsDirectory: logsDirectoryURL, logFileName: logFileName)
+ var logRotationError: Error?
+ do {
+ try LogRotation.rotateLog(
+ logsDirectory: logsDirectoryURL,
+ logFileName: logFileName
+ )
+ } catch {
+ logRotationError = error
+ }
// Create an array of log output streams
var streams: [TextOutputStream] = []
@@ -52,8 +60,10 @@ func initLoggingSystem(bundleIdentifier: String, metadata: Logger.Metadata? = ni
}
}
- if case .failure(let logRotationError) = logRotationResult {
- Logger(label: "LogRotation")
- .error(chainedError: logRotationError, message: "Failed to rotate log")
+ if let logRotationError = logRotationError {
+ Logger(label: "LogRotation").error(
+ chainedError: AnyChainedError(logRotationError),
+ message: "Failed to rotate log"
+ )
}
}
diff --git a/ios/MullvadVPN/LoginViewController.swift b/ios/MullvadVPN/LoginViewController.swift
index f345798941..cba1f78992 100644
--- a/ios/MullvadVPN/LoginViewController.swift
+++ b/ios/MullvadVPN/LoginViewController.swift
@@ -16,13 +16,22 @@ enum AuthenticationMethod {
enum LoginState {
case `default`
case authenticating(AuthenticationMethod)
- case failure(Account.Error)
+ case failure(TunnelManager.Error)
case success(AuthenticationMethod)
}
protocol LoginViewControllerDelegate: AnyObject {
- func loginViewController(_ controller: LoginViewController, loginWithAccountToken accountToken: String, completion: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void)
- func loginViewControllerLoginWithNewAccount(_ controller: LoginViewController, completion: @escaping (Result<REST.AccountResponse, Account.Error>) -> Void)
+ func loginViewController(
+ _ controller: LoginViewController,
+ loginWithAccountToken accountToken: String,
+ completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void
+ )
+
+ func loginViewControllerLoginWithNewAccount(
+ _ controller: LoginViewController,
+ completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void
+ )
+
func loginViewControllerDidLogin(_ controller: LoginViewController)
}
@@ -183,12 +192,14 @@ class LoginViewController: UIViewController, RootContainment {
let accountToken = contentView.accountInputGroup.parsedToken
beginLogin(method: .existingAccount)
- self.delegate?.loginViewController(self, loginWithAccountToken: accountToken, completion: { [weak self] (result) in
- switch result {
+ self.delegate?.loginViewController(self, loginWithAccountToken: accountToken, completion: { [weak self] completion in
+ switch completion {
case .success:
self?.endLogin(.success(.existingAccount))
case .failure(let error):
self?.endLogin(.failure(error))
+ case .cancelled:
+ self?.endLogin(.default)
}
})
}
@@ -199,13 +210,15 @@ class LoginViewController: UIViewController, RootContainment {
contentView.accountInputGroup.clearToken()
updateKeyboardToolbar()
- self.delegate?.loginViewControllerLoginWithNewAccount(self, completion: { [weak self] (result) in
- switch result {
- case .success(let response):
- self?.contentView.accountInputGroup.setToken(response.token)
+ self.delegate?.loginViewControllerLoginWithNewAccount(self, completion: { [weak self] completion in
+ switch completion {
+ case .success(let accountData):
+ self?.contentView.accountInputGroup.setToken(accountData?.number ?? "")
self?.endLogin(.success(.newAccount))
case .failure(let error):
self?.endLogin(.failure(error))
+ case .cancelled:
+ self?.endLogin(.default)
}
})
}
@@ -341,43 +354,7 @@ private extension LoginState {
}
case .failure(let error):
- let localizedUnknownInternalError = NSLocalizedString(
- "SUBHEAD_TITLE_INTERNAL_ERROR",
- tableName: "Login",
- comment: "Subhead displayed in the event of internal error."
- )
-
- switch error {
- case .createAccount(let rpcError), .verifyAccount(let rpcError):
- return rpcError.errorChainDescription ?? ""
- case .tunnelConfiguration(let error):
- if case .pushWireguardKey(let pushError) = error {
- switch pushError {
- case .network(let urlError):
- return String(
- format: NSLocalizedString(
- "SUBHEAD_TITLE_NETWORK_ERROR_FORMAT",
- tableName: "Login",
- value: "Network error: %@",
- comment: "Subhead displayed in the event of network error. Use %@ placeholder to place localized text describing network failure."
- ),
- urlError.localizedDescription
- )
-
- case .unhandledResponse(_, let serverError):
- return serverError?.detail ?? NSLocalizedString(
- "SUBHEAD_TITLE_UNKNOWN_SERVER_ERROR",
- tableName: "Login",
- comment: "Subhead displayed in the event of unknown server error."
- )
-
- case .createURLRequest, .decodeResponse:
- return localizedUnknownInternalError
- }
- } else {
- return localizedUnknownInternalError
- }
- }
+ return error.errorChainDescription ?? ""
case .success(let method):
switch method {
diff --git a/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift b/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift
index c0e89ce1a1..4fd9aacdef 100644
--- a/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift
+++ b/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift
@@ -9,26 +9,26 @@
import Foundation
import UserNotifications
-let kAccountExpiryNotificationIdentifier = "net.mullvad.MullvadVPN.AccountExpiryNotification"
-let kAccountExpiryDefaultTriggerInterval = 3
+let accountExpiryNotificationIdentifier = "net.mullvad.MullvadVPN.AccountExpiryNotification"
+let accountExpiryDefaultTriggerInterval = 3
-class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificationProvider, InAppNotificationProvider, AccountObserver {
+class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificationProvider, InAppNotificationProvider, TunnelObserver {
private var accountExpiry: Date?
/// Interval prior to expiry used to calculate when to trigger notifications.
private let triggerInterval: Int
override var identifier: String {
- return kAccountExpiryNotificationIdentifier
+ return accountExpiryNotificationIdentifier
}
- init(triggerInterval: Int = kAccountExpiryDefaultTriggerInterval) {
+ init(triggerInterval: Int = accountExpiryDefaultTriggerInterval) {
self.triggerInterval = triggerInterval
super.init()
- accountExpiry = Account.shared.expiry
- Account.shared.addObserver(self)
+ accountExpiry = TunnelManager.shared.accountExpiry
+ TunnelManager.shared.addObserver(self)
}
private var trigger: UNNotificationTrigger? {
@@ -70,7 +70,7 @@ class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificatio
content.sound = UNNotificationSound.default
return UNNotificationRequest(
- identifier: kAccountExpiryNotificationIdentifier,
+ identifier: accountExpiryNotificationIdentifier,
content: content,
trigger: trigger
)
@@ -122,18 +122,18 @@ class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificatio
)
}
- func account(_ account: Account, didUpdateExpiry expiry: Date) {
- self.accountExpiry = expiry
- invalidate()
+ // MARK: - TunnelObserver
+
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) {
+ // no-op
}
- func account(_ account: Account, didLoginWithToken token: String, expiry: Date) {
- self.accountExpiry = expiry
- invalidate()
+ func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) {
+ // no-op
}
- func accountDidLogout(_ account: Account) {
- self.accountExpiry = nil
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) {
+ accountExpiry = tunnelSettings?.account.expiry
invalidate()
}
diff --git a/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift b/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift
index 85f0de05ad..afc6e5aa72 100644
--- a/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift
+++ b/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift
@@ -42,7 +42,7 @@ class TunnelErrorNotificationProvider: NotificationProvider, InAppNotificationPr
invalidate()
}
- func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelInfo: TunnelInfo?) {
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) {
// no-op
}
diff --git a/ios/MullvadVPN/PreferencesViewController.swift b/ios/MullvadVPN/PreferencesViewController.swift
index a5cc0dc06c..6d21afeea8 100644
--- a/ios/MullvadVPN/PreferencesViewController.swift
+++ b/ios/MullvadVPN/PreferencesViewController.swift
@@ -43,7 +43,7 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
TunnelManager.shared.addObserver(self)
- if let dnsSettings = TunnelManager.shared.tunnelInfo?.tunnelSettings.interface.dnsSettings {
+ if let dnsSettings = TunnelManager.shared.tunnelSettings?.dnsSettings {
dataSource.update(from: dnsSettings)
}
}
@@ -85,8 +85,8 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
// no-op
}
- func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelInfo: TunnelInfo?) {
- guard let dnsSettings = tunnelInfo?.tunnelSettings.interface.dnsSettings else { return }
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) {
+ guard let dnsSettings = tunnelSettings?.dnsSettings else { return }
dataSource.update(from: dnsSettings)
}
diff --git a/ios/MullvadVPN/PrivateKeyWithMetadata.swift b/ios/MullvadVPN/PrivateKeyWithMetadata.swift
deleted file mode 100644
index 04e73f784c..0000000000
--- a/ios/MullvadVPN/PrivateKeyWithMetadata.swift
+++ /dev/null
@@ -1,100 +0,0 @@
-//
-// PrivateKeyWithMetadata.swift
-// MullvadVPN
-//
-// Created by pronebird on 20/06/2019.
-// Copyright © 2019 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import class WireGuardKitTypes.PrivateKey
-import class WireGuardKitTypes.PublicKey
-
-/// A struct holding a private WireGuard key with associated metadata
-struct PrivateKeyWithMetadata: Equatable {
-
- /// When the key was created
- let creationDate: Date
-
- /// Private key
- let privateKey: PrivateKey
-
- /// Public key metadata
- var publicKeyWithMetadata: PublicKeyWithMetadata {
- return PublicKeyWithMetadata(publicKey: privateKey.publicKey, createdAt: creationDate)
- }
-
- /// Public key
- var publicKey: PublicKey {
- return privateKey.publicKey
- }
-
- /// Initialize the new private key
- init() {
- privateKey = PrivateKey()
- creationDate = Date()
- }
-
- /// Initialize with the existing private key
- init(privateKey: PrivateKey, createdAt: Date) {
- self.privateKey = privateKey
- creationDate = createdAt
- }
-
-}
-
-/// A struct holding a public WireGuard key with associated metadata
-struct PublicKeyWithMetadata: Equatable {
- /// Refers to private key creation date
- let creationDate: Date
-
- /// Public key
- let publicKey: PublicKey
-
- init(publicKey: PublicKey, createdAt: Date) {
- self.publicKey = publicKey
- creationDate = createdAt
- }
-
- /// Returns a base64 encoded string representation that can be used for displaying the key in
- /// the user interface
- func stringRepresentation(maxLength: Int? = nil) -> String {
- let base64EncodedKey = publicKey.base64Key
-
- if let maxLength = maxLength, maxLength < base64EncodedKey.count {
- return base64EncodedKey.prefix(maxLength) + "..."
- } else {
- return base64EncodedKey
- }
- }
-}
-
-extension PrivateKeyWithMetadata: Codable {
-
- private enum CodingKeys: String, CodingKey {
- case privateKeyData, creationDate
- }
-
- func encode(to encoder: Encoder) throws {
- var container = encoder.container(keyedBy: CodingKeys.self)
-
- try container.encode(privateKey.rawValue, forKey: .privateKeyData)
- try container.encode(creationDate, forKey: .creationDate)
- }
-
- init(from decoder: Decoder) throws {
- let container = try decoder.container(keyedBy: CodingKeys.self)
- let privateKeyBytes = try container.decode(Data.self, forKey: .privateKeyData)
-
- guard let privateKey = PrivateKey(rawValue: privateKeyBytes) else {
- throw DecodingError.dataCorruptedError(
- forKey: CodingKeys.privateKeyData,
- in: container,
- debugDescription: "Invalid key data"
- )
- }
-
- self.privateKey = privateKey
- self.creationDate = try container.decode(Date.self, forKey: .creationDate)
- }
-}
diff --git a/ios/MullvadVPN/ProblemReportViewController.swift b/ios/MullvadVPN/ProblemReportViewController.swift
index b9311174e7..47addb7589 100644
--- a/ios/MullvadVPN/ProblemReportViewController.swift
+++ b/ios/MullvadVPN/ProblemReportViewController.swift
@@ -19,7 +19,7 @@ class ProblemReportViewController: UIViewController, UITextFieldDelegate, Condit
let securityGroupIdentifier = ApplicationConfiguration.securityGroupIdentifier
// TODO: make sure we redact old tokens
- let redactStrings = Account.shared.token.flatMap { [$0] } ?? []
+ let redactStrings = TunnelManager.shared.accountNumber.flatMap { [$0] } ?? []
let report = ConsolidatedApplicationLog(
redactCustomStrings: redactStrings,
diff --git a/ios/MullvadVPN/REST/RESTAPIProxy.swift b/ios/MullvadVPN/REST/RESTAPIProxy.swift
index 1271422be1..1d7f259b4b 100644
--- a/ios/MullvadVPN/REST/RESTAPIProxy.swift
+++ b/ios/MullvadVPN/REST/RESTAPIProxy.swift
@@ -25,33 +25,6 @@ extension REST {
)
}
- func createAccount(
- retryStrategy: REST.RetryStrategy,
- completionHandler: @escaping CompletionHandler<AccountResponse>
- ) -> Cancellable
- {
- let requestHandler = AnyRequestHandler { endpoint in
- return try self.requestFactory.createRequest(
- endpoint: endpoint,
- method: .post,
- pathTemplate: "accounts"
- )
- }
-
- let responseHandler = REST.defaultResponseHandler(
- decoding: AccountResponse.self,
- with: responseDecoder
- )
-
- return addOperation(
- name: "create-account",
- retryStrategy: retryStrategy,
- requestHandler: requestHandler,
- responseHandler: responseHandler,
- completionHandler: completionHandler
- )
- }
-
func getAddressList(
retryStrategy: REST.RetryStrategy,
completionHandler: @escaping CompletionHandler<[AnyIPEndpoint]>
@@ -136,205 +109,6 @@ extension REST {
)
}
- func getAccountExpiry(
- accountNumber: String,
- retryStrategy: REST.RetryStrategy,
- completionHandler: @escaping CompletionHandler<AccountResponse>
- ) -> Cancellable
- {
- let requestHandler = AnyRequestHandler { endpoint in
- var requestBuilder = try self.requestFactory
- .createRequestBuilder(
- endpoint: endpoint,
- method: .get,
- pathTemplate: "me"
- )
- requestBuilder.setAuthorization(.accountNumber(accountNumber))
-
- return requestBuilder.getRequest()
- }
-
- let responseHandler = REST.defaultResponseHandler(
- decoding: AccountResponse.self,
- with: responseDecoder
- )
-
- return addOperation(
- name: "get-account-expiry",
- retryStrategy: retryStrategy,
- requestHandler: requestHandler,
- responseHandler: responseHandler,
- completionHandler: completionHandler
- )
- }
-
- func getWireguardKey(
- accountNumber: String,
- publicKey: PublicKey,
- retryStrategy: REST.RetryStrategy,
- completionHandler: @escaping CompletionHandler<WireguardAddressesResponse>
- ) -> Cancellable
- {
- let requestHandler = AnyRequestHandler { endpoint in
- var path: URLPathTemplate = "wireguard-keys/{pubkey}"
- try path.addPercentEncodedReplacement(
- name: "pubkey",
- value: publicKey.base64Key,
- allowedCharacters: .alphanumerics
- )
-
- var requestBuilder = try self.requestFactory
- .createRequestBuilder(
- endpoint: endpoint,
- method: .get,
- pathTemplate: path
- )
- requestBuilder.setAuthorization(.accountNumber(accountNumber))
-
- return requestBuilder.getRequest()
- }
-
- let responseHandler = REST.defaultResponseHandler(
- decoding: WireguardAddressesResponse.self,
- with: responseDecoder
- )
-
- return addOperation(
- name: "get-wireguard-key",
- retryStrategy: retryStrategy,
- requestHandler: requestHandler,
- responseHandler: responseHandler,
- completionHandler: completionHandler
- )
- }
-
- func pushWireguardKey(
- accountNumber: String,
- publicKey: PublicKey,
- retryStrategy: REST.RetryStrategy,
- completionHandler: @escaping CompletionHandler<WireguardAddressesResponse>
- ) -> Cancellable
- {
- let requestHandler = AnyRequestHandler { endpoint in
- var requestBuilder = try self.requestFactory.createRequestBuilder(
- endpoint: endpoint,
- method: .post,
- pathTemplate: "wireguard-keys"
- )
- requestBuilder.setAuthorization(.accountNumber(accountNumber))
-
- let body = PushWireguardKeyRequest(
- pubkey: publicKey.rawValue
- )
-
- try requestBuilder.setHTTPBody(value: body)
-
- return requestBuilder.getRequest()
- }
-
- let responseHandler = REST.defaultResponseHandler(
- decoding: WireguardAddressesResponse.self,
- with: responseDecoder
- )
-
- return addOperation(
- name: "push-wireguard-key",
- retryStrategy: retryStrategy,
- requestHandler: requestHandler,
- responseHandler: responseHandler,
- completionHandler: completionHandler
- )
- }
-
- func replaceWireguardKey(
- accountNumber: String,
- oldPublicKey: PublicKey,
- newPublicKey: PublicKey,
- retryStrategy: REST.RetryStrategy,
- completionHandler: @escaping CompletionHandler<WireguardAddressesResponse>
- ) -> Cancellable
- {
- let requestHandler = AnyRequestHandler { endpoint in
- var requestBuilder = try self.requestFactory.createRequestBuilder(
- endpoint: endpoint,
- method: .post,
- pathTemplate: "replace-wireguard-key"
- )
- requestBuilder.setAuthorization(.accountNumber(accountNumber))
-
- let body = ReplaceWireguardKeyRequest(
- old: oldPublicKey.rawValue,
- new: newPublicKey.rawValue
- )
-
- try requestBuilder.setHTTPBody(value: body)
-
- return requestBuilder.getRequest()
- }
-
- let responseHandler = REST.defaultResponseHandler(
- decoding: WireguardAddressesResponse.self,
- with: responseDecoder
- )
-
- return addOperation(
- name: "replace-wireguard-key",
- retryStrategy: retryStrategy,
- requestHandler: requestHandler,
- responseHandler: responseHandler,
- completionHandler: completionHandler
- )
- }
-
- func deleteWireguardKey(
- accountNumber: String,
- publicKey: PublicKey,
- retryStrategy: REST.RetryStrategy,
- completionHandler: @escaping CompletionHandler<Void>
- ) -> Cancellable
- {
- let requestHandler = AnyRequestHandler { endpoint in
- var path: URLPathTemplate = "wireguard-keys/{pubkey}"
-
- try path.addPercentEncodedReplacement(
- name: "pubkey",
- value: publicKey.base64Key,
- allowedCharacters: .alphanumerics
- )
-
- var requestBuilder = try self.requestFactory
- .createRequestBuilder(
- endpoint: endpoint,
- method: .delete,
- pathTemplate: path
- )
- requestBuilder.setAuthorization(.accountNumber(accountNumber))
-
- return requestBuilder.getRequest()
- }
-
- let responseHandler = AnyResponseHandler { response, data -> ResponseHandlerResult<Void> in
- if HTTPStatus.isSuccess(response.statusCode) {
- return .success(())
- } else {
- return .unhandledResponse(
- try? self.responseDecoder.decode(
- ServerErrorResponse.self,
- from: data
- )
- )
- }
- }
-
- return addOperation(
- name: "delete-wireguard-key",
- retryStrategy: retryStrategy,
- requestHandler: requestHandler,
- responseHandler: responseHandler,
- completionHandler: completionHandler
- )
- }
-
func createApplePayment(
accountNumber: String,
receiptString: Data,
@@ -434,32 +208,11 @@ extension REST {
// MARK: - Response types
- struct AccountResponse: Decodable {
- let token: String
- let expires: Date
- }
-
enum ServerRelaysCacheResponse {
case notModified
case newContent(_ etag: String?, _ value: ServerRelaysResponse)
}
- struct WireguardAddressesResponse: Decodable {
- let id: String
- let pubkey: Data
- let ipv4Address: IPAddressRange
- let ipv6Address: IPAddressRange
- }
-
- fileprivate struct PushWireguardKeyRequest: Encodable {
- let pubkey: Data
- }
-
- fileprivate struct ReplaceWireguardKeyRequest: Encodable {
- let old: Data
- let new: Data
- }
-
fileprivate struct CreateApplePaymentRequest: Encodable {
let receiptString: Data
}
diff --git a/ios/MullvadVPN/REST/RESTError.swift b/ios/MullvadVPN/REST/RESTError.swift
index 3b6f24c49a..e275a9ab34 100644
--- a/ios/MullvadVPN/REST/RESTError.swift
+++ b/ios/MullvadVPN/REST/RESTError.swift
@@ -73,6 +73,7 @@ extension REST {
static let publicKeyInUse = ServerResponseCode(rawValue: "PUBKEY_IN_USE")
static let maxDevicesReached = ServerResponseCode(rawValue: "MAX_DEVICES_REACHED")
static let invalidAccessToken = ServerResponseCode(rawValue: "INVALID_ACCESS_TOKEN")
+ static let deviceNotFound = ServerResponseCode(rawValue: "DEVICE_NOT_FOUND")
let rawValue: String
init(rawValue: String) {
diff --git a/ios/MullvadVPN/REST/RESTRetryStrategy.swift b/ios/MullvadVPN/REST/RESTRetryStrategy.swift
index 29732ef8a8..c70e3bfc70 100644
--- a/ios/MullvadVPN/REST/RESTRetryStrategy.swift
+++ b/ios/MullvadVPN/REST/RESTRetryStrategy.swift
@@ -13,10 +13,13 @@ extension REST {
var maxRetryCount: Int
var retryDelay: DispatchTimeInterval
- /// Strategy configured to never retry
+ /// Strategy configured to never retry.
static var noRetry = RetryStrategy(maxRetryCount: 0, retryDelay: .never)
- /// Startegy configured with 3 retry attempts with 2 seconds delay between
+ /// Startegy configured with 3 retry attempts with 2 seconds delay between.
static var `default` = RetryStrategy(maxRetryCount: 3, retryDelay: .seconds(2))
+
+ /// Strategy configured with 10 retry attempts with 2 seconds delay between.
+ static var aggressive = RetryStrategy(maxRetryCount: 10, retryDelay: .seconds(2))
}
}
diff --git a/ios/MullvadVPN/RelayCache/RelayCacheError.swift b/ios/MullvadVPN/RelayCache/RelayCacheError.swift
index 9e18643a21..fe75cf2d4e 100644
--- a/ios/MullvadVPN/RelayCache/RelayCacheError.swift
+++ b/ios/MullvadVPN/RelayCache/RelayCacheError.swift
@@ -19,7 +19,6 @@ extension RelayCache {
case encodeCache(Swift.Error)
case decodeCache(Swift.Error)
case rest(REST.Error)
- case backgroundTaskScheduler(Swift.Error)
var errorDescription: String? {
switch self {
@@ -37,8 +36,6 @@ extension RelayCache {
return "Write cache error."
case .rest:
return "REST error."
- case .backgroundTaskScheduler:
- return "Background task scheduler error."
}
}
}
diff --git a/ios/MullvadVPN/RelayCache/RelayCacheIO.swift b/ios/MullvadVPN/RelayCache/RelayCacheIO.swift
index fc9a13dd41..ccd623d25a 100644
--- a/ios/MullvadVPN/RelayCache/RelayCacheIO.swift
+++ b/ios/MullvadVPN/RelayCache/RelayCacheIO.swift
@@ -26,17 +26,21 @@ extension RelayCache.IO {
}
/// Safely read the cache file from disk using file coordinator.
- static func read(cacheFileURL: URL) -> Result<RelayCache.CachedRelays, RelayCache.Error> {
+ static func read(cacheFileURL: URL) throws -> RelayCache.CachedRelays {
var result: Result<RelayCache.CachedRelays, RelayCache.Error>?
let fileCoordinator = NSFileCoordinator(filePresenter: nil)
let accessor = { (fileURLForReading: URL) -> Void in
// Decode data from disk
- result = Result { try Data(contentsOf: fileURLForReading) }
- .mapError { RelayCache.Error.readCache($0) }
- .flatMap { (data) in
- Result { try JSONDecoder().decode(RelayCache.CachedRelays.self, from: data) }
- .mapError { RelayCache.Error.decodeCache($0) }
+ do {
+ let data = try Data(contentsOf: fileURLForReading)
+ let relays = try JSONDecoder().decode(RelayCache.CachedRelays.self, from: data)
+
+ result = .success(relays)
+ } catch let error as DecodingError {
+ result = .failure(.decodeCache(error))
+ } catch {
+ result = .failure(.readCache(error))
}
}
@@ -50,50 +54,57 @@ extension RelayCache.IO {
result = .failure(.readCache(error))
}
- return result!
+ 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) -> Result<RelayCache.CachedRelays, RelayCache.Error> {
- return Self.read(cacheFileURL: cacheFileURL)
- .flatMapError { (error) -> Result<RelayCache.CachedRelays, RelayCache.Error> in
- switch error {
- case .decodeCache, .readCache(CocoaError.fileReadNoSuchFile):
- return RelayCache.IO.readPrebundledRelays(fileURL: preBundledRelaysFileURL)
- default:
- return .failure(error)
- }
+ static func readWithFallback(cacheFileURL: URL, preBundledRelaysFileURL: URL) throws -> RelayCache.CachedRelays {
+ do {
+ return try Self.read(cacheFileURL: cacheFileURL)
+ } catch {
+ let error = error as! RelayCache.Error
+
+ switch error {
+ case .decodeCache, .readCache(CocoaError.fileReadNoSuchFile):
+ return try RelayCache.IO.readPrebundledRelays(fileURL: preBundledRelaysFileURL)
+ default:
+ throw error
}
+ }
}
/// Read pre-bundled relays file from disk.
- static func readPrebundledRelays(fileURL: URL) -> Result<RelayCache.CachedRelays, RelayCache.Error> {
- return Result { try Data(contentsOf: fileURL) }
- .mapError { RelayCache.Error.readPrebundledRelays($0) }
- .flatMap { (data) -> Result<RelayCache.CachedRelays, RelayCache.Error> in
- return Result { try REST.Coding.makeJSONDecoder().decode(REST.ServerRelaysResponse.self, from: data) }
- .mapError { RelayCache.Error.decodePrebundledRelays($0) }
- .map { (relays) -> RelayCache.CachedRelays in
- return RelayCache.CachedRelays(
- relays: relays,
- updatedAt: Date(timeIntervalSince1970: 0)
- )
- }
+ static func readPrebundledRelays(fileURL: URL) throws -> RelayCache.CachedRelays {
+ do {
+ let data = try Data(contentsOf: fileURL)
+ let relays = try REST.Coding.makeJSONDecoder()
+ .decode(REST.ServerRelaysResponse.self, from: data)
+
+ return RelayCache.CachedRelays(
+ relays: relays,
+ updatedAt: Date(timeIntervalSince1970: 0)
+ )
+ } catch let error as DecodingError {
+ throw RelayCache.Error.decodePrebundledRelays(error)
+ } catch {
+ throw RelayCache.Error.readPrebundledRelays(error)
}
}
/// Safely write the cache file on disk using file coordinator.
- static func write(cacheFileURL: URL, record: RelayCache.CachedRelays) -> Result<(), RelayCache.Error> {
- var result: Result<(), RelayCache.Error>?
+ static func write(cacheFileURL: URL, record: RelayCache.CachedRelays) throws {
+ var resultError: RelayCache.Error?
let fileCoordinator = NSFileCoordinator(filePresenter: nil)
let accessor = { (fileURLForWriting: URL) -> Void in
- result = Result { try JSONEncoder().encode(record) }
- .mapError { RelayCache.Error.encodeCache($0) }
- .flatMap { (data) in
- Result { try data.write(to: fileURLForWriting) }
- .mapError { RelayCache.Error.writeCache($0) }
+ do {
+ let data = try JSONEncoder().encode(record)
+ try data.write(to: fileURLForWriting)
+ } catch let error as EncodingError {
+ resultError = .encodeCache(error)
+ } catch {
+ resultError = .writeCache(error)
}
}
@@ -103,10 +114,8 @@ extension RelayCache.IO {
error: &error,
byAccessor: accessor)
- if let error = error {
- result = .failure(.writeCache(error))
+ if let resultError = resultError {
+ throw resultError
}
-
- return result!
}
}
diff --git a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
index a07ac38b65..025731fe23 100644
--- a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
+++ b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
@@ -70,15 +70,20 @@ extension RelayCache {
self.isPeriodicUpdatesEnabled = true
- switch RelayCache.IO.read(cacheFileURL: self.cacheFileURL) {
- case .success(let cachedRelays):
- let nextUpdate = cachedRelays.updatedAt.addingTimeInterval(Self.relayUpdateInterval)
- self.scheduleRepeatingTimer(startTime: .now() + nextUpdate.timeIntervalSinceNow)
+ do {
+ let cachedRelays = try RelayCache.IO.read(cacheFileURL: self.cacheFileURL)
+ let nextUpdate = cachedRelays.updatedAt
+ .addingTimeInterval(Self.relayUpdateInterval)
- case .failure(let readError):
- self.logger.error(chainedError: readError, message: "Failed to read the relay cache.")
+ self.scheduleRepeatingTimer(startTime: .now() + nextUpdate.timeIntervalSinceNow)
+ } catch {
+ self.logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to read the relay cache."
+ )
- if Self.shouldDownloadRelaysOnReadFailure(readError) {
+ if let readError = error as? RelayCache.Error,
+ Self.shouldDownloadRelaysOnReadFailure(readError) {
self.scheduleRepeatingTimer(startTime: .now())
}
}
@@ -98,7 +103,11 @@ extension RelayCache {
}
}
- func updateRelays(completionHandler: @escaping (OperationCompletion<RelayCache.FetchResult, RelayCache.Error>) -> Void) -> Cancellable {
+ func updateRelays(
+ completionHandler: @escaping (
+ OperationCompletion<RelayCache.FetchResult, RelayCache.Error>
+ ) -> Void
+ ) -> Cancellable {
let operation = UpdateRelaysOperation(
dispatchQueue: stateQueue,
apiProxy: REST.ProxyFactory.shared.createAPIProxy(),
@@ -131,20 +140,24 @@ extension RelayCache {
func read(completionHandler: @escaping (Result<CachedRelays, RelayCache.Error>) -> Void) {
stateQueue.async {
- let result = RelayCache.IO.readWithFallback(
- cacheFileURL: self.cacheFileURL,
- preBundledRelaysFileURL: self.prebundledRelaysFileURL
- )
+ let result = Result {
+ try RelayCache.IO.readWithFallback(
+ cacheFileURL: self.cacheFileURL,
+ preBundledRelaysFileURL: self.prebundledRelaysFileURL
+ )
+ }.mapError { error in
+ return error as! RelayCache.Error
+ }
completionHandler(result)
}
}
- func readAndWait() -> Result<CachedRelays, RelayCache.Error> {
- return stateQueue.sync {
- return RelayCache.IO.readWithFallback(
- cacheFileURL: self.cacheFileURL,
- preBundledRelaysFileURL: self.prebundledRelaysFileURL
+ func readAndWait() throws -> CachedRelays {
+ return try stateQueue.sync {
+ return try RelayCache.IO.readWithFallback(
+ cacheFileURL: cacheFileURL,
+ preBundledRelaysFileURL: prebundledRelaysFileURL
)
}
}
@@ -241,38 +254,37 @@ extension RelayCache.Tracker {
}
/// Schedules app refresh task relative to the last relays update.
- func scheduleAppRefreshTask() -> Result<(), RelayCache.Error> {
- return readAndWait().flatMap { cachedRelays in
- let beginDate = cachedRelays.updatedAt.addingTimeInterval(Self.relayUpdateInterval)
+ func scheduleAppRefreshTask() throws {
+ let cachedRelays = try readAndWait()
+ let beginDate = cachedRelays.updatedAt.addingTimeInterval(Self.relayUpdateInterval)
- return self.submitAppRefreshTask(at: beginDate)
- }
+ try submitAppRefreshTask(at: beginDate)
}
/// Create and submit task request to scheduler.
- private func submitAppRefreshTask(at beginDate: Date) -> Result<(), RelayCache.Error> {
+ private func submitAppRefreshTask(at beginDate: Date) throws {
let taskIdentifier = ApplicationConfiguration.appRefreshTaskIdentifier
let request = BGAppRefreshTaskRequest(identifier: taskIdentifier)
request.earliestBeginDate = beginDate
- return Result { try BGTaskScheduler.shared.submit(request) }
- .mapError { error in
- return .backgroundTaskScheduler(error)
- }
+ try BGTaskScheduler.shared.submit(request)
}
/// Background task handler
private func handleAppRefreshTask(_ task: BGAppRefreshTask) {
logger.debug("Start app refresh task.")
- let cancellable = self.updateRelays { completion in
+ let cancellable = updateRelays { completion in
switch completion {
case .success(let fetchResult):
self.logger.debug("Finished updating relays in app refresh task: \(fetchResult).")
case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to update relays in app refresh task.")
+ self.logger.error(
+ chainedError: error,
+ message: "Failed to update relays in app refresh task."
+ )
case .cancelled:
self.logger.debug("App refresh task was cancelled.")
@@ -287,13 +299,15 @@ extension RelayCache.Tracker {
// Schedule next refresh
let scheduleDate = Date(timeIntervalSinceNow: Self.relayUpdateInterval)
+ do {
+ try submitAppRefreshTask(at: scheduleDate)
- switch self.submitAppRefreshTask(at: scheduleDate) {
- case .success:
logger.debug("Scheduled next app refresh task at \(scheduleDate.logFormatDate()).")
-
- case .failure(let error):
- logger.error(chainedError: error, message: "Failed to schedule next app refresh task.")
+ } catch {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to schedule next app refresh task."
+ )
}
}
}
@@ -308,7 +322,7 @@ fileprivate class UpdateRelaysOperation: ResultOperation<RelayCache.FetchResult,
private let logger = Logger(label: "RelayCacheTracker.UpdateRelaysOperation")
private let updateHandler: UpdateHandler
- private var downloadCancellable: Cancellable?
+ private var downloadTask: Cancellable?
init(
dispatchQueue: DispatchQueue,
@@ -332,32 +346,34 @@ fileprivate class UpdateRelaysOperation: ResultOperation<RelayCache.FetchResult,
}
override func main() {
- let readResult = RelayCache.IO.read(cacheFileURL: self.cacheFileURL)
-
- switch readResult {
- case .success(let cachedRelays):
- let nextUpdate = cachedRelays.updatedAt.addingTimeInterval(self.relayUpdateInterval)
+ do {
+ let cachedRelays = try RelayCache.IO.read(cacheFileURL: cacheFileURL)
+ let nextUpdate = cachedRelays.updatedAt.addingTimeInterval(relayUpdateInterval)
if nextUpdate <= Date() {
- self.downloadRelays(previouslyCachedRelays: cachedRelays)
+ downloadRelays(previouslyCachedRelays: cachedRelays)
} else {
- self.finish(completion: .success(.throttled))
+ finish(completion: .success(.throttled))
}
+ } catch {
+ let error = error as! RelayCache.Error
- case .failure(let readError):
- self.logger.error(chainedError: readError, message: "Failed to read the relay cache to determine if it needs to be updated.")
+ logger.error(
+ chainedError: error,
+ message: "Failed to read the relay cache to determine if it needs to be updated."
+ )
- if self.shouldDownloadRelaysOnReadFailure(readError) {
- self.downloadRelays(previouslyCachedRelays: nil)
+ if shouldDownloadRelaysOnReadFailure(error) {
+ downloadRelays(previouslyCachedRelays: nil)
} else {
- self.finish(completion: .failure(readError))
+ finish(completion: .failure(error))
}
}
}
override func operationDidCancel() {
- downloadCancellable?.cancel()
- downloadCancellable = nil
+ downloadTask?.cancel()
+ downloadTask = nil
}
private func didReceiveNewRelays(etag: String?, relays: REST.ServerRelaysResponse) {
@@ -365,38 +381,49 @@ fileprivate class UpdateRelaysOperation: ResultOperation<RelayCache.FetchResult,
logger.info("Downloaded \(numRelays) relays.")
- let cachedRelays = RelayCache.CachedRelays(etag: etag, relays: relays, updatedAt: Date())
- let writeResult = RelayCache.IO.write(cacheFileURL: cacheFileURL, record: cachedRelays)
+ let cachedRelays = RelayCache.CachedRelays(
+ etag: etag,
+ relays: relays,
+ updatedAt: Date()
+ )
+
+ do {
+ try RelayCache.IO.write(cacheFileURL: cacheFileURL, record: cachedRelays)
- switch writeResult {
- case .success:
updateHandler(cachedRelays)
finish(completion: .success(.newContent))
+ } catch {
+ let error = error as! RelayCache.Error
- case .failure(let error):
- logger.error(chainedError: error, message: "Failed to store downloaded relays.")
+ logger.error(
+ chainedError: error,
+ message: "Failed to store downloaded relays."
+ )
- finish(completion: .failure(.writeCache(error)))
+ finish(completion: .failure(error))
}
}
private func didReceiveNotModified(previouslyCachedRelays: RelayCache.CachedRelays) {
- logger.info("Relays haven't changed since last check.")
-
var cachedRelays = previouslyCachedRelays
cachedRelays.updatedAt = Date()
- let writeResult = RelayCache.IO.write(cacheFileURL: self.cacheFileURL, record: cachedRelays)
+ logger.info("Relays haven't changed since last check.")
+
+ do {
+ try RelayCache.IO.write(cacheFileURL: cacheFileURL, record: cachedRelays)
- switch writeResult {
- case .success:
finish(completion: .success(.sameContent))
+ } catch {
+ let error = error as! RelayCache.Error
- case .failure(let error):
- logger.error(chainedError: error, message: "Failed to update cached relays timestamp.")
+ logger.error(
+ chainedError: error,
+ message: "Failed to update cached relays timestamp."
+ )
- finish(completion: .failure(.writeCache(error)))
+ finish(completion: .failure(error))
}
}
@@ -407,11 +434,11 @@ fileprivate class UpdateRelaysOperation: ResultOperation<RelayCache.FetchResult,
}
private func downloadRelays(previouslyCachedRelays: RelayCache.CachedRelays?) {
- downloadCancellable = apiProxy.getRelays(etag: previouslyCachedRelays?.etag, retryStrategy: .noRetry) { [weak self] result in
+ downloadTask = apiProxy.getRelays(etag: previouslyCachedRelays?.etag, retryStrategy: .noRetry) { [weak self] completion in
guard let self = self else { return }
self.dispatchQueue.async {
- switch result {
+ switch completion {
case .success(.newContent(let etag, let relays)):
self.didReceiveNewRelays(etag: etag, relays: relays)
diff --git a/ios/MullvadVPN/SettingsAccountCell.swift b/ios/MullvadVPN/SettingsAccountCell.swift
index 30411a1276..6021e4cca2 100644
--- a/ios/MullvadVPN/SettingsAccountCell.swift
+++ b/ios/MullvadVPN/SettingsAccountCell.swift
@@ -17,36 +17,39 @@ class SettingsAccountCell: SettingsCell {
}
private func didUpdateAccountExpiry() {
- if let accountExpiryDate = accountExpiryDate {
- let accountExpiry = AccountExpiry(date: accountExpiryDate)
-
- if accountExpiry.isExpired {
- detailTitleLabel.text = NSLocalizedString(
- "ACCOUNT_CELL_OUT_OF_TIME_LABEL",
- tableName: "Settings",
- comment: "Label displayed when user account ran out of time."
- )
- detailTitleLabel.textColor = .dangerColor
- } else {
- if let remainingTime = accountExpiry.formattedRemainingTime {
- let localizedString = NSLocalizedString(
- "ACCOUNT_CELL_TIME_LEFT_LABEL_FORMAT",
- tableName: "Settings",
- value: "%@ left",
- comment: "The amount of time left on user account. Use %@ placeholder to position the localized text with the time duration left (i.e 10 days)."
- )
- let formattedString = String(format: localizedString, remainingTime)
-
- detailTitleLabel.text = formattedString.uppercased()
- } else {
- detailTitleLabel.text = ""
- }
- detailTitleLabel.textColor = UIColor.Cell.detailTextColor
- }
- } else {
+ guard let accountExpiryDate = accountExpiryDate else {
detailTitleLabel.text = ""
detailTitleLabel.textColor = UIColor.Cell.detailTextColor
+ return
}
- }
+ guard accountExpiryDate > Date() else {
+ detailTitleLabel.text = NSLocalizedString(
+ "ACCOUNT_CELL_OUT_OF_TIME_LABEL",
+ tableName: "Settings",
+ value: "OUT OF TIME",
+ comment: ""
+ )
+ detailTitleLabel.textColor = .dangerColor
+ return
+ }
+
+ let formattedTime = CustomDateComponentsFormatting.localizedString(
+ from: Date(),
+ to: accountExpiryDate,
+ unitsStyle: .full
+ )
+
+ detailTitleLabel.text = formattedTime.map { remainingTimeString in
+ let localizedString = NSLocalizedString(
+ "ACCOUNT_CELL_TIME_LEFT_LABEL_FORMAT",
+ tableName: "Settings",
+ value: "%@ left",
+ comment: ""
+ )
+
+ return String(format: localizedString, remainingTimeString).uppercased()
+ } ?? ""
+ detailTitleLabel.textColor = UIColor.Cell.detailTextColor
+ }
}
diff --git a/ios/MullvadVPN/SettingsDataSource.swift b/ios/MullvadVPN/SettingsDataSource.swift
index 830e0ec2af..5912240f3b 100644
--- a/ios/MullvadVPN/SettingsDataSource.swift
+++ b/ios/MullvadVPN/SettingsDataSource.swift
@@ -8,7 +8,7 @@
import UIKit
-class SettingsDataSource: NSObject, AccountObserver, UITableViewDataSource, UITableViewDelegate {
+class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITableViewDelegate {
private enum CellReuseIdentifiers: String, CaseIterable {
case accountCell
case basicCell
@@ -50,6 +50,7 @@ class SettingsDataSource: NSObject, AccountObserver, UITableViewDataSource, UITa
}
private var snapshot = DataSourceSnapshot<Section, Item>()
+ private var storedAccountData: StoredAccountData?
weak var delegate: SettingsDataSourceDelegate?
@@ -65,7 +66,9 @@ class SettingsDataSource: NSObject, AccountObserver, UITableViewDataSource, UITa
override init() {
super.init()
- Account.shared.addObserver(self)
+ TunnelManager.shared.addObserver(self)
+ storedAccountData = TunnelManager.shared.tunnelSettings?.account
+
updateDataSnapshot()
}
@@ -82,7 +85,7 @@ class SettingsDataSource: NSObject, AccountObserver, UITableViewDataSource, UITa
private func updateDataSnapshot() {
var newSnapshot = DataSourceSnapshot<Section, Item>()
- if Account.shared.isLoggedIn {
+ if TunnelManager.shared.isAccountSet {
newSnapshot.appendSections([.main])
newSnapshot.appendItems([.account, .preferences, .wireguardKey], in: .main)
}
@@ -113,7 +116,7 @@ class SettingsDataSource: NSObject, AccountObserver, UITableViewDataSource, UITa
case .account:
let cell = tableView.dequeueReusableCell(withIdentifier: CellReuseIdentifiers.accountCell.rawValue, for: indexPath) as! SettingsAccountCell
cell.titleLabel.text = NSLocalizedString("ACCOUNT_CELL_LABEL", tableName: "Settings", value: "Account", comment: "")
- cell.accountExpiryDate = Account.shared.expiry
+ cell.accountExpiryDate = TunnelManager.shared.accountExpiry
cell.accessibilityIdentifier = "AccountCell"
cell.disclosureType = .chevron
@@ -199,22 +202,34 @@ class SettingsDataSource: NSObject, AccountObserver, UITableViewDataSource, UITa
return 0
}
- // MARK: - AccountObserver
+ // MARK: - TunnelObserver
- func account(_ account: Account, didUpdateExpiry expiry: Date) {
- tableView?.performBatchUpdates {
- if let indexPath = snapshot.indexPathForItem(.account) {
- tableView?.reloadRows(at: [indexPath], with: .none)
- }
- }
+ func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) {
+ // no-op
}
- func account(_ account: Account, didLoginWithToken token: String, expiry: Date) {
- updateDataSnapshot()
- tableView?.reloadData()
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) {
+ // no-op
}
- func accountDidLogout(_ account: Account) {
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) {
+ let newAccountData = tunnelSettings?.account
+ let oldAccountData = storedAccountData
+
+ storedAccountData = newAccountData
+
+ // Refresh individual row if expiry changed.
+ if let newAccountData = newAccountData, let oldAccountData = oldAccountData,
+ oldAccountData.number == newAccountData.number,
+ oldAccountData.expiry != newAccountData.expiry {
+ tableView?.performBatchUpdates {
+ if let indexPath = snapshot.indexPathForItem(.account) {
+ tableView?.reloadRows(at: [indexPath], with: .none)
+ }
+ }
+ return
+ }
+
updateDataSnapshot()
tableView?.reloadData()
}
diff --git a/ios/MullvadVPN/SettingsManager/SettingsManager.swift b/ios/MullvadVPN/SettingsManager/SettingsManager.swift
new file mode 100644
index 0000000000..41945b807d
--- /dev/null
+++ b/ios/MullvadVPN/SettingsManager/SettingsManager.swift
@@ -0,0 +1,256 @@
+//
+// SettingsManager.swift
+// MullvadVPN
+//
+// Created by pronebird on 29/04/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Logging
+
+enum SettingsManager {}
+
+struct LegacyTunnelSettings {
+ let accountNumber: String
+ let tunnelSettings: TunnelSettingsV1
+}
+
+let keychainServiceName = "Mullvad VPN"
+
+enum KeychainAccountName: String, CaseIterable {
+ case settings = "Settings"
+ case lastUsedAccount = "LastUsedAccount"
+}
+
+extension SettingsManager {
+
+ // MARK: -
+
+ static func getLastUsedAccount() throws -> String {
+ var query = createDefaultAttributes(accountName: .lastUsedAccount)
+ query[kSecReturnData] = true
+
+ var result: CFTypeRef?
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+ guard status == errSecSuccess else {
+ throw KeychainError(code: status)
+ }
+
+ let data = result as! Data
+
+ return String(data: data, encoding: .utf8)!
+ }
+
+ static func setLastUsedAccount(_ string: String?) throws {
+ let query = createDefaultAttributes(accountName: .lastUsedAccount)
+
+ guard let string = string else {
+ switch SecItemDelete(query as CFDictionary) {
+ case errSecSuccess, errSecItemNotFound:
+ return
+ case let status:
+ throw KeychainError(code: status)
+ }
+ }
+
+ let data = string.data(using: .utf8)!
+ var status = SecItemUpdate(
+ query as CFDictionary,
+ [kSecValueData: data] as CFDictionary
+ )
+
+ switch status {
+ case errSecItemNotFound:
+ var insert = query
+ insert[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlock
+ insert[kSecValueData] = data
+
+ status = SecItemAdd(insert as CFDictionary, nil)
+ if status != errSecSuccess {
+ throw KeychainError(code: status)
+ }
+ case errSecSuccess:
+ break
+ default:
+ throw KeychainError(code: status)
+ }
+ }
+
+ // MARK: -
+
+ static func readSettings() throws -> TunnelSettingsV2 {
+ var query = createDefaultAttributes(accountName: .settings)
+ query[kSecReturnData] = true
+
+ var result: CFTypeRef?
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+ guard status == errSecSuccess else {
+ throw KeychainError(code: status)
+ }
+
+ let data = result as! Data
+
+ let decoder = JSONDecoder()
+ return try decoder.decode(TunnelSettingsV2.self, from: data)
+ }
+
+ static func writeSettings(_ settings: TunnelSettingsV2) throws {
+ let encoder = JSONEncoder()
+ let data = try encoder.encode(settings)
+
+ let query = createDefaultAttributes(accountName: .settings)
+ var status = SecItemUpdate(
+ query as CFDictionary,
+ [kSecValueData: data] as CFDictionary
+ )
+
+ switch status {
+ case errSecItemNotFound:
+ var insert = query
+ insert[kSecAttrAccessGroup] = ApplicationConfiguration.securityGroupIdentifier
+ insert[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlock
+ insert[kSecValueData] = data
+
+ status = SecItemAdd(insert as CFDictionary, nil)
+ if status != errSecSuccess {
+ throw KeychainError(code: status)
+ }
+ case errSecSuccess:
+ break
+ default:
+ throw KeychainError(code: status)
+ }
+ }
+
+ static func deleteSettings() throws {
+ let query = createDefaultAttributes(accountName: .settings)
+ let status = SecItemDelete(query as CFDictionary)
+ if status != errSecSuccess {
+ throw KeychainError(code: status)
+ }
+ }
+
+ private static func createDefaultAttributes(accountName: KeychainAccountName) -> [CFString: Any] {
+ return [
+ kSecClass: kSecClassGenericPassword,
+ kSecAttrService: keychainServiceName,
+ kSecAttrAccount: accountName.rawValue
+ ]
+ }
+
+ // MARK: - Legacy settings support
+
+ private static let logger = Logger(label: "SettingsManager")
+
+ static func readLegacySettings() throws -> [LegacyTunnelSettings] {
+ let query: [CFString: Any] = [
+ kSecClass: kSecClassGenericPassword,
+ kSecAttrService: keychainServiceName,
+ kSecReturnAttributes: true,
+ kSecReturnData: true,
+ kSecMatchLimit: kSecMatchLimitAll
+ ]
+
+ var result: CFTypeRef?
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+ guard status == errSecSuccess else {
+ throw KeychainError(code: status)
+ }
+
+ guard let items = result as? [[CFString: Any]] else {
+ return []
+ }
+
+ return items.filter(Self.filterLegacySettings)
+ .compactMap { item -> LegacyTunnelSettings? in
+ guard let accountNumber = item[kSecAttrAccount] as? String,
+ let data = item[kSecValueData] as? Data else {
+ return nil
+ }
+ do {
+ let tunnelSettings = try JSONDecoder().decode(
+ TunnelSettingsV1.self,
+ from: data
+ )
+
+ return LegacyTunnelSettings(
+ accountNumber: accountNumber,
+ tunnelSettings: tunnelSettings
+ )
+ } catch {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to decode legacy settings."
+ )
+ return nil
+ }
+ }
+ }
+
+ static func deleteLegacySettings() {
+ let query: [CFString: Any] = [
+ kSecClass: kSecClassGenericPassword,
+ kSecAttrService: keychainServiceName,
+ kSecReturnAttributes: true,
+ kSecMatchLimit: kSecMatchLimitAll
+ ]
+
+ var result: CFTypeRef?
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+ guard status == errSecSuccess else {
+ let error = KeychainError(code: status)
+
+ if error != .itemNotFound {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to list legacy settings."
+ )
+ }
+
+ return
+ }
+
+ guard let items = result as? [[CFString: Any]] else {
+ return
+ }
+
+ items.filter(Self.filterLegacySettings)
+ .enumerated()
+ .forEach { (index, item) in
+ guard let account = item[kSecAttrAccount] else {
+ return
+ }
+
+ let deleteQuery: [CFString: Any] = [
+ kSecClass: kSecClassGenericPassword,
+ kSecAttrService: keychainServiceName,
+ kSecAttrAccount: account
+ ]
+
+ let status = SecItemDelete(deleteQuery as CFDictionary)
+ if status == errSecSuccess {
+ logger.debug("Removed legacy settings entry \(index).")
+ } else {
+ let error = KeychainError(code: status)
+
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to remove legacy settings entry \(index)."
+ )
+ }
+ }
+ }
+
+ private static func filterLegacySettings(_ item: [CFString: Any]) -> Bool {
+ guard let accountNumber = item[kSecAttrAccount] as? String else {
+ return false
+ }
+
+ return KeychainAccountName(rawValue: accountNumber) == nil
+ }
+}
diff --git a/ios/MullvadVPN/SettingsManager/TunnelSettingsV1.swift b/ios/MullvadVPN/SettingsManager/TunnelSettingsV1.swift
new file mode 100644
index 0000000000..0d7facb554
--- /dev/null
+++ b/ios/MullvadVPN/SettingsManager/TunnelSettingsV1.swift
@@ -0,0 +1,98 @@
+//
+// TunnelSettingsV1.swift
+// MullvadVPN
+//
+// Created by pronebird on 19/06/2019.
+// Copyright © 2019 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import struct Network.IPv4Address
+import class WireGuardKitTypes.PublicKey
+import class WireGuardKitTypes.PrivateKey
+import struct WireGuardKitTypes.IPAddressRange
+
+/// A struct that holds the configuration passed via `NETunnelProviderProtocol`.
+struct TunnelSettingsV1: Codable, Equatable {
+ var relayConstraints = RelayConstraints()
+ var interface = InterfaceSettings()
+}
+
+/// A struct that holds a tun interface configuration.
+struct InterfaceSettings: Codable, Equatable {
+ var privateKey: PrivateKeyWithMetadata
+ var nextPrivateKey: PrivateKeyWithMetadata?
+
+ var addresses: [IPAddressRange]
+ var dnsSettings: DNSSettings
+
+ private enum CodingKeys: String, CodingKey {
+ case privateKey, nextPrivateKey, addresses, dnsSettings
+ }
+
+ init(
+ privateKey: PrivateKeyWithMetadata = PrivateKeyWithMetadata(),
+ nextPrivateKey: PrivateKeyWithMetadata? = nil,
+ addresses: [IPAddressRange] = [],
+ dnsSettings: DNSSettings = DNSSettings()
+ )
+ {
+ self.privateKey = privateKey
+ self.nextPrivateKey = nextPrivateKey
+ self.addresses = addresses
+ self.dnsSettings = dnsSettings
+ }
+
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+
+ privateKey = try container.decode(PrivateKeyWithMetadata.self, forKey: .privateKey)
+ addresses = try container.decode([IPAddressRange].self, forKey: .addresses)
+
+ // Added in 2022.1
+ nextPrivateKey = try container.decodeIfPresent(PrivateKeyWithMetadata.self, forKey: .nextPrivateKey)
+
+ // Provide default value, since `dnsSettings` key does not exist in <= 2021.2
+ dnsSettings = try container.decodeIfPresent(DNSSettings.self, forKey: .dnsSettings)
+ ?? DNSSettings()
+ }
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.container(keyedBy: CodingKeys.self)
+
+ try container.encode(privateKey, forKey: .privateKey)
+ try container.encode(nextPrivateKey, forKey: .nextPrivateKey)
+ try container.encode(addresses, forKey: .addresses)
+ try container.encode(dnsSettings, forKey: .dnsSettings)
+ }
+}
+
+/// A struct holding a private WireGuard key with associated metadata
+struct PrivateKeyWithMetadata: Equatable, Codable {
+ private enum CodingKeys: String, CodingKey {
+ case privateKey = "privateKeyData", creationDate
+ }
+
+ /// When the key was created
+ let creationDate: Date
+
+ /// Private key
+ let privateKey: PrivateKey
+
+ /// Public key
+ var publicKey: PublicKey {
+ return privateKey.publicKey
+ }
+
+ /// Initialize the new private key
+ init() {
+ privateKey = PrivateKey()
+ creationDate = Date()
+ }
+
+ /// Initialize with the existing private key
+ init(privateKey: PrivateKey, createdAt: Date) {
+ self.privateKey = privateKey
+ creationDate = createdAt
+ }
+}
diff --git a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2+REST.swift b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2+REST.swift
new file mode 100644
index 0000000000..438bd69481
--- /dev/null
+++ b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2+REST.swift
@@ -0,0 +1,21 @@
+//
+// TunnelSettingsV2+REST.swift
+// MullvadVPN
+//
+// Created by pronebird on 13/05/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import class WireGuardKitTypes.PrivateKey
+
+extension StoredDeviceData {
+ mutating func update(from device: REST.Device) {
+ identifier = device.id
+ name = device.name
+ creationDate = device.created
+ hijackDNS = device.hijackDNS
+ ipv4Address = device.ipv4Address
+ ipv6Address = device.ipv6Address
+ }
+}
diff --git a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
new file mode 100644
index 0000000000..942e3a27fd
--- /dev/null
+++ b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
@@ -0,0 +1,69 @@
+//
+// TunnelSettingsV2.swift
+// MullvadVPN
+//
+// Created by pronebird on 27/04/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import struct Network.IPv4Address
+import class WireGuardKitTypes.PublicKey
+import class WireGuardKitTypes.PrivateKey
+import struct WireGuardKitTypes.IPAddressRange
+
+struct TunnelSettingsV2: Codable, Equatable {
+ /// Mullvad account data.
+ var account: StoredAccountData
+
+ /// Device data.
+ var device: StoredDeviceData
+
+ /// Relay constraints.
+ var relayConstraints: RelayConstraints
+
+ /// DNS settings.
+ var dnsSettings: DNSSettings
+}
+
+struct StoredAccountData: Codable, Equatable {
+ /// Account identifier.
+ var identifier: String
+
+ /// Account number.
+ var number: String
+
+ /// Account expiry.
+ var expiry: Date
+}
+
+struct StoredDeviceData: Codable, Equatable {
+ /// Device creation date.
+ var creationDate: Date
+
+ /// Device identifier.
+ var identifier: String
+
+ /// Device name.
+ var name: String
+
+ /// Whether relay hijacks DNS from this device.
+ var hijackDNS: Bool
+
+ /// IPv4 address assigned to device.
+ var ipv4Address: IPAddressRange
+
+ /// IPv6 address assignged to device.
+ var ipv6Address: IPAddressRange
+
+ /// WireGuard key data.
+ var wgKeyData: StoredWgKeyData
+}
+
+struct StoredWgKeyData: Codable, Equatable {
+ /// Private key creation date.
+ var creationDate: Date
+
+ /// Private key.
+ var privateKey: PrivateKey
+}
diff --git a/ios/MullvadVPN/SettingsNavigationController.swift b/ios/MullvadVPN/SettingsNavigationController.swift
index b594492cff..b916561373 100644
--- a/ios/MullvadVPN/SettingsNavigationController.swift
+++ b/ios/MullvadVPN/SettingsNavigationController.swift
@@ -62,7 +62,7 @@ class SettingsNavigationController: CustomNavigationController, SettingsViewCont
navigationBar.prefersLargeTitles = true
// Update account expiry
- Account.shared.updateAccountExpiry()
+ TunnelManager.shared.updateAccountData()
}
// MARK: - SettingsViewControllerDelegate
diff --git a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift
index 9f6bf5fc78..7ad0d19a34 100644
--- a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift
+++ b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift
@@ -83,24 +83,29 @@ class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate {
}
private func pickRelay() -> RelaySelectorResult? {
- switch RelayCache.Tracker.shared.readAndWait() {
- case .success(let cachedRelays):
- let keychainReference = self.protocolConfiguration.passwordReference!
-
- switch TunnelSettingsManager.load(searchTerm: .persistentReference(keychainReference)) {
- case .success(let entry):
- return RelaySelector.evaluate(
- relays: cachedRelays.relays,
- constraints: entry.tunnelSettings.relayConstraints
- )
- case .failure(let error):
- self.providerLogger.error(chainedError: error, message: "Failed to load tunnel settings when picking relay.")
+ 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."
+ )
+ return nil
+ }
- return nil
- }
+ do {
+ let tunnelSettings = try SettingsManager.readSettings()
- case .failure(let error):
- self.providerLogger.error(chainedError: error, message: "Failed to read relays when picking relay.")
+ return RelaySelector.evaluate(
+ relays: cachedRelays.relays,
+ constraints: tunnelSettings.relayConstraints
+ )
+ } catch {
+ providerLogger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to read settings when picking relay."
+ )
return nil
}
}
diff --git a/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift b/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift
new file mode 100644
index 0000000000..50fad00647
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift
@@ -0,0 +1,128 @@
+//
+// LoadTunnelConfigurationOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 16/12/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Logging
+
+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
+ ) {
+ self.state = state
+
+ super.init(
+ dispatchQueue: dispatchQueue,
+ completionQueue: dispatchQueue,
+ completionHandler: completionHandler
+ )
+ }
+
+ override func main() {
+ TunnelProviderManagerType.loadAllFromPreferences { tunnels, error in
+ self.dispatchQueue.async {
+ if let error = error {
+ self.finish(completion: .failure(.loadAllVPNConfigurations(error)))
+ } else {
+ self.didLoadVPNConfigurations(tunnels: tunnels)
+ }
+ }
+ }
+ }
+
+ private func didLoadVPNConfigurations(tunnels: [TunnelProviderManagerType]?) {
+ let tunnelProvider = tunnels?.first
+
+ 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 {
+ 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)
+
+ do {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Cannot decode settings. Will attempt to delete them from keychain."
+ )
+
+ try SettingsManager.deleteSettings()
+ } catch {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to delete settings from keychain."
+ )
+ }
+
+ let returnError: TunnelManager.Error = .readSettings(error)
+
+ 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 returnError: TunnelManager.Error = .readSettings(error)
+
+ if let tunnelProvider = tunnelProvider {
+ removeOrphanedTunnel(tunnelProvider: tunnelProvider) { _ in
+ self.finish(completion: .failure(returnError))
+ }
+ } else {
+ finish(completion: .failure(returnError))
+ }
+ }
+ }
+
+ private func removeOrphanedTunnel(tunnelProvider: TunnelProviderManagerType, completion: @escaping (TunnelManager.Error?) -> Void) {
+ 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)
+ }
+ }
+ }
+ }
+}
diff --git a/ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift b/ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift
new file mode 100644
index 0000000000..61327ec409
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift
@@ -0,0 +1,249 @@
+//
+// MigrateSettingsOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 18/05/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Logging
+import class WireGuardKitTypes.PrivateKey
+
+class MigrateSettingsOperation: AsyncOperation {
+ private let accountTokenKey = "accountToken"
+ private let accountExpiryKey = "accountExpiry"
+
+ private let accountsProxy: REST.AccountsProxy
+ private let devicesProxy: REST.DevicesProxy
+
+ private let logger = Logger(label: "MigrateSettingsOperation")
+
+ private var accountTask: Cancellable?
+ private var deviceTask: Cancellable?
+
+ private var accountData: REST.AccountData?
+ private var devices: [REST.Device]?
+
+ init(
+ dispatchQueue: DispatchQueue,
+ accountsProxy: REST.AccountsProxy,
+ devicesProxy: REST.DevicesProxy
+ )
+ {
+ self.accountsProxy = accountsProxy
+ self.devicesProxy = devicesProxy
+
+ super.init(dispatchQueue: dispatchQueue)
+ }
+
+ override func main() {
+ // Read legacy account number from user defaults.
+ let storedAccountNumber = UserDefaults.standard.string(forKey: accountTokenKey)
+
+ guard let storedAccountNumber = storedAccountNumber else {
+ logger.debug("Account number is not found in user defaults. Nothing to migrate.")
+
+ finishMigration()
+ return
+ }
+
+ // Set legacy account number as last used.
+ logger.debug("Found legacy account number.")
+ logger.debug("Store last used account.")
+
+ do {
+ try SettingsManager.setLastUsedAccount(storedAccountNumber)
+ } catch {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to store last used account."
+ )
+ }
+
+ // List legacy settings stored in keychain.
+ logger.debug("Read legacy settings...")
+
+ var storedSettings: [LegacyTunnelSettings] = []
+ do {
+ storedSettings = try SettingsManager.readLegacySettings()
+ } catch .itemNotFound as KeychainError {
+ logger.debug("Legacy settings are not found in keychain.")
+
+ finishMigration()
+ return
+ } catch {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to read legacy settings from keychain."
+ )
+ finishMigration()
+ return
+ }
+
+ // Find settings matching the account number stored in user defaults.
+ let matchingSettings = storedSettings.first { settings in
+ return settings.accountNumber == storedAccountNumber
+ }
+
+ guard let matchingSettings = matchingSettings else {
+ logger.debug(
+ "Could not find legacy settings matching the legacy account number."
+ )
+
+ finishMigration()
+ return
+ }
+
+ // Fetch remote data concurrently.
+ logger.debug("Fetching account and device data...")
+
+ let dispatchGroup = DispatchGroup()
+
+ dispatchGroup.enter()
+ accountTask = accountsProxy.getAccountData(
+ accountNumber: storedAccountNumber,
+ retryStrategy: .aggressive
+ ) { completion in
+ self.dispatchQueue.async {
+ self.didFinishAccountRequest(completion)
+
+ dispatchGroup.leave()
+ }
+ }
+
+ dispatchGroup.enter()
+ deviceTask = devicesProxy.getDevices(
+ accountNumber: storedAccountNumber,
+ retryStrategy: .aggressive
+ ) { completion in
+ self.dispatchQueue.async {
+ self.didFinishDeviceRequest(completion)
+
+ dispatchGroup.leave()
+ }
+ }
+
+ dispatchGroup.notify(queue: dispatchQueue) {
+ // Migrate settings if all data is available.
+ if let accountData = self.accountData, let devices = self.devices {
+ self.migrateSettings(
+ settings: matchingSettings,
+ accountData: accountData,
+ devices: devices
+ )
+ }
+
+ // Finish migration.
+ self.finishMigration()
+ }
+ }
+
+ private func didFinishAccountRequest(_ completion: OperationCompletion<REST.AccountData, REST.Error>) {
+ switch completion {
+ case .success(let accountData):
+ self.accountData = accountData
+
+ case .failure(let error):
+ logger.error(chainedError: error, message: "Failed to fetch accound data.")
+
+ case .cancelled:
+ logger.debug("Account data request was cancelled.")
+ }
+ }
+
+ private func didFinishDeviceRequest(_ completion: OperationCompletion<[REST.Device], REST.Error>) {
+ switch completion {
+ case .success(let devices):
+ self.devices = devices
+
+ case .failure(let error):
+ logger.error(chainedError: error, message: "Failed to fetch devices.")
+
+ case .cancelled:
+ logger.debug("Device request was cancelled.")
+ }
+ }
+
+ private func migrateSettings(
+ settings: LegacyTunnelSettings,
+ accountData: REST.AccountData,
+ devices: [REST.Device]
+ ) {
+ let tunnelSettings = settings.tunnelSettings
+ let interfaceData = settings.tunnelSettings.interface
+
+ // Find device that matches the public key stored in legacy settings.
+ let device = devices.first { device in
+ return device.pubkey == interfaceData.privateKey.publicKey ||
+ device.pubkey == interfaceData.nextPrivateKey?.publicKey
+ }
+
+ guard let device = device else {
+ logger.debug(
+ "Failed to match legacy settings against available devices."
+ )
+ return
+ }
+
+ logger.debug("Found device matching public key stored in legacy settings.")
+
+ // Match private key.
+ let privateKeyWithMetadata: PrivateKeyWithMetadata
+ if let nextKey = interfaceData.nextPrivateKey, nextKey.publicKey == device.pubkey
+ {
+ privateKeyWithMetadata = nextKey
+ } else {
+ privateKeyWithMetadata = interfaceData.privateKey
+ }
+
+ logger.debug("Store new settings...")
+
+ // Create new settings.
+ let newSettings = TunnelSettingsV2(
+ account: StoredAccountData(
+ identifier: accountData.id,
+ number: settings.accountNumber,
+ expiry: accountData.expiry
+ ),
+ device: StoredDeviceData(
+ creationDate: device.created,
+ identifier: device.id,
+ name: device.name,
+ hijackDNS: device.hijackDNS,
+ ipv4Address: device.ipv4Address,
+ ipv6Address: device.ipv6Address,
+ wgKeyData: StoredWgKeyData(
+ creationDate: privateKeyWithMetadata.creationDate,
+ privateKey: privateKeyWithMetadata.privateKey
+ )
+ ),
+ relayConstraints: tunnelSettings.relayConstraints,
+ dnsSettings: interfaceData.dnsSettings
+ )
+
+ // Save settings.
+ do {
+ try SettingsManager.writeSettings(newSettings)
+ } catch {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to write migrated settings."
+ )
+ }
+ }
+
+ private func finishMigration() {
+ let userDefaults = UserDefaults.standard
+
+ logger.debug("Remove legacy settings from keychain.")
+ SettingsManager.deleteLegacySettings()
+
+ logger.debug("Remove legacy settings from user defaults.")
+ userDefaults.removeObject(forKey: accountTokenKey)
+ userDefaults.removeObject(forKey: accountExpiryKey)
+
+ finish()
+ }
+
+}
diff --git a/ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift b/ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift
deleted file mode 100644
index 6a02ad09db..0000000000
--- a/ios/MullvadVPN/TunnelManager/ReplaceKeyOperation.swift
+++ /dev/null
@@ -1,204 +0,0 @@
-//
-// ReplaceKeyOperation.swift
-// MullvadVPN
-//
-// Created by pronebird on 15/12/2021.
-// Copyright © 2021 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import Logging
-
-class ReplaceKeyOperation: ResultOperation<TunnelManager.KeyRotationResult, TunnelManager.Error> {
- private let state: TunnelManager.State
-
- private let apiProxy: REST.APIProxy
- private var restRequest: Cancellable?
-
- private let rotationInterval: TimeInterval?
-
- private let logger = Logger(label: "TunnelManager.ReplaceKeyOperation")
-
- class func operationForKeyRotation(
- dispatchQueue: DispatchQueue,
- state: TunnelManager.State,
- apiProxy: REST.APIProxy,
- rotationInterval: TimeInterval,
- completionHandler: @escaping CompletionHandler
- ) -> ReplaceKeyOperation {
- return ReplaceKeyOperation(
- dispatchQueue: dispatchQueue,
- state: state,
- apiProxy: apiProxy,
- rotationInterval: rotationInterval,
- completionHandler: completionHandler
- )
- }
-
- class func operationForKeyRegeneration(
- dispatchQueue: DispatchQueue,
- state: TunnelManager.State,
- apiProxy: REST.APIProxy,
- completionHandler: @escaping (OperationCompletion<(), TunnelManager.Error>) -> Void
- ) -> ReplaceKeyOperation {
- return ReplaceKeyOperation(
- dispatchQueue: dispatchQueue,
- state: state,
- apiProxy: apiProxy,
- rotationInterval: nil
- ) { completion in
- let mappedCompletion = completion.map { keyRotationResult -> () in
- switch keyRotationResult {
- case .finished:
- return ()
- case .throttled:
- fatalError("ReplaceKeyOperation.operationForKeyRegeneration() must never recieve throttled!")
- }
- }
-
- completionHandler(mappedCompletion)
- }
- }
-
- private init(
- dispatchQueue: DispatchQueue,
- state: TunnelManager.State,
- apiProxy: REST.APIProxy,
- rotationInterval: TimeInterval?,
- completionHandler: @escaping CompletionHandler
- ) {
- self.state = state
-
- self.apiProxy = apiProxy
- self.rotationInterval = rotationInterval
-
- super.init(
- dispatchQueue: dispatchQueue,
- completionQueue: dispatchQueue,
- completionHandler: completionHandler
- )
- }
-
- override func main() {
- self.execute { completion in
- self.finish(completion: completion)
- }
- }
-
- override func operationDidCancel() {
- restRequest?.cancel()
- restRequest = nil
- }
-
- private func execute(completionHandler: @escaping CompletionHandler) {
- guard let tunnelInfo = state.tunnelInfo else {
- completionHandler(.failure(.unsetAccount))
- return
- }
-
- if let rotationInterval = rotationInterval {
- let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate
- let nextRotationDate = creationDate.addingTimeInterval(rotationInterval)
-
- if nextRotationDate > Date() {
- logger.debug("Throttle private key rotation.")
-
- completionHandler(.success(.throttled(creationDate)))
- return
- } else {
- logger.debug("Private key is old enough, rotate right away.")
- }
- } else {
- logger.debug("Rotate private key right away.")
- }
-
- let newPrivateKey: PrivateKeyWithMetadata
- let oldPublicKey = tunnelInfo.tunnelSettings.interface.publicKey
-
- if let nextPrivateKey = tunnelInfo.tunnelSettings.interface.nextPrivateKey {
- newPrivateKey = nextPrivateKey
-
- logger.debug("Next private key is already created.")
- } else {
- newPrivateKey = PrivateKeyWithMetadata()
-
- logger.debug("Create next private key.")
-
- let saveResult = TunnelSettingsManager.update(searchTerm: .accountToken(tunnelInfo.token)) { newTunnelSettings in
- newTunnelSettings.interface.nextPrivateKey = newPrivateKey
- }
-
- switch saveResult {
- case .success(let newTunnelSettings):
- logger.debug("Saved next private key.")
-
- state.tunnelInfo?.tunnelSettings = newTunnelSettings
-
- case .failure(let error):
- logger.error(chainedError: error, message: "Failed to save next private key.")
-
- completionHandler(.failure(.updateTunnelSettings(error)))
- return
- }
- }
-
- logger.debug("Replacing old key with new key on server...")
-
- restRequest = self.apiProxy.replaceWireguardKey(
- accountNumber: tunnelInfo.token,
- oldPublicKey: oldPublicKey,
- newPublicKey: newPrivateKey.publicKey,
- retryStrategy: .default
- ) { completion in
- self.dispatchQueue.async {
- self.didReceiveResponse(
- completion: completion,
- accountToken: tunnelInfo.token,
- newPrivateKey: newPrivateKey,
- completionHandler: completionHandler
- )
- }
- }
- }
-
- private func didReceiveResponse(completion: OperationCompletion<REST.WireguardAddressesResponse, REST.Error>, accountToken: String, newPrivateKey: PrivateKeyWithMetadata, completionHandler: @escaping CompletionHandler) {
- switch completion {
- case .success(let associatedAddresses):
- logger.debug("Replaced old key with new key on server.")
-
- let saveResult = TunnelSettingsManager.update(searchTerm: .accountToken(accountToken)) { newTunnelSettings in
- newTunnelSettings.interface.privateKey = newPrivateKey
- newTunnelSettings.interface.nextPrivateKey = nil
-
- newTunnelSettings.interface.addresses = [
- associatedAddresses.ipv4Address,
- associatedAddresses.ipv6Address
- ]
- }
-
- switch saveResult {
- case .success(let newTunnelSettings):
- logger.debug("Saved associated addresses.")
-
- state.tunnelInfo?.tunnelSettings = newTunnelSettings
-
- completionHandler(.success(.finished))
-
- case .failure(let error):
- logger.error(chainedError: error, message: "Failed to save associated addresses.")
-
- completionHandler(.failure(.updateTunnelSettings(error)))
- }
-
- case .failure(let restError):
- logger.error(chainedError: restError, message: "Failed to replace old key with new key on server.")
-
- completionHandler(.failure(.replaceWireguardKey(restError)))
-
- case .cancelled:
- logger.debug("Cancelled replace key request.")
-
- completionHandler(.cancelled)
- }
- }
-}
diff --git a/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift b/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift
new file mode 100644
index 0000000000..bde9f12811
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift
@@ -0,0 +1,132 @@
+//
+// RotateKeyOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 15/12/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Logging
+import class WireGuardKitTypes.PrivateKey
+
+class RotateKeyOperation: ResultOperation<TunnelManager.KeyRotationResult, TunnelManager.Error> {
+ private let state: TunnelManager.State
+
+ private let devicesProxy: REST.DevicesProxy
+ private var task: Cancellable?
+
+ private let rotationInterval: TimeInterval?
+ private let logger = Logger(label: "ReplaceKeyOperation")
+
+ init(
+ dispatchQueue: DispatchQueue,
+ state: TunnelManager.State,
+ devicesProxy: REST.DevicesProxy,
+ rotationInterval: TimeInterval?,
+ completionHandler: @escaping CompletionHandler
+ ) {
+ self.state = state
+
+ self.devicesProxy = devicesProxy
+ self.rotationInterval = rotationInterval
+
+ super.init(
+ dispatchQueue: dispatchQueue,
+ completionQueue: dispatchQueue,
+ completionHandler: completionHandler
+ )
+ }
+
+ override func main() {
+ guard let tunnelSettings = state.tunnelSettings else {
+ finish(completion: .failure(.unsetAccount))
+ return
+ }
+
+ if let rotationInterval = rotationInterval {
+ let creationDate = tunnelSettings.device.wgKeyData.creationDate
+ let nextRotationDate = creationDate.addingTimeInterval(rotationInterval)
+
+ if nextRotationDate > Date() {
+ logger.debug("Throttle private key rotation.")
+
+ finish(completion: .success(.throttled(creationDate)))
+ return
+ } else {
+ logger.debug("Private key is old enough, rotate right away.")
+ }
+ } else {
+ logger.debug("Rotate private key right away.")
+ }
+
+ logger.debug("Replacing old key with new key on server...")
+
+ let newPrivateKey = PrivateKey()
+
+ task = devicesProxy.rotateDeviceKey(
+ accountNumber: tunnelSettings.account.number,
+ identifier: tunnelSettings.device.identifier,
+ publicKey: newPrivateKey.publicKey,
+ retryStrategy: .default
+ ) { completion in
+ self.dispatchQueue.async {
+ self.didRotateKey(
+ tunnelSettings: tunnelSettings,
+ newPrivateKey: newPrivateKey,
+ completion: completion
+ )
+ }
+ }
+ }
+
+ override func operationDidCancel() {
+ task?.cancel()
+ task = nil
+ }
+
+ private func didRotateKey(
+ tunnelSettings: TunnelSettingsV2,
+ newPrivateKey: PrivateKey,
+ completion: OperationCompletion<REST.Device, REST.Error>
+ )
+ {
+ let mappedCompletion = completion.mapError { error -> TunnelManager.Error in
+ logger.error(
+ chainedError: error,
+ message: "Failed to rotate device key."
+ )
+
+ return .rotateKey(error)
+ }
+
+ guard let device = mappedCompletion.value else {
+ finish(completion: mappedCompletion.assertNoSuccess())
+ return
+ }
+
+ logger.debug("Successfully rotated device key. Persisting settings...")
+
+ do {
+ var newTunnelSettings = tunnelSettings
+ newTunnelSettings.device.update(from: device)
+ newTunnelSettings.device.wgKeyData = StoredWgKeyData(
+ creationDate: Date(),
+ privateKey: newPrivateKey
+ )
+
+ try SettingsManager.writeSettings(newTunnelSettings)
+
+ state.tunnelSettings = newTunnelSettings
+
+ finish(completion: .success(.finished))
+ } catch {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to write settings."
+ )
+
+ finish(completion: .failure(.writeSettings(error)))
+ }
+ }
+}
diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
index 92f1d8df14..769b3c1510 100644
--- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
@@ -8,14 +8,39 @@
import Foundation
import class WireGuardKitTypes.PublicKey
+import class WireGuardKitTypes.PrivateKey
import Logging
-class SetAccountOperation: ResultOperation<(), TunnelManager.Error> {
+enum SetAccountAction {
+ /// Set new account.
+ case new
+
+ /// Set existing account.
+ case existing(String)
+
+ /// Unset account.
+ case unset
+
+ var taskName: String {
+ switch self {
+ case .new:
+ return "Set new account"
+ case .existing:
+ return "Set existing account"
+ case .unset:
+ return "Unset account"
+ }
+ }
+}
+
+class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Error> {
typealias WillDeleteVPNConfigurationHandler = () -> Void
private let state: TunnelManager.State
- private let apiProxy: REST.APIProxy
- private let accountToken: String?
+ private let accountsProxy: REST.AccountsProxy
+ private let devicesProxy: REST.DevicesProxy
+ private let action: SetAccountAction
+ private var task: Cancellable?
private var willDeleteVPNConfigurationHandler: WillDeleteVPNConfigurationHandler?
private let logger = Logger(label: "TunnelManager.SetAccountOperation")
@@ -23,15 +48,17 @@ class SetAccountOperation: ResultOperation<(), TunnelManager.Error> {
init(
dispatchQueue: DispatchQueue,
state: TunnelManager.State,
- apiProxy: REST.APIProxy,
- accountToken: String?,
+ accountsProxy: REST.AccountsProxy,
+ devicesProxy: REST.DevicesProxy,
+ action: SetAccountAction,
willDeleteVPNConfigurationHandler: @escaping WillDeleteVPNConfigurationHandler,
completionHandler: @escaping CompletionHandler
)
{
self.state = state
- self.apiProxy = apiProxy
- self.accountToken = accountToken
+ self.accountsProxy = accountsProxy
+ self.devicesProxy = devicesProxy
+ self.action = action
self.willDeleteVPNConfigurationHandler = willDeleteVPNConfigurationHandler
super.init(
@@ -42,139 +69,245 @@ class SetAccountOperation: ResultOperation<(), TunnelManager.Error> {
}
override func main() {
- execute { completion in
- self.finish(completion: completion)
+ if let tunnelSettings = self.state.tunnelSettings {
+ self.deleteDevice(
+ accountNumber: tunnelSettings.account.number,
+ deviceIdentifier: tunnelSettings.device.identifier
+ )
+ } else {
+ self.fetchAccountData()
}
}
- private func execute(completionHandler: @escaping CompletionHandler) {
- // Delete current account key and configuration if set.
- if let tunnelInfo = state.tunnelInfo, tunnelInfo.token != accountToken {
- let currentAccountToken = tunnelInfo.token
- let currentPublicKey = tunnelInfo.tunnelSettings.interface.publicKey
- let nextPublicKey = tunnelInfo.tunnelSettings.interface.nextPrivateKey?.publicKey
+ private func deleteDevice(accountNumber: String, deviceIdentifier: String) {
+ logger.debug("Delete current device...")
- logger.debug("Unset current account token.")
+ task = devicesProxy.deleteDevice(
+ accountNumber: accountNumber,
+ identifier: deviceIdentifier,
+ retryStrategy: .default,
+ completion: { [weak self] completion in
+ self?.dispatchQueue.async {
+ self?.didDeleteDevice(completion)
+ }
+ })
+ }
- let publicKeys = [currentPublicKey, nextPublicKey].compactMap { $0 }
+ private func didDeleteDevice(_ completion: OperationCompletion<Bool, REST.Error>) {
+ let mappedCompletion = completion.mapError { error -> TunnelManager.Error in
+ logger.error(chainedError: error, message: "Failed to delete device.")
- deletePublicKeys(publicKeys, accountToken: currentAccountToken) {
- self.deleteKeychainEntryAndVPNConfiguration(accountToken: currentAccountToken) {
- self.setNewAccount(completionHandler: completionHandler)
- }
- }
- } else {
- setNewAccount(completionHandler: completionHandler)
+ return .deleteDevice(error)
}
- }
- private func setNewAccount(completionHandler: @escaping CompletionHandler) {
- guard let accountToken = accountToken else {
- logger.debug("Account token is unset.")
- completionHandler(.success(()))
+ guard let isDeleted = mappedCompletion.value else {
+ finish(completion: mappedCompletion.assertNoSuccess())
return
}
- logger.debug("Set new account token.")
+ if isDeleted {
+ logger.debug("Deleted device.")
+ } else {
+ logger.debug("Device is already deleted.")
+ }
- // Check Keychain for leftover settings from previous installation and attempt to remove
- // previous WirGuard keys before proceeding.
- switch TunnelSettingsManager.load(searchTerm: .accountToken(accountToken)) {
- case .success(let keychainEntry):
- let interfaceSettings = keychainEntry.tunnelSettings.interface
+ state.tunnelSettings = nil
- logger.debug("Found leftover tunnel settings in Keychain.")
+ deleteSettingsAndVPNConfiguration { error in
+ if let error = error {
+ self.finish(completion: .failure(error))
+ } else {
+ self.fetchAccountData()
+ }
+ }
+ }
- let publicKeys = [interfaceSettings.publicKey, interfaceSettings.nextPrivateKey?.publicKey]
- .compactMap { $0 }
+ private func fetchAccountData() {
+ switch action {
+ case .unset:
+ logger.debug("Account number is unset.")
- deletePublicKeys(publicKeys, accountToken: accountToken) {
- self.addTunnelSettingsAndPushKey(accountToken: accountToken, completionHandler: completionHandler)
+ finish(completion: .success(nil))
+
+ case .new:
+ logger.debug("Create new account...")
+
+ task = accountsProxy.createAccount(retryStrategy: .default) { [weak self] completion in
+ self?.dispatchQueue.async {
+ self?.didCreateAccount(completion: completion)
+ }
}
- // Explicit return.
- return
+ case .existing(let accountNumber):
+ logger.debug("Request account data...")
- case .failure(.lookupEntry(.itemNotFound)):
- break
+ task = accountsProxy.getAccountData(
+ accountNumber: accountNumber,
+ retryStrategy: .default,
+ completion: { [weak self] completion in
+ self?.dispatchQueue.async {
+ self?.didReceiveAccountData(accountNumber: accountNumber, completion: completion)
+ }
+ })
+ }
+ }
+
+ private func didCreateAccount(completion: OperationCompletion<REST.NewAccountData, REST.Error>) {
+ let mappedCompletion = completion.mapError { error -> TunnelManager.Error in
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to create new account."
+ )
- case .failure(let error):
- logger.error(chainedError: error, message: "Failed to read leftover tunnel settings.")
+ return .createAccount(error)
+ }
+
+ guard let newAccountData = mappedCompletion.value else {
+ finish(completion: mappedCompletion.assertNoSuccess())
+ return
}
- addTunnelSettingsAndPushKey(accountToken: accountToken, completionHandler: completionHandler)
+ logger.debug("Created new account. Updating settings...")
+
+ createDevice(
+ storedAccountData: StoredAccountData(
+ identifier: newAccountData.id,
+ number: newAccountData.number,
+ expiry: newAccountData.expiry
+ )
+ )
}
- private func addTunnelSettingsAndPushKey(accountToken: String, completionHandler: @escaping CompletionHandler) {
- switch addTunnelSettings(accountToken: accountToken) {
- case .success(let tunnelSettings):
- self.pushNewAccountKey(
- accountToken: accountToken,
- publicKey: tunnelSettings.interface.publicKey,
- completionHandler: completionHandler
+ private func didReceiveAccountData(accountNumber: String, completion: OperationCompletion<REST.AccountData, REST.Error>) {
+ let mappedCompletion = completion.mapError { error -> TunnelManager.Error in
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to receive account data."
)
- case .failure(let error):
- logger.error(chainedError: error, message: "Failed to add tunnel settings for new account.")
- completionHandler(.failure(error))
+ return .getAccountData(error)
}
- }
- private func addTunnelSettings(accountToken: String) -> Result<TunnelSettings, TunnelManager.Error> {
- return TunnelSettingsManager.remove(searchTerm: .accountToken(accountToken))
- .flatMapError { error in
- if case .removeEntry(.itemNotFound) = error {
- return .success(())
- } else {
- return .failure(.removeTunnelSettings(error))
- }
- }
- .flatMap { _ in
- let defaultSettings = TunnelSettings()
+ guard let accountData = mappedCompletion.value else {
+ finish(completion: mappedCompletion.assertNoSuccess())
+ return
+ }
- return TunnelSettingsManager.add(configuration: defaultSettings, account: accountToken)
- .map { _ in
- return defaultSettings
- }
- .mapError { error in
- return .addTunnelSettings(error)
- }
- }
+ logger.debug("Received account data.")
+
+ createDevice(
+ storedAccountData: StoredAccountData(
+ identifier: accountData.id,
+ number: accountNumber,
+ expiry: accountData.expiry
+ )
+ )
}
- private func deletePublicKeys(_ publicKeys: [PublicKey], accountToken: String, completionHandler: @escaping () -> Void) {
- let dispatchGroup = DispatchGroup()
+ private func createDevice(storedAccountData: StoredAccountData) {
+ logger.debug("Store last used account.")
- for (index, publicKey) in publicKeys.enumerated() {
- dispatchGroup.enter()
- _ = apiProxy.deleteWireguardKey(accountNumber: accountToken, publicKey: publicKey, retryStrategy: .default) { result in
- self.dispatchQueue.async {
- switch result {
- case .success:
- self.logger.info("Removed key (\(index)) from server.")
+ do {
+ try SettingsManager.setLastUsedAccount(storedAccountData.number)
+ } catch {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to store last used account number."
+ )
+ }
- case .failure(.unhandledResponse(_, let serverErrorResponse))
- where serverErrorResponse?.code == .publicKeyNotFound:
- self.logger.debug("Key (\(index)) was not found on server.")
+ logger.debug("Create device...")
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to delete key (\(index)) on server.")
+ let privateKey = PrivateKey()
- case .cancelled:
- self.logger.debug("Cancelled public key deletion.")
- }
+ let request = REST.CreateDeviceRequest(
+ publicKey: privateKey.publicKey,
+ hijackDNS: false
+ )
- dispatchGroup.leave()
+ task = devicesProxy.createDevice(
+ accountNumber: storedAccountData.number,
+ request: request,
+ retryStrategy: .default,
+ completion: { [weak self] completion in
+ self?.dispatchQueue.async {
+ self?.didCreateDevice(
+ storedAccountData: storedAccountData,
+ privateKey: privateKey,
+ completion: completion
+ )
}
- }
+ })
+ }
+
+ private func didCreateDevice(
+ storedAccountData: StoredAccountData,
+ privateKey: PrivateKey,
+ completion: OperationCompletion<REST.Device, REST.Error>
+ )
+ {
+ let mappedCompletion = completion.mapError { error -> TunnelManager.Error in
+ logger.error(chainedError: error, message: "Failed to create device.")
+ return .createDevice(error)
}
- dispatchGroup.notify(queue: dispatchQueue) {
- completionHandler()
+ guard let device = mappedCompletion.value else {
+ finish(completion: mappedCompletion.assertNoSuccess())
+ return
+ }
+
+ logger.debug("Created device. Saving settings...")
+
+ let tunnelSettings = TunnelSettingsV2(
+ account: storedAccountData,
+ device: StoredDeviceData(
+ creationDate: device.created,
+ identifier: device.id,
+ name: device.name,
+ hijackDNS: device.hijackDNS,
+ ipv4Address: device.ipv4Address,
+ ipv6Address: device.ipv6Address,
+ wgKeyData: StoredWgKeyData(
+ creationDate: Date(),
+ privateKey: privateKey
+ )
+ ),
+ relayConstraints: RelayConstraints(),
+ dnsSettings: DNSSettings()
+ )
+
+ do {
+ try SettingsManager.writeSettings(tunnelSettings)
+
+ state.tunnelSettings = tunnelSettings
+
+ finish(completion: .success(storedAccountData))
+ } catch {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to write settings."
+ )
+ finish(completion: .failure(.writeSettings(error)))
}
}
- private func deleteKeychainEntryAndVPNConfiguration(accountToken: String, completionHandler: @escaping () -> Void) {
+ private func deleteSettingsAndVPNConfiguration(
+ completionHandler: @escaping (TunnelManager.Error?) -> Void
+ ) {
+ // Delete keychain entry.
+ do {
+ try SettingsManager.deleteSettings()
+ } catch .itemNotFound as KeychainError {
+ logger.debug("Settings are already deleted.")
+ } catch {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to delete settings."
+ )
+ completionHandler(.deleteSettings(error))
+ return
+ }
+
// Tell the caller to unsubscribe from VPN status notifications.
willDeleteVPNConfigurationHandler?()
willDeleteVPNConfigurationHandler = nil
@@ -182,23 +315,12 @@ class SetAccountOperation: ResultOperation<(), TunnelManager.Error> {
// Reset tunnel state to disconnected
state.tunnelStatus.reset(to: .disconnected)
- // Remove tunnel info
- state.tunnelInfo = nil
-
- // Remove settings from Keychain
- if case .failure(let error) = TunnelSettingsManager.remove(searchTerm: .accountToken(accountToken)) {
- // Ignore Keychain errors because that normally means that the Keychain
- // configuration was already removed and we shouldn't be blocking the
- // user from logging out
- logger.error(
- chainedError: error,
- message: "Failed to delete old account settings."
- )
- }
+ // Remove tunnel settins
+ state.tunnelSettings = nil
// Finish immediately if tunnel provider is not set.
guard let tunnel = state.tunnel else {
- completionHandler()
+ completionHandler(nil)
return
}
@@ -215,62 +337,8 @@ class SetAccountOperation: ResultOperation<(), TunnelManager.Error> {
self.state.setTunnel(nil, shouldRefreshTunnelState: false)
- completionHandler()
- }
- }
- }
-
- private func pushNewAccountKey(accountToken: String, publicKey: PublicKey, completionHandler: @escaping CompletionHandler) {
- _ = apiProxy.pushWireguardKey(accountNumber: accountToken, publicKey: publicKey, retryStrategy: .default) { result in
- self.dispatchQueue.async {
- switch result {
- case .success(let associatedAddresses):
- self.logger.debug("Pushed new key to server.")
-
- self.saveAssociatedAddresses(associatedAddresses, accountToken: accountToken, newPrivateKey: nil, completionHandler: completionHandler)
-
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to push new key to server.")
-
- completionHandler(.failure(.pushWireguardKey(error)))
-
- case .cancelled:
- self.logger.debug("Cancelled new key push to server.")
-
- completionHandler(.cancelled)
- }
+ completionHandler(nil)
}
}
}
-
- private func saveAssociatedAddresses(_ associatedAddresses: REST.WireguardAddressesResponse, accountToken: String, newPrivateKey: PrivateKeyWithMetadata?, completionHandler: @escaping (OperationCompletion<(), TunnelManager.Error>) -> Void) {
- let saveResult = TunnelSettingsManager.update(searchTerm: .accountToken(accountToken)) { tunnelSettings in
- tunnelSettings.interface.addresses = [
- associatedAddresses.ipv4Address,
- associatedAddresses.ipv6Address
- ]
-
- if let newPrivateKey = newPrivateKey {
- tunnelSettings.interface.privateKey = newPrivateKey
- tunnelSettings.interface.nextPrivateKey = nil
- }
- }
-
- switch saveResult {
- case .success(let newTunnelSettings):
- logger.debug("Saved associated addresses.")
-
- state.tunnelInfo = TunnelInfo(
- token: accountToken,
- tunnelSettings: newTunnelSettings
- )
-
- completionHandler(.success(()))
-
- case .failure(let error):
- logger.error(chainedError: error, message: "Failed to save associated addresses.")
-
- completionHandler(.failure(.updateTunnelSettings(error)))
- }
- }
}
diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
index 8b72dbcea7..0bcfa6458b 100644
--- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
@@ -33,8 +33,8 @@ class StartTunnelOperation: ResultOperation<(), TunnelManager.Error> {
}
override func main() {
- guard let tunnelInfo = state.tunnelInfo else {
- self.finish(completion: .failure(.unsetAccount))
+ guard let tunnelSettings = state.tunnelSettings else {
+ finish(completion: .failure(.unsetAccount))
return
}
@@ -50,7 +50,7 @@ class StartTunnelOperation: ResultOperation<(), TunnelManager.Error> {
switch readResult {
case .success(let cachedRelays):
self.didReceiveRelays(
- tunnelInfo: tunnelInfo,
+ tunnelSettings: tunnelSettings,
cachedRelays: cachedRelays
)
@@ -66,10 +66,10 @@ class StartTunnelOperation: ResultOperation<(), TunnelManager.Error> {
}
}
- private func didReceiveRelays(tunnelInfo: TunnelInfo, cachedRelays: RelayCache.CachedRelays) {
+ private func didReceiveRelays(tunnelSettings: TunnelSettingsV2, cachedRelays: RelayCache.CachedRelays) {
let selectorResult = RelaySelector.evaluate(
relays: cachedRelays.relays,
- constraints: tunnelInfo.tunnelSettings.relayConstraints
+ constraints: tunnelSettings.relayConstraints
)
guard let selectorResult = selectorResult else {
@@ -77,13 +77,16 @@ class StartTunnelOperation: ResultOperation<(), TunnelManager.Error> {
return
}
- Self.makeTunnelProvider(accountToken: tunnelInfo.token) { makeTunnelProviderResult in
+ Self.makeTunnelProvider { makeTunnelProviderResult in
self.dispatchQueue.async {
switch makeTunnelProviderResult {
case .success(let tunnelProvider):
let startTunnelResult = Result { try self.startTunnel(tunnelProvider: tunnelProvider, selectorResult: selectorResult) }
+ .mapError { error -> TunnelManager.Error in
+ return .startVPNTunnel(error)
+ }
- self.finish(completion: OperationCompletion(result: startTunnelResult.mapError { .startVPNTunnel($0) }))
+ self.finish(completion: OperationCompletion(result: startTunnelResult))
case .failure(let error):
self.finish(completion: .failure(error))
@@ -109,22 +112,27 @@ class StartTunnelOperation: ResultOperation<(), TunnelManager.Error> {
try tunnelProvider.connection.startVPNTunnel(options: tunnelOptions.rawOptions())
}
- private class func makeTunnelProvider(accountToken: String, completionHandler: @escaping (Result<TunnelProviderManagerType, TunnelManager.Error>) -> Void) {
+ private class func makeTunnelProvider(completionHandler: @escaping (Result<TunnelProviderManagerType, TunnelManager.Error>) -> Void) {
TunnelProviderManagerType.loadAllFromPreferences { tunnelProviders, error in
if let error = error {
completionHandler(.failure(.loadAllVPNConfigurations(error)))
return
}
- let result = Self.setupTunnelProvider(
- accountToken: accountToken,
- tunnels: tunnelProviders
- )
+ let protocolConfig = NETunnelProviderProtocol()
+ protocolConfig.providerBundleIdentifier = ApplicationConfiguration.packetTunnelExtensionIdentifier
+ protocolConfig.serverAddress = ""
- guard case .success(let tunnelProvider) = result else {
- completionHandler(result)
- return
- }
+ let tunnelProvider = tunnelProviders?.first ?? TunnelProviderManagerType()
+ tunnelProvider.isEnabled = true
+ tunnelProvider.localizedDescription = "WireGuard"
+ tunnelProvider.protocolConfiguration = protocolConfig
+
+ // Enable on-demand VPN, always connect the tunnel when on Wi-Fi or cellular.
+ let alwaysOnRule = NEOnDemandRuleConnect()
+ alwaysOnRule.interfaceTypeMatch = .any
+ tunnelProvider.onDemandRules = [alwaysOnRule]
+ tunnelProvider.isOnDemandEnabled = true
tunnelProvider.saveToPreferences { error in
if let error = error {
@@ -146,34 +154,4 @@ class StartTunnelOperation: ResultOperation<(), TunnelManager.Error> {
}
}
}
-
- private class func setupTunnelProvider(accountToken: String, tunnels: [TunnelProviderManagerType]?) -> Result<TunnelProviderManagerType, TunnelManager.Error> {
- // Request persistent keychain reference to tunnel settings
- return TunnelSettingsManager.getPersistentKeychainReference(account: accountToken)
- .mapError { error in
- return .obtainPersistentKeychainReference(error)
- }
- .map { passwordReference in
- // Get the first available tunnel or make a new one
- let tunnelProvider = tunnels?.first ?? TunnelProviderManagerType()
-
- let protocolConfig = NETunnelProviderProtocol()
- protocolConfig.providerBundleIdentifier = ApplicationConfiguration.packetTunnelExtensionIdentifier
- protocolConfig.serverAddress = ""
- protocolConfig.username = accountToken
- protocolConfig.passwordReference = passwordReference
-
- tunnelProvider.isEnabled = true
- tunnelProvider.localizedDescription = "WireGuard"
- tunnelProvider.protocolConfiguration = protocolConfig
-
- // Enable on-demand VPN, always connect the tunnel when on Wi-Fi or cellular
- let alwaysOnRule = NEOnDemandRuleConnect()
- alwaysOnRule.interfaceTypeMatch = .any
- tunnelProvider.onDemandRules = [alwaysOnRule]
- tunnelProvider.isOnDemandEnabled = true
-
- return tunnelProvider
- }
- }
}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelInfo.swift b/ios/MullvadVPN/TunnelManager/TunnelInfo.swift
deleted file mode 100644
index e16ab22632..0000000000
--- a/ios/MullvadVPN/TunnelManager/TunnelInfo.swift
+++ /dev/null
@@ -1,18 +0,0 @@
-//
-// TunnelInfo.swift
-// TunnelInfo
-//
-// Created by pronebird on 10/09/2021.
-// Copyright © 2021 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-
-/// Struct that holds current account token and tunnel settings.
-struct TunnelInfo: Equatable {
- /// Mullvad account token
- var token: String
-
- /// Tunnel settings
- var tunnelSettings: TunnelSettings
-}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
index 2b216b409e..71a7c3a4a9 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
@@ -10,6 +10,7 @@ import BackgroundTasks
import Foundation
import NetworkExtension
import UIKit
+import StoreKit
import Logging
import class WireGuardKitTypes.PublicKey
@@ -47,12 +48,16 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
static let shared: TunnelManager = {
- return TunnelManager(apiProxy: REST.ProxyFactory.shared.createAPIProxy())
+ return TunnelManager(
+ accountsProxy: REST.ProxyFactory.shared.createAccountsProxy(),
+ devicesProxy: REST.ProxyFactory.shared.createDevicesProxy()
+ )
}()
// MARK: - Internal variables
- private let apiProxy: REST.APIProxy
+ private let accountsProxy: REST.AccountsProxy
+ private let devicesProxy: REST.DevicesProxy
private let logger = Logger(label: "TunnelManager")
private let stateQueue = DispatchQueue(label: "TunnelManager.stateQueue")
@@ -72,18 +77,37 @@ final class TunnelManager: TunnelManagerStateDelegate {
private var isPolling = false
private var lastConnectingDate: Date?
- var tunnelInfo: TunnelInfo? {
- return state.tunnelInfo
+ var accountNumber: String? {
+ return state.tunnelSettings?.account.number
+ }
+
+ var accountExpiry: Date? {
+ return state.tunnelSettings?.account.expiry
+ }
+
+ var isAccountSet: Bool {
+ return state.tunnelSettings != nil
+ }
+
+ var device: StoredDeviceData? {
+ return state.tunnelSettings?.device
+ }
+
+ var tunnelSettings: TunnelSettingsV2? {
+ return state.tunnelSettings
}
var tunnelState: TunnelState {
return state.tunnelStatus.state
}
- private init(apiProxy: REST.APIProxy) {
- self.apiProxy = apiProxy
+ private init(accountsProxy: REST.AccountsProxy, devicesProxy: REST.DevicesProxy) {
+ self.accountsProxy = accountsProxy
+ self.devicesProxy = devicesProxy
self.state = TunnelManager.State(queue: stateQueue)
self.state.delegate = self
+ self.operationQueue.name = "TunnelManager.operationQueue"
+ self.operationQueue.underlyingQueue = stateQueue
NotificationCenter.default.addObserver(
self,
@@ -125,9 +149,12 @@ final class TunnelManager: TunnelManagerStateDelegate {
guard self.isRunningPeriodicPrivateKeyRotation else { return }
- if let tunnelInfo = self.state.tunnelInfo {
- let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate
- let scheduleDate = Date(timeInterval: TunnelManagerConfiguration.privateKeyRotationInterval, since: creationDate)
+ if let tunnelSettings = state.tunnelSettings {
+ let creationDate = tunnelSettings.device.wgKeyData.creationDate
+ let scheduleDate = Date(
+ timeInterval: TunnelManagerConfiguration.privateKeyRotationInterval,
+ since: creationDate
+ )
schedulePrivateKeyRotationTimer(scheduleDate)
} else {
@@ -171,12 +198,17 @@ final class TunnelManager: TunnelManagerStateDelegate {
// MARK: - Public methods
- /// Initialize the TunnelManager with the tunnel from the system.
- ///
- /// The given account token is used to ensure that the system tunnel was configured for the same
- /// account. The system tunnel is removed in case of inconsistency.
- func loadTunnel(accountToken: String?, completionHandler: @escaping (TunnelManager.Error?) -> Void) {
- let operation = LoadTunnelOperation(dispatchQueue: stateQueue, state: state, accountToken: accountToken) { [weak self] completion in
+ func loadConfiguration(completionHandler: @escaping (TunnelManager.Error?) -> Void) {
+ let migrateSettingsOperation = MigrateSettingsOperation(
+ dispatchQueue: stateQueue,
+ accountsProxy: accountsProxy,
+ devicesProxy: devicesProxy
+ )
+
+ let loadTunnelOperation = LoadTunnelConfigurationOperation(
+ dispatchQueue: stateQueue,
+ state: state
+ ) { [weak self] completion in
guard let self = self else { return }
dispatchPrecondition(condition: .onQueue(self.stateQueue))
@@ -192,17 +224,31 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
}
- let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Load tunnel") {
- operation.cancel()
+ let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(
+ withName: "Load tunnel configuration"
+ ) {
+ // no-op
}
- operation.completionBlock = {
+ loadTunnelOperation.completionBlock = {
UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
}
- exclusivityController.addOperation(operation, categories: [OperationCategory.manageTunnelProvider, OperationCategory.changeTunnelSettings])
+ exclusivityController.addOperation(migrateSettingsOperation, categories: [
+ OperationCategory.changeTunnelSettings
+ ])
- operationQueue.addOperation(operation)
+ exclusivityController.addOperation(loadTunnelOperation, categories: [
+ OperationCategory.manageTunnelProvider,
+ OperationCategory.changeTunnelSettings
+ ])
+
+ loadTunnelOperation.addDependency(migrateSettingsOperation)
+
+ operationQueue.addOperations([
+ migrateSettingsOperation,
+ loadTunnelOperation
+ ], waitUntilFinished: false)
}
func startTunnel() {
@@ -241,7 +287,10 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
func stopTunnel() {
- let operation = StopTunnelOperation(dispatchQueue: stateQueue, state: state) { [weak self] completion in
+ let operation = StopTunnelOperation(
+ dispatchQueue: stateQueue,
+ state: state
+ ) { [weak self] completion in
guard let self = self else { return }
dispatchPrecondition(condition: .onQueue(self.stateQueue))
@@ -309,14 +358,38 @@ final class TunnelManager: TunnelManagerStateDelegate {
operationQueue.addOperation(operation)
}
- func setAccount(accountToken: String, completionHandler: @escaping (TunnelManager.Error?) -> Void) {
- let operation = makeSetAccountOperation(accountToken: accountToken) { completion in
- DispatchQueue.main.async {
- completionHandler(completion.error)
- }
- }
+ func setAccount(action: SetAccountAction, completionHandler: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void) {
+ let operation = SetAccountOperation(
+ dispatchQueue: stateQueue,
+ state: state,
+ accountsProxy: accountsProxy,
+ devicesProxy: devicesProxy,
+ action: action,
+ willDeleteVPNConfigurationHandler: { [weak self] in
+ guard let self = self else { return }
+
+ dispatchPrecondition(condition: .onQueue(self.stateQueue))
- let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Set tunnel account") {
+ // Unregister from receiving VPN connection status changes
+ self.unsubscribeVPNStatusObserver()
+
+ // Cancel last VPN status mapping operation
+ self.lastMapConnectionStatusOperation?.cancel()
+ self.lastMapConnectionStatusOperation = nil
+ },
+ completionHandler: { [weak self] completion in
+ guard let self = self else { return }
+
+ dispatchPrecondition(condition: .onQueue(self.stateQueue))
+
+ self.updatePrivateKeyRotationTimer()
+
+ DispatchQueue.main.async {
+ completionHandler(completion)
+ }
+ })
+
+ let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: action.taskName) {
operation.cancel()
}
@@ -324,19 +397,33 @@ final class TunnelManager: TunnelManagerStateDelegate {
UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
}
- exclusivityController.addOperation(operation, categories: [OperationCategory.manageTunnelProvider, OperationCategory.changeTunnelSettings])
+ exclusivityController.addOperation(operation, categories: [
+ OperationCategory.manageTunnelProvider,
+ OperationCategory.changeTunnelSettings
+ ])
operationQueue.addOperation(operation)
}
func unsetAccount(completionHandler: @escaping () -> Void) {
- let operation = makeSetAccountOperation(accountToken: nil) { _ in
- DispatchQueue.main.async {
- completionHandler()
- }
+ setAccount(action: .unset) { _ in
+ completionHandler()
}
+ }
+
+ func updateAccountData(_ completionHandler: ((TunnelManager.Error?) -> Void)? = nil) {
+ let operation = UpdateAccountDataOperation(
+ dispatchQueue: stateQueue,
+ state: state,
+ accountsProxy: accountsProxy
+ )
- let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Unset tunnel account") {
+ operation.completionQueue = .main
+ operation.completionHandler = { completion in
+ completionHandler?(completion.error)
+ }
+
+ let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Update account data") {
operation.cancel()
}
@@ -344,13 +431,47 @@ final class TunnelManager: TunnelManagerStateDelegate {
UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
}
- exclusivityController.addOperation(operation, categories: [OperationCategory.manageTunnelProvider, OperationCategory.changeTunnelSettings])
+ exclusivityController.addOperation(operation, categories: [
+ OperationCategory.changeTunnelSettings
+ ])
operationQueue.addOperation(operation)
}
+ func updateDeviceData(_ completionHandler: @escaping (OperationCompletion<StoredDeviceData, TunnelManager.Error>) -> Void) -> Cancellable {
+ let operation = UpdateDeviceDataOperation(
+ dispatchQueue: stateQueue,
+ state: state,
+ devicesProxy: devicesProxy
+ )
+
+ operation.completionQueue = .main
+ operation.completionHandler = completionHandler
+
+ let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Update device data") {
+ operation.cancel()
+ }
+
+ operation.completionBlock = {
+ UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
+ }
+
+ exclusivityController.addOperation(operation, categories: [
+ OperationCategory.changeTunnelSettings
+ ])
+
+ operationQueue.addOperation(operation)
+
+ return operation
+ }
+
func regeneratePrivateKey(completionHandler: ((TunnelManager.Error?) -> Void)? = nil) {
- let operation = ReplaceKeyOperation.operationForKeyRegeneration(dispatchQueue: stateQueue, state: state, apiProxy: apiProxy) { [weak self] completion in
+ 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))
@@ -386,10 +507,10 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
func rotatePrivateKey(completionHandler: @escaping (OperationCompletion<KeyRotationResult, TunnelManager.Error>) -> Void) -> Cancellable {
- let operation = ReplaceKeyOperation.operationForKeyRotation(
+ let operation = RotateKeyOperation(
dispatchQueue: stateQueue,
state: state,
- apiProxy: apiProxy,
+ devicesProxy: devicesProxy,
rotationInterval: TunnelManagerConfiguration.privateKeyRotationInterval
) { [weak self] completion in
guard let self = self else { return }
@@ -445,7 +566,7 @@ final class TunnelManager: TunnelManagerStateDelegate {
scheduleTunnelSettingsUpdate(
taskName: "Set DNS settings",
modificationBlock: { tunnelSettings in
- tunnelSettings.interface.dnsSettings = newDNSSettings
+ tunnelSettings.dnsSettings = newDNSSettings
},
completionHandler: completionHandler
)
@@ -467,10 +588,10 @@ final class TunnelManager: TunnelManagerStateDelegate {
// MARK: - TunnelManagerStateDelegate
- func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelInfo newTunnelInfo: TunnelInfo?) {
+ func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelSettings newTunnelSettinggs: TunnelSettingsV2?) {
DispatchQueue.main.async {
- self.observerList.forEach { (observer) in
- observer.tunnelManager(self, didUpdateTunnelSettings: newTunnelInfo)
+ self.observerList.forEach { observer in
+ observer.tunnelManager(self, didUpdateTunnelSettings: newTunnelSettinggs)
}
}
}
@@ -545,7 +666,11 @@ final class TunnelManager: TunnelManagerStateDelegate {
private func updateTunnelStatus(_ connectionStatus: NEVPNStatus) {
dispatchPrecondition(condition: .onQueue(stateQueue))
- let operation = MapConnectionStatusOperation(queue: stateQueue, state: state, connectionStatus: connectionStatus) { [weak self] in
+ let operation = MapConnectionStatusOperation(
+ queue: stateQueue,
+ state: state,
+ connectionStatus: connectionStatus
+ ) { [weak self] in
guard let self = self else { return }
dispatchPrecondition(condition: .onQueue(self.stateQueue))
@@ -569,60 +694,38 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
}
- private func makeSetAccountOperation(accountToken: String?, completionHandler: @escaping (OperationCompletion<(), TunnelManager.Error>) -> Void) -> Operation {
- return SetAccountOperation(
- dispatchQueue: stateQueue,
- state: state,
- apiProxy: apiProxy,
- accountToken: accountToken,
- willDeleteVPNConfigurationHandler: { [weak self] in
- guard let self = self else { return }
-
- dispatchPrecondition(condition: .onQueue(self.stateQueue))
-
- // Unregister from receiving VPN connection status changes
- self.unsubscribeVPNStatusObserver()
-
- // Cancel last VPN status mapping operation
- self.lastMapConnectionStatusOperation?.cancel()
- self.lastMapConnectionStatusOperation = nil
- },
- completionHandler: { [weak self] completion in
- guard let self = self else { return }
-
- dispatchPrecondition(condition: .onQueue(self.stateQueue))
-
- self.updatePrivateKeyRotationTimer()
-
- completionHandler(completion)
- })
- }
+ fileprivate func scheduleTunnelSettingsUpdate(taskName: String, modificationBlock: @escaping (inout TunnelSettingsV2) -> Void, completionHandler: @escaping (TunnelManager.Error?) -> Void) {
+ let operation = ResultBlockOperation<Void, TunnelManager.Error>(
+ dispatchQueue: stateQueue
+ ) { operation in
+ guard var tunnelSettings = self.tunnelSettings else {
+ operation.finish(completion: .failure(.unsetAccount))
+ return
+ }
- private func scheduleTunnelSettingsUpdate(taskName: String, modificationBlock: @escaping (inout TunnelSettings) -> Void, completionHandler: @escaping (TunnelManager.Error?) -> Void) {
- let operation = SetTunnelSettingsOperation(
- dispatchQueue: stateQueue,
- state: state,
- modificationBlock: modificationBlock,
- completionHandler: { [weak self] completion in
- guard let self = self else { return }
+ do {
+ modificationBlock(&tunnelSettings)
- dispatchPrecondition(condition: .onQueue(self.stateQueue))
+ try SettingsManager.writeSettings(tunnelSettings)
- switch completion {
- case .success:
- self.reconnectTunnel(completionHandler: nil)
+ self.state.tunnelSettings = tunnelSettings
+ self.reconnectTunnel(completionHandler: nil)
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to set tunnel settings.")
+ operation.finish(completion: .success(()))
+ } catch {
+ self.logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to write settings."
+ )
- case .cancelled:
- break
- }
+ operation.finish(completion: .failure(.writeSettings(error)))
+ }
+ }
- DispatchQueue.main.async {
- completionHandler(completion.error)
- }
- })
+ operation.completionQueue = .main
+ operation.completionHandler = { completion in
+ completionHandler(completion.error)
+ }
let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: taskName) {
operation.cancel()
@@ -753,29 +856,29 @@ extension TunnelManager {
}
/// Schedule background task relative to the private key creation date.
- func scheduleBackgroundTask() -> Result<(), TunnelManager.Error> {
- if let tunnelInfo = self.state.tunnelInfo {
- let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate
- let beginDate = Date(timeInterval: TunnelManagerConfiguration.privateKeyRotationInterval, since: creationDate)
-
- return submitBackgroundTask(at: beginDate)
- } else {
- return .failure(.unsetAccount)
+ 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) -> Result<(), TunnelManager.Error> {
+ private func submitBackgroundTask(at beginDate: Date) throws {
let taskIdentifier = ApplicationConfiguration.privateKeyRotationTaskIdentifier
let request = BGProcessingTaskRequest(identifier: taskIdentifier)
request.earliestBeginDate = beginDate
request.requiresNetworkConnectivity = true
- return Result { try BGTaskScheduler.shared.submit(request) }
- .mapError { error in
- return .backgroundTaskScheduler(error)
- }
+ try BGTaskScheduler.shared.submit(request)
}
/// Background task handler.
@@ -785,12 +888,17 @@ extension TunnelManager {
let cancellableTask = rotatePrivateKey { completion in
if let scheduleDate = self.handlePrivateKeyRotationCompletion(completion) {
// Schedule next background task
- switch self.submitBackgroundTask(at: scheduleDate) {
- case .success:
- self.logger.debug("Scheduled next private key rotation task at \(scheduleDate.logFormatDate())")
+ do {
+ try self.submitBackgroundTask(at: scheduleDate)
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to schedule next private key rotation task.")
+ 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."
+ )
}
}
@@ -845,9 +953,10 @@ extension TunnelManager {
// Do not retry if logged out.
return nil
- case .replaceWireguardKey(.unhandledResponse(_, let serverErrorResponse))
- where serverErrorResponse?.code == .invalidAccount:
- // Do not retry if account was removed.
+ case .rotateKey(.unhandledResponse(_, let serverErrorResponse))
+ where serverErrorResponse?.code == .invalidAccount ||
+ serverErrorResponse?.code == .deviceNotFound:
+ // Do not retry if account or device were removed.
return nil
default:
@@ -855,3 +964,39 @@ extension TunnelManager {
}
}
}
+
+extension TunnelManager: AppStorePaymentObserver {
+ func appStorePaymentManager(_ manager: AppStorePaymentManager,
+ transaction: SKPaymentTransaction?,
+ payment: SKPayment,
+ accountToken: String?,
+ didFailWithError error: AppStorePaymentManager.Error
+ )
+ {
+ // no-op
+ }
+
+ func appStorePaymentManager(_ manager: AppStorePaymentManager,
+ transaction: SKPaymentTransaction,
+ accountToken: String,
+ didFinishWithResponse response: REST.CreateApplePaymentResponse
+ )
+ {
+ scheduleTunnelSettingsUpdate(
+ taskName: "Update account expiry after in-app purchase",
+ modificationBlock: { tunnelSettings in
+ if tunnelSettings.account.number == accountToken {
+ tunnelSettings.account.expiry = response.newExpiry
+ }
+ },
+ completionHandler: { error in
+ guard let error = error else { return }
+
+ self.logger.error(
+ chainedError: error,
+ message: "Failed to update account expiry after in-app purchase"
+ )
+ }
+ )
+ }
+}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift b/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift
index 4f0b346417..fb1fd03c7f 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift
@@ -14,62 +14,58 @@ extension TunnelManager {
/// Account is unset.
case unsetAccount
- /// A failure to start the VPN tunnel via system call.
+ /// Failure to start the VPN tunnel via system call.
case startVPNTunnel(Swift.Error)
- /// A failure to load the system VPN configurations created by the app.
+ /// Failure to load the system VPN configurations created by the app.
case loadAllVPNConfigurations(Swift.Error)
- /// A failure to save the system VPN configuration.
+ /// Failure to save the system VPN configuration.
case saveVPNConfiguration(Swift.Error)
- /// A failure to reload the system VPN configuration.
+ /// Failure to reload the system VPN configuration.
case reloadVPNConfiguration(Swift.Error)
- /// A failure to remove the system VPN configuration.
+ /// Failure to remove the system VPN configuration.
case removeVPNConfiguration(Swift.Error)
- /// A failure to perform a recovery (by removing the VPN configuration) when a corrupt
- /// VPN configuration is detected.
- case removeInconsistentVPNConfiguration(Swift.Error)
+ /// Failure to read settings.
+ case readSettings(Swift.Error)
- /// A failure to read tunnel settings.
- case readTunnelSettings(TunnelSettingsManager.Error)
+ /// Failure to write settings.
+ case writeSettings(Swift.Error)
- /// A failure to read relays cache.
+ /// Failure to delete settings.
+ case deleteSettings(Swift.Error)
+
+ /// Failure to read relays cache.
case readRelays(RelayCache.Error)
- /// A failure to find a relay satisfying the given constraints.
+ /// Failure to find a relay satisfying the given constraints.
case cannotSatisfyRelayConstraints
- /// A failure to add the tunnel settings.
- case addTunnelSettings(TunnelSettingsManager.Error)
-
- /// A failure to update the tunnel settings.
- case updateTunnelSettings(TunnelSettingsManager.Error)
-
- /// A failure to remove the tunnel settings from Keychain.
- case removeTunnelSettings(TunnelSettingsManager.Error)
+ /// Failure to create device.
+ case createDevice(REST.Error)
- /// A failure to migrate tunnel settings.
- case migrateTunnelSettings(TunnelSettingsManager.Error)
+ /// Failure to delete device.
+ case deleteDevice(REST.Error)
- /// Unable to obtain the persistent keychain reference for the tunnel settings.
- case obtainPersistentKeychainReference(TunnelSettingsManager.Error)
+ /// Failure to obtain device data.
+ case getDevice(REST.Error)
- /// A failure to push the public WireGuard key.
- case pushWireguardKey(REST.Error)
+ /// Requested device is already revoked.
+ case deviceRevoked
- /// A failure to replace the public WireGuard key.
- case replaceWireguardKey(REST.Error)
+ /// Failure to obtain account data.
+ case getAccountData(REST.Error)
- /// A failure to remove the public WireGuard key.
- case removeWireguardKey(REST.Error)
+ /// Failure to create account.
+ case createAccount(REST.Error)
- /// A failure to schedule background task.
- case backgroundTaskScheduler(Swift.Error)
+ /// Failure to rotate WireGuard key.
+ case rotateKey(REST.Error)
- /// A failure to reload tunnel.
+ /// Failure to reload tunnel.
case reloadTunnel(TunnelIPC.Error)
var errorDescription: String? {
@@ -86,32 +82,30 @@ extension TunnelManager {
return "Failed to reload the system VPN configuration."
case .removeVPNConfiguration:
return "Failed to remove the system VPN configuration."
- case .removeInconsistentVPNConfiguration:
- return "Failed to remove the inconsistent VPN tunnel."
- case .readTunnelSettings:
- return "Failed to read the tunnel settings."
+ case .readSettings:
+ return "Failed to read settings."
case .readRelays:
return "Failed to read relays."
case .cannotSatisfyRelayConstraints:
return "Failed to satisfy the relay constraints."
- case .addTunnelSettings:
- return "Failed to add the tunnel settings."
- case .updateTunnelSettings:
- return "Failed to update the tunnel settings."
- case .removeTunnelSettings:
- return "Failed to remove the tunnel settings."
- case .migrateTunnelSettings:
- return "Failed to migrate the tunnel settings."
- case .obtainPersistentKeychainReference:
- return "Failed to obtain the persistent keychain reference."
- case .pushWireguardKey:
- return "Failed to push the WireGuard key to server."
- case .replaceWireguardKey:
- return "Failed to replace the WireGuard key on server."
- case .removeWireguardKey:
- return "Failed to remove the WireGuard key from server."
- case .backgroundTaskScheduler:
- return "Failed to schedule background task."
+ case .writeSettings:
+ return "Failed to write settings."
+ case .deleteSettings:
+ return "Failed to delete settings."
+ case .createDevice:
+ return "Failed to create a device."
+ case .deleteDevice:
+ return "Failed to delete a device."
+ case .getDevice:
+ return "Failed to obtain device data."
+ case .deviceRevoked:
+ return "Requested device is already revoked."
+ case .getAccountData:
+ return "Failed to obtain account data."
+ case .createAccount:
+ return "Failed to create new account."
+ case .rotateKey:
+ return "Failed to rotate WireGuard key."
case .reloadTunnel:
return "Failed to reload tunnel."
}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift b/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift
index 3802390376..990e8e482f 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift
@@ -10,7 +10,7 @@ import Foundation
import NetworkExtension
protocol TunnelManagerStateDelegate: AnyObject {
- func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelInfo newTunnelInfo: TunnelInfo?)
+ 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)
}
@@ -23,7 +23,7 @@ extension TunnelManager {
private let queueMarkerKey = DispatchSpecificKey<Bool>()
- private var _tunnelInfo: TunnelInfo?
+ private var _tunnelSettings: TunnelSettingsV2?
private var _tunnelObject: Tunnel?
private var _tunnelStatus = TunnelStatus(
isNetworkReachable: false,
@@ -31,18 +31,18 @@ extension TunnelManager {
state: .disconnected
)
- var tunnelInfo: TunnelInfo? {
+ var tunnelSettings: TunnelSettingsV2? {
get {
return performBlock {
- return _tunnelInfo
+ return _tunnelSettings
}
}
set {
performBlock {
- if _tunnelInfo != newValue {
- _tunnelInfo = newValue
+ if _tunnelSettings != newValue {
+ _tunnelSettings = newValue
- delegate?.tunnelManagerState(self, didChangeTunnelInfo: newValue)
+ delegate?.tunnelManagerState(self, didChangeTunnelSettings: newValue)
}
}
}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelObserver.swift b/ios/MullvadVPN/TunnelManager/TunnelObserver.swift
index 5fefb4a89e..6e1934e30e 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelObserver.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelObserver.swift
@@ -10,6 +10,6 @@ import Foundation
protocol TunnelObserver: AnyObject {
func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState)
- func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelInfo: TunnelInfo?)
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?)
func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error)
}
diff --git a/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift b/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift
new file mode 100644
index 0000000000..ae0f0dea7a
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift
@@ -0,0 +1,87 @@
+//
+// UpdateAccountDataOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 12/05/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Logging
+
+class UpdateAccountDataOperation: ResultOperation<Void, TunnelManager.Error> {
+ private let logger = Logger(label: "UpdateAccountDataOperation")
+ private let state: TunnelManager.State
+ private let accountsProxy: REST.AccountsProxy
+ private var task: Cancellable?
+
+ init(
+ dispatchQueue: DispatchQueue,
+ state: TunnelManager.State,
+ accountsProxy: REST.AccountsProxy
+ )
+ {
+ self.state = state
+ self.accountsProxy = accountsProxy
+
+ super.init(dispatchQueue: dispatchQueue)
+ }
+
+ override func main() {
+ guard let tunnelSettings = state.tunnelSettings else {
+ finish(completion: .failure(.unsetAccount))
+ return
+ }
+
+ task = accountsProxy.getAccountData(
+ accountNumber: tunnelSettings.account.number,
+ retryStrategy: .default
+ ) { completion in
+ self.dispatchQueue.async {
+ self.didReceiveAccountData(
+ tunnelSettings: tunnelSettings,
+ completion: completion
+ )
+ }
+ }
+ }
+
+ override func operationDidCancel() {
+ task?.cancel()
+ task = nil
+ }
+
+ private func didReceiveAccountData(
+ tunnelSettings: TunnelSettingsV2,
+ completion: OperationCompletion<REST.AccountData, REST.Error>
+ )
+ {
+ let mappedCompletion = completion.mapError { error -> TunnelManager.Error in
+ self.logger.error(
+ chainedError: error,
+ message: "Failed to fetch account expiry."
+ )
+ return .getAccountData(error)
+ }
+
+ guard let accountData = mappedCompletion.value else {
+ finish(completion: mappedCompletion.assertNoSuccess())
+ return
+ }
+
+ do {
+ var newTunnelSettings = tunnelSettings
+ newTunnelSettings.account.expiry = accountData.expiry
+ try SettingsManager.writeSettings(newTunnelSettings)
+
+ finish(completion: .success(()))
+ } catch {
+ self.logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to save account data."
+ )
+
+ finish(completion: .failure(.writeSettings(error)))
+ }
+ }
+}
diff --git a/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift b/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift
new file mode 100644
index 0000000000..6c0fd08823
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift
@@ -0,0 +1,96 @@
+//
+// UpdateDeviceDataOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 13/05/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Logging
+import class WireGuardKitTypes.PublicKey
+
+class UpdateDeviceDataOperation: ResultOperation<StoredDeviceData, TunnelManager.Error> {
+ private let logger = Logger(label: "UpdateDeviceDataOperation")
+
+ private let state: TunnelManager.State
+ private let devicesProxy: REST.DevicesProxy
+
+ private var task: Cancellable?
+
+ init(
+ dispatchQueue: DispatchQueue,
+ state: TunnelManager.State,
+ devicesProxy: REST.DevicesProxy
+ )
+ {
+ self.state = state
+ self.devicesProxy = devicesProxy
+
+ super.init(dispatchQueue: dispatchQueue)
+ }
+
+ override func main() {
+ guard let tunnelSettings = state.tunnelSettings else {
+ finish(completion: .failure(.unsetAccount))
+ return
+ }
+
+ task = devicesProxy.getDevice(
+ accountNumber: tunnelSettings.account.number,
+ identifier: tunnelSettings.device.identifier,
+ retryStrategy: .default,
+ completion: { [weak self] completion in
+ self?.dispatchQueue.async {
+ self?.didReceiveDeviceResponse(
+ tunnelSettings: tunnelSettings,
+ completion: completion
+ )
+ }
+ })
+ }
+
+ override func operationDidCancel() {
+ task?.cancel()
+ task = nil
+ }
+
+ private func didReceiveDeviceResponse(
+ tunnelSettings: TunnelSettingsV2,
+ completion: OperationCompletion<REST.Device?, REST.Error>
+ ) {
+ let mappedCompletion = completion
+ .mapError { error -> TunnelManager.Error in
+ return .getDevice(error)
+ }
+ .flatMap { device -> OperationCompletion<REST.Device, TunnelManager.Error> in
+ if let device = device {
+ return .success(device)
+ } else {
+ return .failure(.deviceRevoked)
+ }
+ }
+
+ guard let device = mappedCompletion.value else {
+ finish(completion: mappedCompletion.assertNoSuccess())
+ return
+ }
+
+ do {
+ var newTunnelSettings = tunnelSettings
+ newTunnelSettings.device.update(from: device)
+
+ try SettingsManager.writeSettings(newTunnelSettings)
+
+ finish(completion: .success(newTunnelSettings.device))
+ } catch {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to write settings."
+ )
+
+ finish(completion: .failure(.writeSettings(error)))
+ }
+ }
+
+}
diff --git a/ios/MullvadVPN/TunnelSettingsManager.swift b/ios/MullvadVPN/TunnelSettingsManager.swift
deleted file mode 100644
index b4aec2b8ba..0000000000
--- a/ios/MullvadVPN/TunnelSettingsManager.swift
+++ /dev/null
@@ -1,258 +0,0 @@
-//
-// TunnelSettingsManager.swift
-// MullvadVPN
-//
-// Created by pronebird on 02/10/2019.
-// Copyright © 2019 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import Security
-
-/// Service name used for keychain items
-private let kServiceName = "Mullvad VPN"
-
-enum TunnelSettingsManager {}
-
-extension TunnelSettingsManager {
-
- enum Error: ChainedError {
- /// A failure to encode the given tunnel settings
- case encode(Swift.Error)
-
- /// A failure to decode the data stored in Keychain
- case decode(Swift.Error)
-
- /// A failure to add a new entry to Keychain
- case addEntry(Keychain.Error)
-
- /// A failure to update the existing entry in Keychain
- case updateEntry(Keychain.Error)
-
- /// A failure to remove an entry in Keychain
- case removeEntry(Keychain.Error)
-
- /// A failure to query the entry in Keychain
- case lookupEntry(Keychain.Error)
-
- /// Missing attributes required to perform an operation.
- case missingRequiredAttributes
-
- var errorDescription: String? {
- switch self {
- case .encode:
- return "Failure to encode settings."
- case .decode:
- return "Failure to decode settings."
- case .addEntry:
- return "Failure to add keychain entry."
- case .updateEntry:
- return "Failure to update keychain entry."
- case .removeEntry:
- return "Failure to remove keychain entry."
- case .lookupEntry:
- return "Failure to lookup keychain entry."
- case .missingRequiredAttributes:
- return "Keychain entry is missing required set of attributes."
- }
- }
- }
-
- typealias Result<T> = Swift.Result<T, Error>
-
- /// Keychain access level that should be used for all items containing tunnel settings
- private static let keychainAccessibleLevel = Keychain.Accessible.afterFirstUnlock
-
- enum KeychainSearchTerm {
- case accountToken(String)
- case persistentReference(Data)
-
- /// Returns `Keychain.Attributes` appropriate for adding or querying the item
- fileprivate func makeKeychainAttributes() -> Keychain.Attributes {
- var attributes = Keychain.Attributes()
- attributes.class = .genericPassword
-
- switch self {
- case .accountToken(let accountToken):
- attributes.account = accountToken
- attributes.service = kServiceName
-
- case .persistentReference(let persistentReference):
- attributes.valuePersistentReference = persistentReference
- }
-
- return attributes
- }
- }
-
- struct KeychainEntry {
- let accountToken: String
- let tunnelSettings: TunnelSettings
- }
-
- static func load(searchTerm: KeychainSearchTerm) -> Result<KeychainEntry> {
- var query = searchTerm.makeKeychainAttributes()
- query.return = [.data, .attributes]
-
- return Keychain.findFirst(query: query)
- .mapError { .lookupEntry($0) }
- .flatMap { (attributes) in
- guard let account = attributes?.account, let data = attributes?.valueData else {
- return .failure(.missingRequiredAttributes)
- }
-
- return Self.decode(data: data)
- .map { KeychainEntry(accountToken: account, tunnelSettings: $0) }
- }
- }
-
- static func add(configuration: TunnelSettings, account: String) -> Result<()> {
- Self.encode(tunnelConfig: configuration)
- .flatMap { (data) -> Result<()> in
- var attributes = KeychainSearchTerm.accountToken(account)
- .makeKeychainAttributes()
-
- // Share the item with the application group
- attributes.accessGroup = ApplicationConfiguration.securityGroupIdentifier
-
- // Make sure the keychain item is available after the first unlock to enable
- // automatic key rotation in background (from the packet tunnel process)
- attributes.accessible = Self.keychainAccessibleLevel
-
- // Store value
- attributes.valueData = data
-
- return Keychain.add(attributes)
- .mapError { .addEntry($0) }
- .map { _ in () }
- }
- }
-
- /// This is a migration path for the existing Keychain entries created by 2020.2 or before.
- ///
- /// - Set the appropriate `accessible` so that the Packet Tunnel can access the tunnel
- /// configuration when the device is locked.
- /// - Add revision field
- ///
- /// - Returns: A boolean that indicates whether the entry was up to date prior to the
- /// migration request.
-
- static func migrateKeychainEntry(searchTerm: KeychainSearchTerm) -> Result<Bool> {
- var queryAttributes = searchTerm.makeKeychainAttributes()
- queryAttributes.return = [.attributes]
-
- return Keychain.findFirst(query: queryAttributes)
- .mapError { .lookupEntry($0) }
- .flatMap { itemAttributes -> Result<Bool> in
- let searchAttributes = searchTerm.makeKeychainAttributes()
- var updateAttributes = Keychain.Attributes()
-
- // Fix the accessibility permission for the Keychain entry
- if itemAttributes?.accessible != Self.keychainAccessibleLevel {
- updateAttributes.accessible = Self.keychainAccessibleLevel
- }
-
- // Return immediately if nothing to update (i.e the keychain query is empty)
- if updateAttributes.keychainRepresentation().isEmpty {
- return .success(false)
- } else {
- return Keychain.update(query: searchAttributes, update: updateAttributes)
- .mapError { .updateEntry($0) }
- .map { true }
- }
- }
- }
-
- /// Reads the tunnel settings from Keychain, then passes it to the given closure for
- /// modifications, saves the result back to Keychain.
- ///
- /// The given block may run multiple times if Keychain entry was changed between read and write
- /// operations.
- static func update(searchTerm: KeychainSearchTerm,
- using changeConfiguration: (inout TunnelSettings) -> Void) -> Result<TunnelSettings>
- {
- var searchQuery = searchTerm.makeKeychainAttributes()
- searchQuery.return = [.attributes, .data]
-
- let result = Keychain.findFirst(query: searchQuery)
- .mapError { .lookupEntry($0) }
- .flatMap { itemAttributes -> Result<TunnelSettings> in
- guard let serializedData = itemAttributes?.valueData,
- let account = itemAttributes?.account else { return .failure(.missingRequiredAttributes) }
-
- return Self.decode(data: serializedData)
- .flatMap { (tunnelConfig) -> Result<TunnelSettings> in
- var tunnelConfig = tunnelConfig
- changeConfiguration(&tunnelConfig)
-
- return Self.encode(tunnelConfig: tunnelConfig)
- .flatMap { (newData) -> Result<TunnelSettings> in
- // `SecItemUpdate` does not accept query parameters when using
- // persistent reference, so constraint the query to account
- // token instead now when we know it
- let updateQuery = KeychainSearchTerm
- .accountToken(account)
- .makeKeychainAttributes()
-
- var updateAttributes = Keychain.Attributes()
- updateAttributes.valueData = newData
-
- return Keychain.update(query: updateQuery, update: updateAttributes)
- .mapError { .updateEntry($0) }
- .map { tunnelConfig }
- }
- }
- }
-
- return result
- }
-
- static func remove(searchTerm: KeychainSearchTerm) -> Result<()> {
- return Keychain.delete(query: searchTerm.makeKeychainAttributes())
- .mapError { .removeEntry($0) }
- }
-
- /// Get a persistent reference to the Keychain item for the given account token
- static func getPersistentKeychainReference(account: String) -> Result<Data> {
- var query = KeychainSearchTerm.accountToken(account)
- .makeKeychainAttributes()
- query.return = [.persistentReference]
-
- return Keychain.findFirst(query: query)
- .mapError { .lookupEntry($0) }
- .flatMap { attributes -> Result<Data> in
- guard let persistentReference = attributes?.valuePersistentReference else {
- return .failure(.missingRequiredAttributes)
- }
- return .success(persistentReference)
- }
- }
-
- /// Verify that the keychain entry exists.
- /// Returns an error in case of failure to access Keychain.
- static func exists(searchTerm: KeychainSearchTerm) -> Result<Bool> {
- let query = searchTerm.makeKeychainAttributes()
-
- return Keychain.findFirst(query: query)
- .map({ (attributes) -> Bool in
- return true
- })
- .flatMapError({ (error) -> Result<Bool> in
- if case .itemNotFound = error {
- return .success(false)
- } else {
- return .failure(.lookupEntry(error))
- }
- })
- }
-
- private static func encode(tunnelConfig: TunnelSettings) -> Result<Data> {
- return Swift.Result { try JSONEncoder().encode(tunnelConfig) }
- .mapError { .encode($0) }
- }
-
- private static func decode(data: Data) -> Result<TunnelSettings> {
- return Swift.Result { try JSONDecoder().decode(TunnelSettings.self, from: data) }
- .mapError { .decode($0) }
- }
-}
diff --git a/ios/MullvadVPN/WireguardKeysViewController.swift b/ios/MullvadVPN/WireguardKeysViewController.swift
index f3fad9cbc6..71b5f9e5b9 100644
--- a/ios/MullvadVPN/WireguardKeysViewController.swift
+++ b/ios/MullvadVPN/WireguardKeysViewController.swift
@@ -11,10 +11,10 @@ import UIKit
import Logging
/// A UI refresh interval for the public key creation date (in seconds)
-private let kCreationDateRefreshInterval = Int(60)
+private let creationDateRefreshInterval = Int(60)
/// A maximum number of characters to display out of the entire public key representation
-private let kDisplayPublicKeyMaxLength = 20
+private let displayPublicKeyMaxLength = 20
private enum WireguardKeysViewState {
case `default`
@@ -34,7 +34,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
private var publicKeyPeriodicUpdateTimer: DispatchSourceTimer?
private var copyToPasteboardWork: DispatchWorkItem?
- private var verifyKeyCancellable: Cancellable?
+ private var updateDeviceTask: Cancellable?
private let alertPresenter = AlertPresenter()
private var state: WireguardKeysViewState = .default {
@@ -47,8 +47,6 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
return .lightContent
}
- private let apiProxy = REST.ProxyFactory.shared.createAPIProxy()
-
override func viewDidLoad() {
super.viewDidLoad()
@@ -82,16 +80,16 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
contentView.verifyKeyButton.addTarget(self, action: #selector(handleVerifyKey(_:)), for: .touchUpInside)
TunnelManager.shared.addObserver(self)
- updatePublicKey(tunnelSettings: TunnelManager.shared.tunnelInfo?.tunnelSettings, animated: false)
+ updatePublicKey(deviceData: TunnelManager.shared.device, animated: false)
startPublicKeyPeriodicUpdate()
}
private func startPublicKeyPeriodicUpdate() {
- let interval = DispatchTimeInterval.seconds(kCreationDateRefreshInterval)
+ let interval = DispatchTimeInterval.seconds(creationDateRefreshInterval)
let timerSource = DispatchSource.makeTimerSource(queue: .main)
timerSource.setEventHandler { [weak self] () -> Void in
- self?.updatePublicKey(tunnelSettings: TunnelManager.shared.tunnelInfo?.tunnelSettings, animated: true)
+ self?.updatePublicKey(deviceData: TunnelManager.shared.device, animated: true)
}
timerSource.schedule(deadline: .now() + interval, repeating: interval)
timerSource.activate()
@@ -105,8 +103,8 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
// no-op
}
- func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelInfo: TunnelInfo?) {
- self.updatePublicKey(tunnelSettings: tunnelInfo?.tunnelSettings, animated: true)
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) {
+ updatePublicKey(deviceData: tunnelSettings?.device, animated: true)
}
func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) {
@@ -116,20 +114,16 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
// MARK: - Actions
private func copyPublicKey() {
- guard let tunnelInfo = TunnelManager.shared.tunnelInfo else { return }
-
- let metadata = tunnelInfo.tunnelSettings.interface.privateKey.publicKeyWithMetadata
+ guard let tunnelSettings = TunnelManager.shared.tunnelSettings else { return }
- UIPasteboard.general.string = metadata.stringRepresentation()
+ UIPasteboard.general.string = tunnelSettings.device.wgKeyData.privateKey.publicKey.base64Key
setPublicKeyTitle(
string: NSLocalizedString("COPIED_TO_PASTEBOARD_LABEL", tableName: "WireguardKeys", comment: ""),
animated: true)
let workItem = DispatchWorkItem { [weak self] in
- let tunnelSettings = TunnelManager.shared.tunnelInfo?.tunnelSettings
-
- self?.updatePublicKey(tunnelSettings: tunnelSettings, animated: true)
+ self?.updatePublicKey(deviceData: TunnelManager.shared.device, animated: true)
}
DispatchQueue.main.asyncAfter(wallDeadline: .now() + .seconds(3), execute: workItem)
@@ -161,13 +155,16 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
contentView.creationRowView.value = formatKeyGenerationElapsedTime(with: creationDate) ?? "-"
}
- private func updatePublicKey(tunnelSettings: TunnelSettings?, animated: Bool) {
- if let publicKey = tunnelSettings?.interface.privateKey.publicKeyWithMetadata {
- let displayKey = publicKey
- .stringRepresentation(maxLength: kDisplayPublicKeyMaxLength)
+ private func updatePublicKey(deviceData: StoredDeviceData?, animated: Bool) {
+ if let wgKeyData = deviceData?.wgKeyData {
+ let displayKey = wgKeyData.privateKey
+ .publicKey
+ .base64Key
+ .prefix(displayPublicKeyMaxLength)
+ .appending("...")
setPublicKeyTitle(string: displayKey, animated: animated)
- updateCreationDateLabel(with: publicKey.creationDate)
+ updateCreationDateLabel(with: wgKeyData.creationDate)
} else {
setPublicKeyTitle(string: "-", animated: animated)
contentView.creationRowView.value = "-"
@@ -209,26 +206,19 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
}
private func verifyKey() {
- guard let tunnelInfo = TunnelManager.shared.tunnelInfo else { return }
-
updateViewState(.verifyingKey)
- verifyKeyCancellable?.cancel()
+ updateDeviceTask?.cancel()
- verifyKeyCancellable = apiProxy.getWireguardKey(
- accountNumber: tunnelInfo.token,
- publicKey: tunnelInfo.tunnelSettings.interface.publicKey,
- retryStrategy: .default
- ) { [weak self] result in
+ updateDeviceTask = TunnelManager.shared.updateDeviceData { [weak self] completion in
guard let self = self else { return }
- switch result {
+ switch completion {
case .success:
self.updateViewState(.verifiedKey(true))
case .failure(let error):
- if case .unhandledResponse(_, let serverErrorResponse) = error,
- serverErrorResponse?.code == .publicKeyNotFound {
+ if case .deviceRevoked = error {
self.updateViewState(.verifiedKey(false))
} else {
self.showKeyVerificationFailureAlert(error)
@@ -254,26 +244,39 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
}
}
- private func showKeyVerificationFailureAlert(_ error: REST.Error) {
+ private func showKeyVerificationFailureAlert(_ error: TunnelManager.Error) {
let reason = error.errorChainDescription ?? ""
let errorDescription = String(
format: NSLocalizedString(
"VERIFY_KEY_FAILURE_ALERT_MESSAGE",
tableName: "WireguardKeys",
- value: "Failed to verify the WireGuard key on server: %@",
+ value: "Failed to verify the WireGuard key: %@",
comment: ""
),
reason
)
let alertController = UIAlertController(
- title: NSLocalizedString("VERIFY_KEY_FAILURE_ALERT_TITLE", tableName: "WireguardKeys", comment: ""),
+ title: NSLocalizedString(
+ "VERIFY_KEY_FAILURE_ALERT_TITLE",
+ tableName: "WireguardKeys",
+ value: "Cannot verify the key",
+ comment: ""
+ ),
message: errorDescription,
preferredStyle: .alert
)
alertController.addAction(
- UIAlertAction(title: NSLocalizedString("VERIFY_KEY_FAILURE_ALERT_OK_ACTION", tableName: "WireguardKeys", comment: ""), style: .cancel)
+ UIAlertAction(
+ title: NSLocalizedString(
+ "VERIFY_KEY_FAILURE_ALERT_OK_ACTION",
+ tableName: "WireguardKeys",
+ value: "OK",
+ comment: ""
+ ),
+ style: .cancel
+ )
)
alertPresenter.enqueue(alertController, presentingController: self)
@@ -281,12 +284,25 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
private func showKeyRegenerationFailureAlert(_ error: TunnelManager.Error) {
let alertController = UIAlertController(
- title: NSLocalizedString("REGENERATE_KEY_FAILURE_ALERT_TITLE", tableName: "WireguardKeys", comment: ""),
+ title: NSLocalizedString(
+ "REGENERATE_KEY_FAILURE_ALERT_TITLE",
+ tableName: "WireguardKeys",
+ value: "Cannot regenerate the key",
+ comment: ""
+ ),
message: error.errorChainDescription,
preferredStyle: .alert
)
alertController.addAction(
- UIAlertAction(title: NSLocalizedString("REGENERATE_KEY_FAILURE_ALERT_OK_ACTION", tableName: "WireguardKeys", comment: ""), style: .cancel)
+ UIAlertAction(
+ title: NSLocalizedString(
+ "REGENERATE_KEY_FAILURE_ALERT_OK_ACTION",
+ tableName: "WireguardKeys",
+ value: "OK",
+ comment: ""
+ ),
+ style: .cancel
+ )
)
alertPresenter.enqueue(alertController, presentingController: self)
diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift
index 2001f405fc..45052da9a2 100644
--- a/ios/PacketTunnel/PacketTunnelProvider.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider.swift
@@ -32,11 +32,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
/// A system completion handler passed from startTunnel and saved for later use once the
/// connection is established.
- private var startTunnelCompletionHandler: ((PacketTunnelProviderError?) -> Void)?
+ private var startTunnelCompletionHandler: ((Error?) -> Void)?
/// A completion handler passed during reassertion and saved for later use once the connection
/// is reestablished.
- private var reassertTunnelCompletionHandler: ((PacketTunnelProviderError?) -> Void)?
+ private var reassertTunnelCompletionHandler: ((Error?) -> Void)?
/// Tunnel monitor.
private var tunnelMonitor: TunnelMonitor!
@@ -96,12 +96,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
// Read tunnel configuration.
let tunnelConfiguration: PacketTunnelConfiguration
- switch makeConfiguration(appSelectorResult) {
- case .success(let configuration):
- tunnelConfiguration = configuration
+ do {
+ tunnelConfiguration = try makeConfiguration(nil)
+ } catch {
+ providerLogger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to start the tunnel."
+ )
- case .failure(let error):
- providerLogger.error(chainedError: error, message: "Failed to start the tunnel.")
completionHandler(error)
return
}
@@ -190,7 +192,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
guard let self = self else { return }
if let error = error {
- self.providerLogger.error(chainedError: error, message: "Failed to reload tunnel settings.")
+ self.providerLogger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to reload tunnel settings."
+ )
} else {
self.providerLogger.debug("Reloaded tunnel settings.")
}
@@ -241,7 +246,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
providerLogger.debug("Recover connection. Picking next relay...")
- let handleRecoveryFailure = { (_ error: PacketTunnelProviderError) in
+ let handleRecoveryFailure = { (_ error: Error) in
// Stop tunnel monitor.
tunnelMonitor.stop()
@@ -260,11 +265,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
// Read tunnel configuration.
let tunnelConfiguration: PacketTunnelConfiguration
- switch makeConfiguration(nil) {
- case .success(let configuration):
- tunnelConfiguration = configuration
-
- case .failure(let error):
+ do {
+ tunnelConfiguration = try makeConfiguration(nil)
+ } catch {
handleRecoveryFailure(error)
return
}
@@ -278,7 +281,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
adapter.update(tunnelConfiguration: tunnelConfiguration.wgTunnelConfig) { error in
self.dispatchQueue.async {
if let error = error {
- handleRecoveryFailure(.updateWireguardConfiguration(error))
+ let providerError: PacketTunnelProviderError =
+ .updateWireGuardConfiguration(error)
+
+ handleRecoveryFailure(providerError)
}
}
}
@@ -296,50 +302,35 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
// MARK: - Private
- private func makeConfiguration(_ appSelectorResult: RelaySelectorResult? = nil) -> Result<PacketTunnelConfiguration, PacketTunnelProviderError> {
- guard let passwordRef = protocolConfiguration.passwordReference else {
- return .failure(.missingKeychainConfigurationReference)
- }
-
- let keychainEntry: TunnelSettingsManager.KeychainEntry
- switch TunnelSettingsManager.load(searchTerm: .persistentReference(passwordRef)) {
- case .success(let entry):
- keychainEntry = entry
- case .failure(let error):
- return .failure(.cannotReadTunnelSettings(error))
+ private func makeConfiguration(_ appSelectorResult: RelaySelectorResult? = nil)
+ throws -> PacketTunnelConfiguration
+ {
+ let tunnelSettings: TunnelSettingsV2
+ do {
+ tunnelSettings = try SettingsManager.readSettings()
+ } catch {
+ throw PacketTunnelProviderError.readSettings(error)
}
- let selectorResult: RelaySelectorResult
- if let appSelectorResult = appSelectorResult {
- selectorResult = appSelectorResult
- } else {
- let relayConstraints = keychainEntry.tunnelSettings.relayConstraints
- switch Self.selectRelayEndpoint(relayConstraints: relayConstraints) {
- case .success(let value):
- selectorResult = value
- case .failure(let error):
- return .failure(error)
- }
- }
+ let selectorResult = try appSelectorResult
+ ?? Self.selectRelayEndpoint(
+ relayConstraints: tunnelSettings.relayConstraints
+ )
- let tunnelConfiguration = PacketTunnelConfiguration(
- tunnelSettings: keychainEntry.tunnelSettings,
+ return PacketTunnelConfiguration(
+ tunnelSettings: tunnelSettings,
selectorResult: selectorResult
)
-
- return .success(tunnelConfiguration)
}
- private func reloadTunnelSettings(completionHandler: @escaping (PacketTunnelProviderError?) -> Void) {
+ private func reloadTunnelSettings(completionHandler: @escaping (Error?) -> Void) {
dispatchPrecondition(condition: .onQueue(dispatchQueue))
// Read tunnel configuration.
let tunnelConfiguration: PacketTunnelConfiguration
- switch makeConfiguration(nil) {
- case .success(let configuration):
- tunnelConfiguration = configuration
-
- case .failure(let error):
+ do {
+ tunnelConfiguration = try makeConfiguration(nil)
+ } catch {
completionHandler(error)
return
}
@@ -379,7 +370,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
}
// Call completion handler immediately.
- completionHandler(.updateWireguardConfiguration(error))
+ completionHandler(PacketTunnelProviderError.updateWireGuardConfiguration(error))
} else {
// Store completion handler and call it from TunnelMonitorDelegate once
// the connection is established.
@@ -412,45 +403,52 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
/// Load relay cache with potential networking to refresh the cache and pick the relay for the
/// given relay constraints.
- private class func selectRelayEndpoint(relayConstraints: RelayConstraints) -> Result<RelaySelectorResult, PacketTunnelProviderError> {
- let cacheFileURL = RelayCache.IO.defaultCacheFileURL(forSecurityApplicationGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier)!
+ private class func selectRelayEndpoint(relayConstraints: RelayConstraints)
+ throws -> RelaySelectorResult
+ {
+ let cacheFileURL = RelayCache.IO.defaultCacheFileURL(
+ forSecurityApplicationGroupIdentifier: ApplicationConfiguration.securityGroupIdentifier
+ )!
let prebundledRelaysURL = RelayCache.IO.preBundledRelaysFileURL!
+ let cachedRelayList: RelayCache.CachedRelays
+ do {
+ cachedRelayList = try RelayCache.IO.readWithFallback(
+ cacheFileURL: cacheFileURL,
+ preBundledRelaysFileURL: prebundledRelaysURL
+ )
+ } catch {
+ throw PacketTunnelProviderError.readRelayCache(error)
+ }
- return RelayCache.IO.readWithFallback(cacheFileURL: cacheFileURL, preBundledRelaysFileURL: prebundledRelaysURL)
- .mapError { error in
- return PacketTunnelProviderError.readRelayCache(error)
- }
- .flatMap { cachedRelayList in
- if let selectorResult = RelaySelector.evaluate(relays: cachedRelayList.relays, constraints: relayConstraints) {
- return .success(selectorResult)
- } else {
- return .failure(.noRelaySatisfyingConstraint)
- }
- }
+ if let selectorResult = RelaySelector.evaluate(
+ relays: cachedRelayList.relays,
+ constraints: relayConstraints
+ ) {
+ return selectorResult
+ } else {
+ throw PacketTunnelProviderError.noRelaySatisfyingConstraint
+ }
}
}
enum PacketTunnelProviderError: ChainedError {
- /// Failure to read the relay cache
- case readRelayCache(RelayCache.Error)
+ /// Failure to read the relay cache.
+ case readRelayCache(Error)
- /// Failure to satisfy the relay constraint
+ /// Failure to satisfy the relay constraint.
case noRelaySatisfyingConstraint
- /// Missing the persistent keychain reference to the tunnel settings
- case missingKeychainConfigurationReference
-
- /// Failure to read the tunnel settings from Keychain
- case cannotReadTunnelSettings(TunnelSettingsManager.Error)
+ /// Failure to read settings from keychain.
+ case readSettings(Error)
- /// Failure to start the Wireguard backend
+ /// Failure to start the Wireguard backend.
case startWireguardAdapter(WireGuardAdapterError)
- /// Failure to stop the Wireguard backend
+ /// Failure to stop the Wireguard backend.
case stopWireguardAdapter(WireGuardAdapterError)
- /// Failure to update the Wireguard configuration
- case updateWireguardConfiguration(WireGuardAdapterError)
+ /// Failure to update Wireguard configuration.
+ case updateWireGuardConfiguration(WireGuardAdapterError)
var errorDescription: String? {
switch self {
@@ -460,11 +458,8 @@ enum PacketTunnelProviderError: ChainedError {
case .noRelaySatisfyingConstraint:
return "No relay satisfying the given constraint."
- case .missingKeychainConfigurationReference:
- return "Keychain configuration reference is not set on protocol configuration."
-
- case .cannotReadTunnelSettings:
- return "Failure to read tunnel settings."
+ case .readSettings:
+ return "Failure to read settings."
case .startWireguardAdapter:
return "Failure to start the WireGuard adapter."
@@ -472,14 +467,14 @@ enum PacketTunnelProviderError: ChainedError {
case .stopWireguardAdapter:
return "Failure to stop the WireGuard adapter."
- case .updateWireguardConfiguration:
- return "Failure to update the Wireguard configuration."
+ case .updateWireGuardConfiguration:
+ return "Failure to update the WireGuard configuration."
}
}
}
struct PacketTunnelConfiguration {
- var tunnelSettings: TunnelSettings
+ var tunnelSettings: TunnelSettingsV2
var selectorResult: RelaySelectorResult
}
@@ -502,17 +497,22 @@ extension PacketTunnelConfiguration {
return peerConfig
}
- var interfaceConfig = InterfaceConfiguration(privateKey: tunnelSettings.interface.privateKey.privateKey)
+ var interfaceConfig = InterfaceConfiguration(
+ privateKey: tunnelSettings.device.wgKeyData.privateKey
+ )
interfaceConfig.listenPort = 0
interfaceConfig.dns = dnsServers.map { DNSServer(address: $0) }
- interfaceConfig.addresses = tunnelSettings.interface.addresses
+ interfaceConfig.addresses = [
+ tunnelSettings.device.ipv4Address,
+ tunnelSettings.device.ipv6Address
+ ]
return TunnelConfiguration(name: nil, interface: interfaceConfig, peers: peerConfigs)
}
var dnsServers: [IPAddress] {
let mullvadEndpoint = selectorResult.endpoint
- let dnsSettings = tunnelSettings.interface.dnsSettings
+ let dnsSettings = tunnelSettings.dnsSettings
if dnsSettings.effectiveEnableCustomDNS {
let dnsServers = dnsSettings.customDNSDomains
diff --git a/ios/PacketTunnel/Pinger.swift b/ios/PacketTunnel/Pinger.swift
index fd76fdc5ec..feb0fd678a 100644
--- a/ios/PacketTunnel/Pinger.swift
+++ b/ios/PacketTunnel/Pinger.swift
@@ -33,14 +33,14 @@ final class Pinger {
stop()
}
- func start(delay: DispatchTimeInterval, repeating repeatInterval: DispatchTimeInterval) -> Result<(), Pinger.Error> {
+ func start(delay: DispatchTimeInterval, repeating repeatInterval: DispatchTimeInterval) throws {
stateLock.lock()
defer { stateLock.unlock() }
stop()
guard let newSocket = CFSocketCreate(kCFAllocatorDefault, AF_INET, SOCK_DGRAM, IPPROTO_ICMP, 0, nil, nil) else {
- return .failure(.createSocket)
+ throw Error.createSocket
}
let flags = CFSocketGetSocketFlags(newSocket)
@@ -48,12 +48,10 @@ final class Pinger {
CFSocketSetSocketFlags(newSocket, flags | kCFSocketCloseOnInvalidate)
}
- if case .failure(let error) = bindSocket(newSocket) {
- return .failure(error)
- }
+ try bindSocket(newSocket)
guard let runLoop = CFSocketCreateRunLoopSource(kCFAllocatorDefault, newSocket, 0) else {
- return .failure(.createRunLoop)
+ throw Error.createRunLoop
}
CFRunLoopAddSource(CFRunLoopGetMain(), runLoop, .defaultMode)
@@ -68,8 +66,6 @@ final class Pinger {
newTimer.schedule(wallDeadline: .now() + delay, repeating: repeatInterval)
newTimer.resume()
-
- return .success(())
}
func stop() {
@@ -135,15 +131,15 @@ final class Pinger {
return nextSequenceNumber
}
- private func bindSocket(_ socket: CFSocket) -> Result<(), Pinger.Error> {
+ private func bindSocket(_ socket: CFSocket) throws {
guard let interfaceName = interfaceName else {
logger.debug("Interface is not specified.")
- return .success(())
+ return
}
var index = if_nametoindex(interfaceName)
guard index > 0 else {
- return .failure(.mapInterfaceNameToIndex(errno))
+ throw Error.mapInterfaceNameToIndex(errno)
}
logger.debug("Bind socket to \"\(interfaceName)\" (index: \(index))...")
@@ -159,9 +155,7 @@ final class Pinger {
if result == -1 {
logger.error("Failed to bind socket to \"\(interfaceName)\" (index: \(index), errno: \(errno)).")
- return .failure(.bindSocket(errno))
- } else {
- return .success(())
+ throw Error.bindSocket(errno)
}
}
diff --git a/ios/PacketTunnel/TunnelMonitor.swift b/ios/PacketTunnel/TunnelMonitor.swift
index bec23409f0..b39a26d4d5 100644
--- a/ios/PacketTunnel/TunnelMonitor.swift
+++ b/ios/PacketTunnel/TunnelMonitor.swift
@@ -125,16 +125,16 @@ final class TunnelMonitor {
stopPinging()
}
- private func startPinging(address: IPv4Address) -> Result<(), Pinger.Error> {
+ private func startPinging(address: IPv4Address) throws {
let newPinger = Pinger(address: address, interfaceName: adapter.interfaceName)
- let pingerResult = newPinger.start(delay: TunnelMonitorConfiguration.pingStartDelay, repeating: TunnelMonitorConfiguration.pingInterval)
- if case .success = pingerResult {
- pinger = newPinger
- isPinging = true
- }
+ try newPinger.start(
+ delay: TunnelMonitorConfiguration.pingStartDelay,
+ repeating: TunnelMonitorConfiguration.pingInterval
+ )
- return pingerResult
+ pinger = newPinger
+ isPinging = true
}
private func stopPinging() {
@@ -230,8 +230,9 @@ final class TunnelMonitor {
case (true, false):
logger.debug("Network is reachable. Starting to ping.")
- switch startPinging(address: address) {
- case .success:
+ do {
+ try startPinging(address: address)
+
// Reset the last recovery attempt date.
firstAttemptDate = Date()
lastAttemptDate = firstAttemptDate
@@ -242,10 +243,14 @@ final class TunnelMonitor {
delegateQueue.async {
self.delegate?.tunnelMonitor(self, networkReachabilityStatusDidChange: isNetworkReachable)
}
+ } catch {
+ let error = error as! Pinger.Error
- case .failure(let error):
if error != lastError {
- logger.error(chainedError: AnyChainedError(error), message: "Failed to start pinging.")
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to start pinging."
+ )
lastError = error
}
}