summaryrefslogtreecommitdiffhomepage
path: root/ios
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-08-01 16:10:04 +0200
committerAndrej Mihajlov <and@mullvad.net>2022-08-01 16:10:04 +0200
commita3df757018bc0b41036d2ec710819b16be356bb5 (patch)
tree3488694d74c9ea19ce744be14ecda8ca639c829b /ios
parent68303e0872fa3370e31ec3e83a1d1f40c05caf37 (diff)
parenta9fd39392f26e8952051295357246daa598205b5 (diff)
downloadmullvadvpn-a3df757018bc0b41036d2ec710819b16be356bb5.tar.xz
mullvadvpn-a3df757018bc0b41036d2ec710819b16be356bb5.zip
Merge branch 'add-device-state'
Diffstat (limited to 'ios')
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj38
-rw-r--r--ios/MullvadVPN/AccountViewController.swift49
-rw-r--r--ios/MullvadVPN/AppDelegate.swift58
-rw-r--r--ios/MullvadVPN/ConnectViewController.swift26
-rw-r--r--ios/MullvadVPN/DisplayChainedError.swift187
-rw-r--r--ios/MullvadVPN/LoginViewController.swift8
-rw-r--r--ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift20
-rw-r--r--ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift45
-rw-r--r--ios/MullvadVPN/Operations/OperationCompletion.swift22
-rw-r--r--ios/MullvadVPN/PreferencesViewController.swift24
-rw-r--r--ios/MullvadVPN/ProblemReportViewController.swift4
-rw-r--r--ios/MullvadVPN/REST/RESTError.swift8
-rw-r--r--ios/MullvadVPN/REST/ServerRelaysResponse.swift2
-rw-r--r--ios/MullvadVPN/Result+Extensions.swift11
-rw-r--r--ios/MullvadVPN/RevokedDeviceViewController.swift191
-rw-r--r--ios/MullvadVPN/SceneDelegate.swift293
-rw-r--r--ios/MullvadVPN/SettingsDataSource.swift16
-rw-r--r--ios/MullvadVPN/SettingsManager/SettingsManager.swift189
-rw-r--r--ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift48
-rw-r--r--ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift142
-rw-r--r--ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift33
-rw-r--r--ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift12
-rw-r--r--ios/MullvadVPN/TunnelManager/ReconnectTunnelOperation.swift19
-rw-r--r--ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift77
-rw-r--r--ios/MullvadVPN/TunnelManager/SetAccountOperation.swift237
-rw-r--r--ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift144
-rw-r--r--ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift24
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelInteractor.swift45
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift856
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManagerErrors.swift75
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelObserver.swift12
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelState.swift9
-rw-r--r--ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift55
-rw-r--r--ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift66
-rw-r--r--ios/MullvadVPN/WireguardKeysViewController.swift34
-rw-r--r--ios/PacketTunnel/PacketTunnelConfiguration.swift21
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider.swift2
37 files changed, 1820 insertions, 1282 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 3a2fb2c726..55f041db82 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -7,7 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
- 5801C9A527A14B2A0031566A /* TunnelManagerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5801C9A427A14B2A0031566A /* TunnelManagerState.swift */; };
58059DDC28465E8F002B1049 /* TransformOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58059DDB28465E8F002B1049 /* TransformOperation.swift */; };
58059DDE28468158002B1049 /* OutputOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58059DDD28468158002B1049 /* OutputOperation.swift */; };
58059DE02846823E002B1049 /* ResultOperation+Output.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58059DDF2846823E002B1049 /* ResultOperation+Output.swift */; };
@@ -16,6 +15,7 @@
5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; };
5807E2C2243203D000F5FF30 /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2C1243203D000F5FF30 /* StringTests.swift */; };
5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; };
+ 580909D32876D09A0078138D /* RevokedDeviceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580909D22876D09A0078138D /* RevokedDeviceViewController.swift */; };
58095C4F2760BA9100890776 /* AddressCacheStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58095C4E2760BA9100890776 /* AddressCacheStore.swift */; };
58095C512760BBB500890776 /* AddressCacheTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58095C502760BBB400890776 /* AddressCacheTracker.swift */; };
58095C532760EEC700890776 /* RESTNetworkOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58095C522760EEC700890776 /* RESTNetworkOperation.swift */; };
@@ -53,7 +53,7 @@
5820675C26E6576800655B05 /* RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675A26E6576800655B05 /* RelayCache.swift */; };
5820675E26E6839900655B05 /* PresentAlertOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820675D26E6839900655B05 /* PresentAlertOperation.swift */; };
5820676226E75D8500655B05 /* REST.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820674D26E6510200655B05 /* REST.swift */; };
- 5820676426E771DB00655B05 /* TunnelManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820676326E771DB00655B05 /* TunnelManagerError.swift */; };
+ 5820676426E771DB00655B05 /* TunnelManagerErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820676326E771DB00655B05 /* TunnelManagerErrors.swift */; };
5820676826E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820676726E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift */; };
5823FA5426CE49F700283BF8 /* TunnelObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */; };
58289082286B590900478596 /* UIFont+Monospaced.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58289081286B590900478596 /* UIFont+Monospaced.swift */; };
@@ -193,6 +193,7 @@
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 */; };
+ 58968FAE28743E2000B799DC /* TunnelInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58968FAD28743E2000B799DC /* TunnelInteractor.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 */; };
@@ -349,7 +350,6 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
- 5801C9A427A14B2A0031566A /* TunnelManagerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerState.swift; sourceTree = "<group>"; };
58059DDB28465E8F002B1049 /* TransformOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformOperation.swift; sourceTree = "<group>"; };
58059DDD28468158002B1049 /* OutputOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputOperation.swift; sourceTree = "<group>"; };
58059DDF2846823E002B1049 /* ResultOperation+Output.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResultOperation+Output.swift"; sourceTree = "<group>"; };
@@ -358,6 +358,7 @@
5808273928487E3E006B77A4 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Base.xcconfig; sourceTree = "<group>"; };
5808273B284888BC006B77A4 /* App.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = App.xcconfig; sourceTree = "<group>"; };
5808273C284888E5006B77A4 /* PacketTunnel.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = PacketTunnel.xcconfig; sourceTree = "<group>"; };
+ 580909D22876D09A0078138D /* RevokedDeviceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevokedDeviceViewController.swift; sourceTree = "<group>"; };
58095C4E2760BA9100890776 /* AddressCacheStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCacheStore.swift; sourceTree = "<group>"; };
58095C502760BBB400890776 /* AddressCacheTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCacheTracker.swift; sourceTree = "<group>"; };
58095C522760EEC700890776 /* RESTNetworkOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTNetworkOperation.swift; sourceTree = "<group>"; };
@@ -380,7 +381,7 @@
5820674F26E6514100655B05 /* HTTP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTP.swift; sourceTree = "<group>"; };
5820675A26E6576800655B05 /* RelayCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCache.swift; sourceTree = "<group>"; };
5820675D26E6839900655B05 /* PresentAlertOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentAlertOperation.swift; sourceTree = "<group>"; };
- 5820676326E771DB00655B05 /* TunnelManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerError.swift; sourceTree = "<group>"; };
+ 5820676326E771DB00655B05 /* TunnelManagerErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerErrors.swift; sourceTree = "<group>"; };
5820676726E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+UIBackgroundFetchResult.swift"; sourceTree = "<group>"; };
5823FA4F26CA690600283BF8 /* OSLogHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogHandler.swift; sourceTree = "<group>"; };
5823FA5326CE49F600283BF8 /* TunnelObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObserver.swift; sourceTree = "<group>"; };
@@ -483,6 +484,7 @@
5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+ProductVersion.swift"; sourceTree = "<group>"; };
5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+KeyboardNavigation.swift"; sourceTree = "<group>"; };
5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyTableViewHeaderFooterView.swift; sourceTree = "<group>"; };
+ 58968FAD28743E2000B799DC /* TunnelInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelInteractor.swift; sourceTree = "<group>"; };
5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDateComponentsFormatting.swift; sourceTree = "<group>"; };
5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDateComponentsFormattingTests.swift; sourceTree = "<group>"; };
5896CEF126972DEB00B0FAE8 /* AccountContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountContentView.swift; sourceTree = "<group>"; };
@@ -691,27 +693,27 @@
5823FA5726CE4A4100283BF8 /* TunnelManager */ = {
isa = PBXGroup;
children = (
- 587C575226D2615F005EF767 /* PacketTunnelOptions.swift */,
- 5835B7CB233B76CB0096D79F /* TunnelManager.swift */,
- 5801C9A427A14B2A0031566A /* TunnelManagerState.swift */,
- 5820676326E771DB00655B05 /* TunnelManagerError.swift */,
- 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */,
- 58B93A1226C3F13600A55733 /* TunnelState.swift */,
588527B1276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift */,
- 584B17AA27637DE40057F3B8 /* ReconnectTunnelOperation.swift */,
+ 58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */,
+ 58161C9B28352F850028ECFD /* MigrateSettingsOperation.swift */,
+ 587C575226D2615F005EF767 /* PacketTunnelOptions.swift */,
585DA89826B0329200B8C587 /* PacketTunnelStatus.swift */,
+ 584B17AA27637DE40057F3B8 /* ReconnectTunnelOperation.swift */,
+ 58F2E14B276A61C000A79513 /* RotateKeyOperation.swift */,
586E54FA27A2DF6D0029B88B /* SendTunnelProviderMessageOperation.swift */,
588527B3276B4F2F00BAA373 /* SetAccountOperation.swift */,
58F2E143276A13F300A79513 /* StartTunnelOperation.swift */,
58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */,
- 58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */,
- 58F2E14B276A61C000A79513 /* RotateKeyOperation.swift */,
58E0A98727C8F46300FE6BDD /* Tunnel.swift */,
5875960926F371FC00BF6711 /* Tunnel+Messaging.swift */,
+ 58968FAD28743E2000B799DC /* TunnelInteractor.swift */,
+ 5835B7CB233B76CB0096D79F /* TunnelManager.swift */,
+ 5820676326E771DB00655B05 /* TunnelManagerErrors.swift */,
+ 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */,
585DA89226B0323E00B8C587 /* TunnelProviderMessage.swift */,
+ 58B93A1226C3F13600A55733 /* TunnelState.swift */,
5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */,
58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */,
- 58161C9B28352F850028ECFD /* MigrateSettingsOperation.swift */,
);
path = TunnelManager;
sourceTree = "<group>";
@@ -915,6 +917,7 @@
585DA87F26B0268500B8C587 /* REST */,
58F1311427E0B2AB007AC5BC /* Result+Extensions.swift */,
5820676726E79E7B00655B05 /* Result+UIBackgroundFetchResult.swift */,
+ 580909D22876D09A0078138D /* RevokedDeviceViewController.swift */,
587425C02299833500CA2045 /* RootContainerViewController.swift */,
58E25F802837BBBB002CFB2C /* SceneDelegate.swift */,
5888AD82227B11080051EB06 /* SelectLocationCell.swift */,
@@ -1328,6 +1331,7 @@
584789E026529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */,
58161C9C28352F850028ECFD /* MigrateSettingsOperation.swift in Sources */,
58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */,
+ 58968FAE28743E2000B799DC /* TunnelInteractor.swift in Sources */,
588D2FE3248AC27F00E313F7 /* AsyncOperation.swift in Sources */,
5820675026E6514100655B05 /* HTTP.swift in Sources */,
584D26C2270C8542004EA533 /* SettingsStaticTextFooterView.swift in Sources */,
@@ -1376,7 +1380,6 @@
5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */,
58F7CA882692E34000FC59FD /* WireguardKeysContentView.swift in Sources */,
58554F7B280B125F00013055 /* RESTAccountsProxy.swift in Sources */,
- 5801C9A527A14B2A0031566A /* TunnelManagerState.swift in Sources */,
5868585524054096000B8131 /* AppButton.swift in Sources */,
58781CC922AE7CA8009B9D8E /* RelayConstraints.swift in Sources */,
584E96BC240FD4DA00D3334F /* Location.swift in Sources */,
@@ -1385,7 +1388,7 @@
58E25F812837BBBB002CFB2C /* SceneDelegate.swift in Sources */,
585E820327F3285E00939F0E /* SendAppStoreReceiptOperation.swift in Sources */,
584B17AB27637DE40057F3B8 /* ReconnectTunnelOperation.swift in Sources */,
- 5820676426E771DB00655B05 /* TunnelManagerError.swift in Sources */,
+ 5820676426E771DB00655B05 /* TunnelManagerErrors.swift in Sources */,
585B4B8726D9098900555C4C /* TunnelErrorNotificationProvider.swift in Sources */,
58FEAFB92750DA2F003C1625 /* AddressCache.swift in Sources */,
58B67B482602079E008EF58E /* RelaySelector.swift in Sources */,
@@ -1411,6 +1414,7 @@
58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */,
58FD5BE724192A2C00112C88 /* AppStoreReceipt.swift in Sources */,
5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */,
+ 580909D32876D09A0078138D /* RevokedDeviceViewController.swift in Sources */,
5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */,
58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */,
58FEEB46260A028D00A621A8 /* GeoJSON.swift in Sources */,
@@ -1483,6 +1487,7 @@
files = (
5850366825A47AC700A43E93 /* IPAddressRange+Codable.swift in Sources */,
58FB865F26EA2E6D00F188BC /* LogFormatting.swift in Sources */,
+ 58E072A528814C28008902F8 /* TunnelMonitorDelegate.swift in Sources */,
587C575426D2615F005EF767 /* PacketTunnelOptions.swift in Sources */,
58BFA5CD22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */,
5820675826E652AF00655B05 /* RelayCacheIO.swift in Sources */,
@@ -1491,6 +1496,7 @@
5806767C27048E9B00C858CB /* PacketTunnelProvider.swift in Sources */,
585DA89426B0323E00B8C587 /* TunnelProviderMessage.swift in Sources */,
587AD7C723421D8600E93A53 /* TunnelSettingsV1.swift in Sources */,
+ 58E07299288031D5008902F8 /* WireGuardAdapterError+Localization.swift in Sources */,
58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */,
582AD44127BE6178002A6BFC /* CodingErrors+ChainedError.swift in Sources */,
5840250222B1124600E4CFEC /* IPAddress+Codable.swift in Sources */,
diff --git a/ios/MullvadVPN/AccountViewController.swift b/ios/MullvadVPN/AccountViewController.swift
index f594e0a6c1..579ca685fd 100644
--- a/ios/MullvadVPN/AccountViewController.swift
+++ b/ios/MullvadVPN/AccountViewController.swift
@@ -25,7 +25,6 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb
private var pendingPayment: SKPayment?
private let alertPresenter = AlertPresenter()
- private let logger = Logger(label: "AccountViewController")
weak var delegate: AccountViewControllerDelegate?
@@ -84,7 +83,6 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb
comment: ""
)
- contentView.accountTokenRowView.accountNumber = TunnelManager.shared.accountNumber
contentView.accountTokenRowView.copyAccountNumber = { [weak self] in
self?.copyAccountToken()
}
@@ -96,8 +94,7 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb
AppStorePaymentManager.shared.addPaymentObserver(self)
TunnelManager.shared.addObserver(self)
- updateAccountExpiry(expiryDate: TunnelManager.shared.accountExpiry)
- updateDeviceName(TunnelManager.shared.device?.name)
+ updateView(from: TunnelManager.shared.deviceState)
// Make sure to disable IAPs when payments are restricted
if AppStorePaymentManager.canMakePayments {
@@ -109,12 +106,14 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb
// MARK: - Private methods
- private func updateDeviceName(_ deviceName: String?) {
- contentView.accountDeviceRow.deviceName = deviceName
- }
+ private func updateView(from deviceState: DeviceState?) {
+ guard case .loggedIn(let accountData, let deviceData) = deviceState else {
+ return
+ }
- private func updateAccountExpiry(expiryDate: Date?) {
- contentView.accountExpiryRowView.value = expiryDate
+ contentView.accountDeviceRow.deviceName = deviceData.name
+ contentView.accountTokenRowView.accountNumber = accountData.number
+ contentView.accountExpiryRowView.value = accountData.expiry
}
private func requestStoreProducts() {
@@ -320,17 +319,16 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb
// no-op
}
- func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) {
+ func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) {
// no-op
}
- func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) {
- guard let tunnelSettings = tunnelSettings else {
- return
- }
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) {
+ // no-op
+ }
- updateDeviceName(tunnelSettings.device.name)
- updateAccountExpiry(expiryDate: tunnelSettings.account.expiry)
+ func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) {
+ updateView(from: deviceState)
}
// MARK: - AppStorePaymentObserver
@@ -384,26 +382,35 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, TunnelOb
}
private func copyAccountToken() {
- UIPasteboard.general.string = TunnelManager.shared.accountNumber
+ guard let accountData = TunnelManager.shared.deviceState.accountData else {
+ return
+ }
+
+ UIPasteboard.general.string = accountData.number
}
@objc private func doPurchase() {
- guard let product = product, let accountNumber = TunnelManager.shared.accountNumber else { return }
+ guard let accountData = TunnelManager.shared.deviceState.accountData,
+ let product = product else {
+ return
+ }
let payment = SKPayment(product: product)
pendingPayment = payment
compoundInteractionRestriction.increase(animated: true)
- AppStorePaymentManager.shared.addPayment(payment, for: accountNumber)
+ AppStorePaymentManager.shared.addPayment(payment, for: accountData.number)
}
@objc private func restorePurchases() {
- guard let accountNumber = TunnelManager.shared.accountNumber else { return }
+ guard let accountData = TunnelManager.shared.deviceState.accountData else {
+ return
+ }
compoundInteractionRestriction.increase(animated: true)
- _ = AppStorePaymentManager.shared.restorePurchases(for: accountNumber) { completion in
+ _ = AppStorePaymentManager.shared.restorePurchases(for: accountData.number) { completion in
switch completion {
case .success(let response):
self.showTimeAddedConfirmationAlert(with: response, context: .restoration)
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index 95a32e0ffd..3db5eebe09 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -32,7 +32,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// MARK: - Application lifecycle
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
- // Setup logging
initLoggingSystem(bundleIdentifier: Bundle.main.bundleIdentifier!)
logger = Logger(label: "AppDelegate")
@@ -43,10 +42,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
#endif
if #available(iOS 13.0, *) {
- // Register background tasks on iOS 13
registerBackgroundTasks()
} else {
- // Set background refresh interval on iOS 12
application.setMinimumBackgroundFetchInterval(
ApplicationConfiguration.minimumBackgroundFetchInterval
)
@@ -55,44 +52,32 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
setupPaymentHandler()
setupNotificationHandler()
- // Start initialization
- let setupTunnelManagerOperation = AsyncBlockOperation(dispatchQueue: .main) { blockOperation in
+ let setupTunnelManagerOperation = AsyncBlockOperation(dispatchQueue: .main) { operation in
TunnelManager.shared.loadConfiguration { error in
- dispatchPrecondition(condition: .onQueue(.main))
-
+ // TODO: avoid throwing fatal error and show the problem report UI instead.
if let error = error {
- self.logger.error(chainedError: error, message: "Failed to load tunnels")
-
- // TODO: avoid throwing fatal error and show the problem report UI instead.
- fatalError(
- error.displayChain(message: "Failed to load VPN tunnel configuration")
- )
+ fatalError(error.localizedDescription)
}
- blockOperation.finish()
- }
- }
+ self.logger.debug("Finished initialization.")
- let setupUIOperation = AsyncBlockOperation(dispatchQueue: .main) {
- self.logger.debug("Finished initialization.")
+ NotificationManager.shared.updateNotifications()
+ AppStorePaymentManager.shared.startPaymentQueueMonitoring()
- NotificationManager.shared.updateNotifications()
- AppStorePaymentManager.shared.startPaymentQueueMonitoring()
+ operation.finish()
+ }
}
- operationQueue.addOperations([
- setupTunnelManagerOperation,
- setupUIOperation
- ], waitUntilFinished: false)
+ operationQueue.addOperation(setupTunnelManagerOperation)
if #available(iOS 13, *) {
- return true
+ // no-op
} else {
sceneDelegate = SceneDelegate()
sceneDelegate?.setupScene(windowFactory: ClassicWindowFactory())
-
- return true
}
+
+ return true
}
func application(
@@ -123,7 +108,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
- let rotatePrivateKeyOperation = ResultBlockOperation<Bool, TunnelManager.Error>
+ let rotatePrivateKeyOperation = ResultBlockOperation<Bool, Error>
{ operation in
let handle = TunnelManager.shared.rotatePrivateKey(forceRotate: false) { completion in
operation.finish(completion: completion)
@@ -387,21 +372,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// MARK: - AppStorePaymentManagerDelegate
extension AppDelegate: AppStorePaymentManagerDelegate {
-
- func appStorePaymentManager(_ manager: AppStorePaymentManager,
- didRequestAccountTokenFor payment: SKPayment) -> String?
+ func appStorePaymentManager(
+ _ manager: AppStorePaymentManager,
+ didRequestAccountTokenFor payment: SKPayment
+ ) -> String?
{
- // 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 TunnelManager.shared.accountNumber
+ // Since we do not persist the relation between payment and account number between the
+ // app launches, we assume that all successful purchases belong to the active account
+ // number.
+ return TunnelManager.shared.deviceState.accountData?.number
}
-
}
// MARK: - UNUserNotificationCenterDelegate
extension AppDelegate: UNUserNotificationCenterDelegate {
-
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let blockOperation = AsyncBlockOperation(dispatchQueue: .main) {
if response.notification.request.identifier == accountExpiryNotificationIdentifier,
@@ -429,5 +414,4 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
completionHandler([])
}
}
-
}
diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift
index 4dbde5d2c2..6a3fe21d0c 100644
--- a/ios/MullvadVPN/ConnectViewController.swift
+++ b/ios/MullvadVPN/ConnectViewController.swift
@@ -50,17 +50,15 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen
}
var preferredHeaderBarPresentation: HeaderBarPresentation {
- guard TunnelManager.shared.isAccountSet else {
+ switch TunnelManager.shared.deviceState {
+ case .loggedIn, .revoked:
+ return HeaderBarPresentation(
+ style: tunnelState.isSecured ? .secured : .unsecured,
+ showsDivider: false
+ )
+ case .loggedOut:
return HeaderBarPresentation(style: .default, showsDivider: true)
}
-
- switch tunnelState {
- case .connecting, .reconnecting, .connected:
- return HeaderBarPresentation(style: .secured, showsDivider: false)
-
- case .disconnecting, .disconnected, .pendingReconnect:
- return HeaderBarPresentation(style: .unsecured, showsDivider: false)
- }
}
var prefersHeaderBarHidden: Bool {
@@ -92,7 +90,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen
contentView.selectLocationButton.addTarget(self, action: #selector(handleSelectLocation(_:)), for: .touchUpInside)
TunnelManager.shared.addObserver(self)
- self.tunnelState = TunnelManager.shared.tunnelState
+ self.tunnelState = TunnelManager.shared.tunnelStatus.state
addSubviews()
setupMapView()
@@ -151,7 +149,11 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen
// no-op
}
- func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) {
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) {
+ // no-op
+ }
+
+ func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) {
setNeedsHeaderBarStyleAppearanceUpdate()
}
@@ -159,7 +161,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen
self.tunnelState = tunnelState
}
- func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) {
+ func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) {
// no-op
}
diff --git a/ios/MullvadVPN/DisplayChainedError.swift b/ios/MullvadVPN/DisplayChainedError.swift
index 0120d2fb14..cb0ab32a9d 100644
--- a/ios/MullvadVPN/DisplayChainedError.swift
+++ b/ios/MullvadVPN/DisplayChainedError.swift
@@ -55,189 +55,6 @@ extension REST.Error: DisplayChainedError {
}
}
-extension TunnelManager.Error: DisplayChainedError {
- var errorChainDescription: String? {
- switch self {
- case .loadAllVPNConfigurations(let systemError):
- return String(
- format: NSLocalizedString(
- "LOAD_ALL_VPN_CONFIGURATIONS_ERROR",
- tableName: "TunnelManager",
- value: "Failed to load system VPN configurations: %@",
- comment: ""
- ),
- systemError.localizedDescription
- )
- case .reloadVPNConfiguration(let systemError):
- return String(
- format: NSLocalizedString(
- "RELOAD_VPN_CONFIGURATIONS_ERROR",
- tableName: "TunnelManager",
- value: "Failed to reload a VPN configuration: %@",
- comment: ""
- ),
- systemError.localizedDescription
- )
- case .saveVPNConfiguration(let systemError):
- return String(
- format: NSLocalizedString(
- "SAVE_VPN_CONFIGURATION_ERROR",
- tableName: "TunnelManager",
- value: "Failed to save a VPN tunnel configuration: %@",
- comment: ""
- ),
- systemError.localizedDescription
- )
- case .startVPNTunnel(let systemError):
- return String(
- format: NSLocalizedString(
- "START_VPN_TUNNEL_ERROR",
- tableName: "TunnelManager",
- value: "System error when starting the VPN tunnel: %@",
- comment: ""
- ),
- systemError.localizedDescription
- )
- case .removeVPNConfiguration(let systemError):
- return String(
- format: NSLocalizedString(
- "REMOVE_VPN_CONFIGURATION_ERROR",
- tableName: "TunnelManager",
- value: "Failed to remove the system VPN configuration: %@",
- comment: ""
- ),
- systemError.localizedDescription
- )
- case .readSettings:
- return NSLocalizedString(
- "READ_TUNNEL_SETTINGS_ERROR",
- tableName: "TunnelManager",
- value: "Failed to read settings",
- comment: ""
- )
- case .writeSettings:
- return NSLocalizedString(
- "WRITE_TUNNEL_SETTINGS_ERROR",
- tableName: "TunnelManager",
- value: "Failed to write settings",
- comment: ""
- )
- case .deleteSettings:
- return NSLocalizedString(
- "DELETE_TUNNEL_SETTINGS_ERROR",
- tableName: "TunnelManager",
- value: "Failed to delete 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 .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(
- "DEVICE_REVOKED_ERROR",
- tableName: "TunnelManager",
- value: "Device is revoked.",
- comment: ""
- )
- case .createDevice(let restError):
- return String(
- format: NSLocalizedString(
- "CREATE_DEVICE_ERROR",
- tableName: "TunnelManager",
- value: "Failed to create a device: %@",
- comment: ""
- ),
- restError.errorChainDescription ?? ""
- )
- case .rotateKey(let restError):
- return String(
- format: NSLocalizedString(
- "ROTATE_KEY_ERROR",
- tableName: "TunnelManager",
- value: "Failed to rotate WireGuard key: %@",
- comment: ""
- ),
- restError.errorChainDescription ?? ""
- )
- case .unsetAccount:
- return NSLocalizedString(
- "UNSET_ACCOUNT_ERROR",
- tableName: "TunnelManager",
- value: "Internal error: account is unset",
- comment: ""
- )
- case .unsetTunnel:
- return NSLocalizedString(
- "UNSET_TUNNEL_ERROR",
- tableName: "TunnelManager",
- value: "Tunnel is unset.",
- comment: ""
- )
- case .readRelays:
- return NSLocalizedString(
- "READ_RELAYS_ERROR",
- tableName: "TunnelManager",
- value: "Failed to read relays.",
- comment: ""
- )
- case .cannotSatisfyRelayConstraints:
- return NSLocalizedString(
- "CANNOT_SATISFY_RELAY_CONSTRAINTS_ERROR",
- tableName: "TunnelManager",
- value: "Failed to satisfy relay constraints.",
- comment: ""
- )
- case .reloadTunnel(let error):
- return String(
- format: NSLocalizedString(
- "RELOAD_TUNNEL_ERROR",
- tableName: "TunnelManager",
- value: "Failed to reload tunnel: %@",
- comment: ""
- ),
- error.localizedDescription
- )
- 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 ?? ""
- )
- }
- }
-}
-
extension SKError: LocalizedError {
public var errorDescription: String? {
switch self.code {
@@ -296,9 +113,7 @@ extension AppStorePaymentManager.Error: DisplayChainedError {
case .validateAccount(let restError):
let reason = restError.errorChainDescription ?? ""
- if case .unhandledResponse(_, let serverErrorResponse) = restError,
- serverErrorResponse?.code == .invalidAccount
- {
+ if restError.compareErrorCode(.invalidAccount) {
return String(
format: NSLocalizedString(
"INVALID_ACCOUNT_ERROR",
diff --git a/ios/MullvadVPN/LoginViewController.swift b/ios/MullvadVPN/LoginViewController.swift
index bdffad84b2..0b8213dfd6 100644
--- a/ios/MullvadVPN/LoginViewController.swift
+++ b/ios/MullvadVPN/LoginViewController.swift
@@ -16,7 +16,7 @@ enum AuthenticationMethod {
enum LoginState {
case `default`
case authenticating(AuthenticationMethod)
- case failure(TunnelManager.Error)
+ case failure(Error)
case success(AuthenticationMethod)
}
@@ -24,12 +24,12 @@ protocol LoginViewControllerDelegate: AnyObject {
func loginViewController(
_ controller: LoginViewController,
loginWithAccountToken accountToken: String,
- completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void
+ completion: @escaping (OperationCompletion<StoredAccountData?, Error>) -> Void
)
func loginViewControllerLoginWithNewAccount(
_ controller: LoginViewController,
- completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void
+ completion: @escaping (OperationCompletion<StoredAccountData?, Error>) -> Void
)
func loginViewControllerDidLogin(_ controller: LoginViewController)
@@ -389,7 +389,7 @@ private extension LoginState {
}
case .failure(let error):
- return error.errorChainDescription ?? ""
+ return error.localizedDescription
case .success(let method):
switch method {
diff --git a/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift b/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift
index 5fa9794bf9..ae3942ca30 100644
--- a/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift
+++ b/ios/MullvadVPN/Notifications/AccountExpiryNotificationProvider.swift
@@ -27,8 +27,8 @@ class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificatio
super.init()
- accountExpiry = TunnelManager.shared.accountExpiry
TunnelManager.shared.addObserver(self)
+ accountExpiry = TunnelManager.shared.deviceState.accountData?.expiry
}
private var trigger: UNNotificationTrigger? {
@@ -124,23 +124,31 @@ class AccountExpiryNotificationProvider: NotificationProvider, SystemNotificatio
)
}
+ private func invalidate(deviceState: DeviceState) {
+ accountExpiry = deviceState.accountData?.expiry
+ invalidate()
+ }
+
// MARK: - TunnelObserver
func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) {
- // no-op
+ invalidate(deviceState: manager.deviceState)
}
func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) {
// no-op
}
- func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) {
+ func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) {
// no-op
}
- func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) {
- accountExpiry = tunnelSettings?.account.expiry
- invalidate()
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) {
+ // no-op
+ }
+
+ func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) {
+ invalidate(deviceState: manager.deviceState)
}
}
diff --git a/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift b/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift
index 3018400c22..24c21479f2 100644
--- a/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift
+++ b/ios/MullvadVPN/Notifications/TunnelErrorNotificationProvider.swift
@@ -16,19 +16,22 @@ class TunnelErrorNotificationProvider: NotificationProvider, InAppNotificationPr
var notificationDescriptor: InAppNotificationDescriptor? {
guard let lastError = lastError else { return nil }
+ let body = (lastError as? LocalizedNotificationError)?.localizedNotificationBody
+ ?? lastError.localizedDescription
+
return InAppNotificationDescriptor(
identifier: identifier,
style: .error,
title: NSLocalizedString(
"TUNNEL_ERROR_INAPP_NOTIFICATION_TITLE",
- value: "Tunnel error",
+ value: "TUNNEL ERROR",
comment: ""
),
- body: lastError.errorChainDescription ?? "No error description provided."
+ body: body
)
}
- private var lastError: TunnelManager.Error?
+ private var lastError: Error?
override init() {
super.init()
@@ -52,17 +55,49 @@ class TunnelErrorNotificationProvider: NotificationProvider, InAppNotificationPr
invalidate()
}
- func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) {
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) {
+ // no-op
+ }
+
+ func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) {
// no-op
}
- func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) {
+ func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) {
// Save tunnel error
lastError = error
// Tell manager to refresh displayed notifications
invalidate()
}
+}
+
+protocol LocalizedNotificationError {
+ var localizedNotificationBody: String? { get }
+}
+extension StartTunnelError: LocalizedNotificationError {
+ var localizedNotificationBody: String? {
+ return String(
+ format: NSLocalizedString(
+ "START_TUNNEL_ERROR_INAPP_NOTIFICATION_BODY",
+ value: "Failed to start the tunnel: %@.",
+ comment: ""
+ ),
+ underlyingError.localizedDescription
+ )
+ }
+}
+extension StopTunnelError: LocalizedNotificationError {
+ var localizedNotificationBody: String? {
+ return String(
+ format: NSLocalizedString(
+ "STOP_TUNNEL_ERROR_INAPP_NOTIFICATION_BODY",
+ value: "Failed to stop the tunnel: %@.",
+ comment: ""
+ ),
+ underlyingError.localizedDescription
+ )
+ }
}
diff --git a/ios/MullvadVPN/Operations/OperationCompletion.swift b/ios/MullvadVPN/Operations/OperationCompletion.swift
index b8cf56a574..5bb73071a3 100644
--- a/ios/MullvadVPN/Operations/OperationCompletion.swift
+++ b/ios/MullvadVPN/Operations/OperationCompletion.swift
@@ -57,6 +57,14 @@ enum OperationCompletion<Success, Failure: Error> {
}
}
+ init(error: Failure?) where Success == Void {
+ if let error = error {
+ self = .failure(error)
+ } else {
+ self = .success(())
+ }
+ }
+
func map<NewSuccess>(_ block: (Success) -> NewSuccess) -> OperationCompletion<NewSuccess, Failure> {
switch self {
case .success(let value):
@@ -116,20 +124,6 @@ enum OperationCompletion<Success, Failure: Error> {
}
}
- func assertNoSuccess<NewSuccess>() -> OperationCompletion<NewSuccess, Failure> {
- return map { success in
- return success as! NewSuccess
- }
- }
-
- func assertFailure<NewFailure: Error>(_ failureType: NewFailure.Type)
- -> OperationCompletion<Success, NewFailure>
- {
- return mapError { error -> NewFailure in
- return error as! NewFailure
- }
- }
-
func ignoreOutput() -> OperationCompletion<Void, Failure> {
return map { _ in () }
}
diff --git a/ios/MullvadVPN/PreferencesViewController.swift b/ios/MullvadVPN/PreferencesViewController.swift
index d3ab3ffa74..05fdb793fd 100644
--- a/ios/MullvadVPN/PreferencesViewController.swift
+++ b/ios/MullvadVPN/PreferencesViewController.swift
@@ -11,8 +11,6 @@ import Logging
class PreferencesViewController: UITableViewController, PreferencesDataSourceDelegate, TunnelObserver {
- private let logger = Logger(label: "PreferencesViewController")
-
private let dataSource = PreferencesDataSource()
override var preferredStatusBarStyle: UIStatusBarStyle {
@@ -47,10 +45,7 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
navigationItem.rightBarButtonItem = editButtonItem
TunnelManager.shared.addObserver(self)
-
- if let dnsSettings = TunnelManager.shared.tunnelSettings?.dnsSettings {
- dataSource.update(from: dnsSettings)
- }
+ dataSource.update(from: TunnelManager.shared.settings.dnsSettings)
}
override func setEditing(_ editing: Bool, animated: Bool) {
@@ -73,11 +68,7 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
func preferencesDataSource(_ dataSource: PreferencesDataSource, didChangeViewModel dataModel: PreferencesViewModel) {
let dnsSettings = dataModel.asDNSSettings()
- TunnelManager.shared.setDNSSettings(dnsSettings) { [weak self] error in
- if let error = error {
- self?.logger.error(chainedError: error, message: "Failed to save DNS settings")
- }
- }
+ TunnelManager.shared.setDNSSettings(dnsSettings)
}
// MARK: - TunnelObserver
@@ -90,14 +81,17 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
// no-op
}
- func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) {
+ func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) {
// no-op
}
- func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) {
- guard let dnsSettings = tunnelSettings?.dnsSettings else { return }
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) {
+ dataSource.update(from: tunnelSettings.dnsSettings)
+ }
+
- dataSource.update(from: dnsSettings)
+ func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) {
+ // no-op
}
}
diff --git a/ios/MullvadVPN/ProblemReportViewController.swift b/ios/MullvadVPN/ProblemReportViewController.swift
index 5f1213f76c..0087faa18f 100644
--- a/ios/MullvadVPN/ProblemReportViewController.swift
+++ b/ios/MullvadVPN/ProblemReportViewController.swift
@@ -19,7 +19,9 @@ class ProblemReportViewController: UIViewController, UITextFieldDelegate, Condit
let securityGroupIdentifier = ApplicationConfiguration.securityGroupIdentifier
// TODO: make sure we redact old tokens
- let redactStrings = TunnelManager.shared.accountNumber.flatMap { [$0] } ?? []
+
+ let redactStrings = [TunnelManager.shared.deviceState.accountData?.number]
+ .compactMap { $0 }
let report = ConsolidatedApplicationLog(
redactCustomStrings: redactStrings,
diff --git a/ios/MullvadVPN/REST/RESTError.swift b/ios/MullvadVPN/REST/RESTError.swift
index e275a9ab34..175370563f 100644
--- a/ios/MullvadVPN/REST/RESTError.swift
+++ b/ios/MullvadVPN/REST/RESTError.swift
@@ -46,6 +46,14 @@ extension REST {
return "Failure to decode URL response data."
}
}
+
+ func compareErrorCode(_ code: ServerResponseCode) -> Bool {
+ if case .unhandledResponse(_, let serverResponse) = self {
+ return serverResponse?.code == code
+ } else {
+ return false
+ }
+ }
}
struct ServerErrorResponse: Decodable {
diff --git a/ios/MullvadVPN/REST/ServerRelaysResponse.swift b/ios/MullvadVPN/REST/ServerRelaysResponse.swift
index a5606510dc..f2f6baa4ad 100644
--- a/ios/MullvadVPN/REST/ServerRelaysResponse.swift
+++ b/ios/MullvadVPN/REST/ServerRelaysResponse.swift
@@ -18,7 +18,7 @@ extension REST {
let longitude: Double
}
- struct ServerRelay: Codable {
+ struct ServerRelay: Codable, Equatable {
let hostname: String
let active: Bool
let owned: Bool
diff --git a/ios/MullvadVPN/Result+Extensions.swift b/ios/MullvadVPN/Result+Extensions.swift
index 795ad8901d..9a2245d673 100644
--- a/ios/MullvadVPN/Result+Extensions.swift
+++ b/ios/MullvadVPN/Result+Extensions.swift
@@ -27,3 +27,14 @@ extension Result {
}
}
}
+
+extension Result {
+ func flattenValue<T>() -> T? where Success == Optional<T> {
+ switch self {
+ case .success(let optional):
+ return optional.flatMap { $0 }
+ case .failure:
+ return nil
+ }
+ }
+}
diff --git a/ios/MullvadVPN/RevokedDeviceViewController.swift b/ios/MullvadVPN/RevokedDeviceViewController.swift
new file mode 100644
index 0000000000..117ad69e91
--- /dev/null
+++ b/ios/MullvadVPN/RevokedDeviceViewController.swift
@@ -0,0 +1,191 @@
+//
+// RevokedDeviceViewController.swift
+// MullvadVPN
+//
+// Created by pronebird on 07/07/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+protocol RevokedDeviceViewControllerDelegate: AnyObject {
+ func revokedDeviceControllerDidRequestLogout(_ controller: RevokedDeviceViewController)
+}
+
+class RevokedDeviceViewController: UIViewController, RootContainment, TunnelObserver {
+
+ private lazy var imageView: StatusImageView = {
+ let statusImageView = StatusImageView(style: .failure)
+ statusImageView.translatesAutoresizingMaskIntoConstraints = false
+ return statusImageView
+ }()
+
+ private lazy var titleLabel: UILabel = {
+ let titleLabel = UILabel()
+ titleLabel.translatesAutoresizingMaskIntoConstraints = false
+ titleLabel.font = UIFont.systemFont(ofSize: 24, weight: .bold)
+ titleLabel.numberOfLines = 0
+ titleLabel.textColor = .white
+ titleLabel.text = NSLocalizedString(
+ "TITLE_LABEL",
+ tableName: "RevokedDevice",
+ value: "Device is inactive",
+ comment: ""
+ )
+ titleLabel.font = UIFont.systemFont(ofSize: 32)
+ return titleLabel
+ }()
+
+ private lazy var bodyLabel: UILabel = {
+ let bodyLabel = UILabel()
+ bodyLabel.translatesAutoresizingMaskIntoConstraints = false
+ bodyLabel.font = UIFont.systemFont(ofSize: 17)
+ bodyLabel.numberOfLines = 0
+ bodyLabel.textColor = .white
+ bodyLabel.text = NSLocalizedString(
+ "DESCRIPTION_LABEL",
+ tableName: "RevokedDevice",
+ value: "You have revoked this device. To connect again, you will need to log back in.",
+ comment: ""
+ )
+ return bodyLabel
+ }()
+
+ private lazy var footerLabel: UILabel = {
+ let bodyLabel = UILabel()
+ bodyLabel.translatesAutoresizingMaskIntoConstraints = false
+ bodyLabel.font = UIFont.systemFont(ofSize: 17)
+ bodyLabel.numberOfLines = 0
+ bodyLabel.textColor = .white
+ bodyLabel.text = NSLocalizedString(
+ "UNBLOCK_INTERNET_LABEL",
+ tableName: "RevokedDevice",
+ value: "Going to login will unblock the Internet on this device.",
+ comment: ""
+ )
+ return bodyLabel
+ }()
+
+ private lazy var logoutButton: AppButton = {
+ let button = AppButton(style: .default)
+ button.translatesAutoresizingMaskIntoConstraints = false
+ button.setTitle(
+ NSLocalizedString(
+ "GOTO_LOGIN_BUTTON_LABEL",
+ tableName: "RevokedDevice",
+ value: "Go to login",
+ comment: ""
+ ),
+ for: .normal
+ )
+ return button
+ }()
+
+ weak var delegate: RevokedDeviceViewControllerDelegate?
+
+ override var preferredStatusBarStyle: UIStatusBarStyle {
+ return .lightContent
+ }
+
+ var preferredHeaderBarPresentation: HeaderBarPresentation {
+ let tunnelState = TunnelManager.shared.tunnelStatus.state
+
+ return HeaderBarPresentation(
+ style: tunnelState.isSecured ? .secured : .unsecured,
+ showsDivider: true
+ )
+ }
+
+ var prefersHeaderBarHidden: Bool {
+ return false
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ view.backgroundColor = .secondaryColor
+ view.layoutMargins = UIMetrics.contentLayoutMargins
+
+ for subview in [imageView, titleLabel, bodyLabel, footerLabel, logoutButton] {
+ view.addSubview(subview)
+ }
+
+ logoutButton.addTarget(
+ self,
+ action: #selector(didTapLogoutButton(_:)),
+ for: .touchUpInside
+ )
+
+ NSLayoutConstraint.activate([
+ imageView.topAnchor.constraint(
+ equalTo: view.layoutMarginsGuide.topAnchor,
+ constant: 30
+ ),
+ imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
+
+ titleLabel.topAnchor.constraint(
+ equalTo: imageView.bottomAnchor,
+ constant: 30
+ ),
+ titleLabel.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
+ titleLabel.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
+
+ bodyLabel.topAnchor.constraint(
+ equalTo: titleLabel.bottomAnchor,
+ constant: 16
+ ),
+ bodyLabel.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
+ bodyLabel.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
+
+ footerLabel.topAnchor.constraint(
+ equalTo: bodyLabel.bottomAnchor,
+ constant: 16
+ ),
+ footerLabel.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
+ footerLabel.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
+
+ logoutButton.topAnchor.constraint(greaterThanOrEqualTo: footerLabel.bottomAnchor),
+ logoutButton.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
+ logoutButton.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
+ logoutButton.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor)
+ ])
+
+ TunnelManager.shared.addObserver(self)
+ updateView(tunnelState: TunnelManager.shared.tunnelStatus.state)
+ }
+
+ @objc private func didTapLogoutButton(_ sender: Any?) {
+ logoutButton.isEnabled = false
+
+ delegate?.revokedDeviceControllerDidRequestLogout(self)
+ }
+
+ private func updateView(tunnelState: TunnelState) {
+ logoutButton.style = tunnelState.isSecured ? .danger : .default
+ footerLabel.isHidden = !tunnelState.isSecured
+ }
+
+ // MARK: - TunnelObserver
+
+ func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) {
+ // no-op
+ }
+
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState) {
+ setNeedsHeaderBarStyleAppearanceUpdate()
+ updateView(tunnelState: tunnelState)
+ }
+
+ func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) {
+ // no-op
+ }
+
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) {
+ // no-op
+ }
+
+ func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) {
+ // no-op
+ }
+
+}
diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift
index 19ce61eb36..3d134115e1 100644
--- a/ios/MullvadVPN/SceneDelegate.swift
+++ b/ios/MullvadVPN/SceneDelegate.swift
@@ -17,6 +17,11 @@ class SceneDelegate: UIResponder {
private var isSceneConfigured = false
private let rootContainer = RootContainerViewController()
+
+ // Modal root container is used on iPad to present login, TOS, revoked device, device management
+ // view controllers above `rootContainer` which only contains split controller.
+ private lazy var modalRootContainer = RootContainerViewController()
+
private var splitViewController: CustomSplitViewController?
private var selectLocationViewController: SelectLocationViewController?
private var connectController: ConnectViewController?
@@ -39,7 +44,7 @@ class SceneDelegate: UIResponder {
window?.makeKeyAndVisible()
TunnelManager.shared.addObserver(self)
- if TunnelManager.shared.isLoadedConfiguration {
+ if TunnelManager.shared.isConfigurationLoaded {
configureScene()
}
}
@@ -207,9 +212,9 @@ extension SceneDelegate: RootContainerViewControllerDelegate {
}
func rootContainerViewAccessibilityPerformMagicTap(_ controller: RootContainerViewController) -> Bool {
- guard TunnelManager.shared.isAccountSet else { return false }
+ guard TunnelManager.shared.deviceState.isLoggedIn else { return false }
- switch TunnelManager.shared.tunnelState {
+ switch TunnelManager.shared.tunnelStatus.state {
case .connected, .connecting, .reconnecting:
TunnelManager.shared.reconnectTunnel(selectNewRelay: true)
case .disconnecting, .disconnected:
@@ -224,6 +229,7 @@ extension SceneDelegate: RootContainerViewControllerDelegate {
extension SceneDelegate {
private func setupPadUI() {
+ let tunnelManager = TunnelManager.shared
let selectLocationController = makeSelectLocationController()
let connectController = makeConnectViewController()
@@ -240,32 +246,77 @@ extension SceneDelegate {
self.connectController = connectController
rootContainer.setViewControllers([splitViewController], animated: false)
- showSplitViewMaster(TunnelManager.shared.isAccountSet, animated: false)
+ showSplitViewMaster(tunnelManager.deviceState.isLoggedIn, animated: false)
+
+ modalRootContainer.delegate = self
+
+ let showNextController = { [weak self] (animated: Bool) in
+ guard let self = self else { return }
- let rootContainerWrapper = makeLoginContainerController()
+ lazy var viewControllers: [UIViewController] = [self.makeLoginController()]
- if !TermsOfService.isAgreed {
- let termsOfServiceViewController = self.makeTermsOfServiceController { [weak self] viewController in
- guard let self = self else { return }
+ switch tunnelManager.deviceState {
+ case .loggedIn:
+ let didDismissModalRoot = {
+ self.showAccountSettingsControllerIfAccountExpired()
+ }
- if TunnelManager.shared.isAccountSet {
- rootContainerWrapper.dismiss(animated: true) {
- self.showAccountSettingsControllerIfAccountExpired()
- }
+ // Dismiss modal root container if needed before proceeding.
+ if self.isModalRootPresented {
+ self.modalRootContainer.dismiss(animated: animated, completion: didDismissModalRoot)
} else {
- rootContainerWrapper.pushViewController(self.makeLoginController(), animated: true)
+ didDismissModalRoot()
}
+
+ return
+
+ case .loggedOut:
+ break
+
+ case .revoked:
+ viewControllers.append(self.makeRevokedDeviceController())
}
- rootContainerWrapper.setViewControllers([termsOfServiceViewController], animated: false)
- rootContainer.present(rootContainerWrapper, animated: false)
- } else if !TunnelManager.shared.isAccountSet {
- rootContainerWrapper.setViewControllers([makeLoginController()], animated: false)
- rootContainer.present(rootContainerWrapper, animated: false)
+
+ // Configure modal container.
+ self.modalRootContainer.setViewControllers(
+ viewControllers,
+ animated: self.isModalRootPresented && animated
+ )
+
+ // Present modal container if not presented yet.
+ self.presentModalRootContainerIfNeeded(animated: animated)
+ }
+
+ if TermsOfService.isAgreed {
+ showNextController(false)
} else {
- self.showAccountSettingsControllerIfAccountExpired()
+ let termsOfServiceController = self.makeTermsOfServiceController { _ in
+ showNextController(true)
+ }
+
+ modalRootContainer.setViewControllers([termsOfServiceController], animated: false)
+ presentModalRootContainerIfNeeded(animated: false)
}
}
+ private func presentModalRootContainerIfNeeded(animated: Bool) {
+ modalRootContainer.preferredContentSize = CGSize(width: 480, height: 600)
+ modalRootContainer.modalPresentationStyle = .formSheet
+ modalRootContainer.presentationController?.delegate = self
+
+ if #available(iOS 13.0, *) {
+ modalRootContainer.isModalInPresentation = true
+ }
+
+ if modalRootContainer.presentingViewController == nil {
+ rootContainer.present(modalRootContainer, animated: animated)
+ }
+ }
+
+ private var isModalRootPresented: Bool {
+ return modalRootContainer.presentingViewController != nil
+ }
+
private func setupPhoneUI() {
let showNextController = { [weak self] (animated: Bool) in
guard let self = self else { return }
@@ -273,10 +324,17 @@ extension SceneDelegate {
let loginViewController = self.makeLoginController()
var viewControllers: [UIViewController] = [loginViewController]
- if TunnelManager.shared.isAccountSet {
+ switch TunnelManager.shared.deviceState {
+ case .loggedIn:
let connectController = self.makeConnectViewController()
viewControllers.append(connectController)
self.connectController = connectController
+
+ case .loggedOut:
+ break
+
+ case .revoked:
+ viewControllers.append(self.makeRevokedDeviceController())
}
self.rootContainer.setViewControllers(viewControllers, animated: animated) {
@@ -328,14 +386,13 @@ extension SceneDelegate {
selectLocationController.setCachedRelays(cachedRelays)
}
- let relayConstraints = TunnelManager.shared.tunnelSettings?.relayConstraints
- if let relayLocation = relayConstraints?.location.value {
- selectLocationController.setSelectedRelayLocation(
- relayLocation,
- animated: false,
- scrollPosition: .middle
- )
- }
+ let relayConstraints = TunnelManager.shared.settings.relayConstraints
+
+ selectLocationController.setSelectedRelayLocation(
+ relayConstraints.location.value,
+ animated: false,
+ scrollPosition: .middle
+ )
return selectLocationController
}
@@ -361,22 +418,10 @@ extension SceneDelegate {
return controller
}
- private func makeLoginContainerController() -> RootContainerViewController {
- let rootContainerWrapper = RootContainerViewController()
- rootContainerWrapper.delegate = self
- rootContainerWrapper.preferredContentSize = CGSize(width: 480, height: 600)
-
- if UIDevice.current.userInterfaceIdiom == .pad {
- rootContainerWrapper.modalPresentationStyle = .formSheet
- if #available(iOS 13.0, *) {
- // Prevent swiping off the login or terms of service controllers
- rootContainerWrapper.isModalInPresentation = true
- }
- }
-
- rootContainerWrapper.presentationController?.delegate = self
-
- return rootContainerWrapper
+ private func makeRevokedDeviceController() -> RevokedDeviceViewController {
+ let controller = RevokedDeviceViewController()
+ controller.delegate = self
+ return controller
}
private func makeLoginController() -> LoginViewController {
@@ -386,22 +431,57 @@ extension SceneDelegate {
}
private func showAccountSettingsControllerIfAccountExpired() {
- guard let accountExpiry = TunnelManager.shared.accountExpiry, accountExpiry <= Date() else { return }
+ guard case .loggedIn(let accountData, _) = TunnelManager.shared.deviceState else {
+ return
+ }
- rootContainer.showSettings(navigateTo: .account, animated: true)
+ if accountData.expiry <= Date() {
+ rootContainer.showSettings(navigateTo: .account, animated: true)
+ }
}
private func showSplitViewMaster(_ show: Bool, animated: Bool) {
splitViewController?.preferredDisplayMode = show ? .allVisible : .primaryHidden
connectController?.setMainContentHidden(!show, animated: animated)
}
+
+ private func showLoginViewAfterLogout(dismissController: UIViewController?) {
+ switch UIDevice.current.userInterfaceIdiom {
+ case .phone:
+ let loginController = rootContainer.viewControllers.first as? LoginViewController
+ loginController?.reset()
+
+ rootContainer.popToRootViewController(animated: false)
+ dismissController?.dismiss(animated: true)
+
+ case .pad:
+ let didDismissSourceController = {
+ self.presentModalRootContainerIfNeeded(animated: true)
+ }
+
+ let loginController = modalRootContainer.viewControllers.first as? LoginViewController
+ loginController?.reset()
+
+ modalRootContainer.popToRootViewController(animated: isModalRootPresented)
+ showSplitViewMaster(false, animated: true)
+
+ if let dismissController = dismissController {
+ dismissController.dismiss(animated: true, completion: didDismissSourceController)
+ } else {
+ didDismissSourceController()
+ }
+
+ default:
+ fatalError()
+ }
+ }
}
// MARK: - LoginViewControllerDelegate
extension SceneDelegate: LoginViewControllerDelegate {
- func loginViewController(_ controller: LoginViewController, loginWithAccountToken accountNumber: String, completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void) {
+ func loginViewController(_ controller: LoginViewController, loginWithAccountToken accountNumber: String, completion: @escaping (OperationCompletion<StoredAccountData?, Error>) -> Void) {
rootContainer.setEnableSettingsButton(false)
TunnelManager.shared.setAccount(action: .existing(accountNumber)) { operationCompletion in
@@ -411,7 +491,10 @@ extension SceneDelegate: LoginViewControllerDelegate {
// 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: AnyChainedError(error),
+ message: "Failed to log in with existing account."
+ )
fallthrough
case .cancelled:
@@ -422,7 +505,7 @@ extension SceneDelegate: LoginViewControllerDelegate {
}
}
- func loginViewControllerLoginWithNewAccount(_ controller: LoginViewController, completion: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void) {
+ func loginViewControllerLoginWithNewAccount(_ controller: LoginViewController, completion: @escaping (OperationCompletion<StoredAccountData?, Error>) -> Void) {
rootContainer.setEnableSettingsButton(false)
TunnelManager.shared.setAccount(action: .new) { operationCompletion in
@@ -432,7 +515,10 @@ extension SceneDelegate: LoginViewControllerDelegate {
// 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: AnyChainedError(error),
+ message: "Failed to log in with new account."
+ )
fallthrough
case .cancelled:
@@ -449,9 +535,9 @@ extension SceneDelegate: LoginViewControllerDelegate {
// Move the settings button back into header bar
rootContainer.removeSettingsButtonFromPresentationContainer()
- let relayConstraints = TunnelManager.shared.tunnelSettings?.relayConstraints
+ let relayConstraints = TunnelManager.shared.settings.relayConstraints
self.selectLocationViewController?.setSelectedRelayLocation(
- relayConstraints?.location.value,
+ relayConstraints.location.value,
animated: false,
scrollPosition: .middle
)
@@ -484,34 +570,11 @@ extension SceneDelegate: LoginViewControllerDelegate {
extension SceneDelegate: SettingsNavigationControllerDelegate {
func settingsNavigationController(_ controller: SettingsNavigationController, didFinishWithReason reason: SettingsDismissReason) {
- switch UIDevice.current.userInterfaceIdiom {
- case .phone:
- if case .userLoggedOut = reason {
- rootContainer.popToRootViewController(animated: false)
-
- let loginController = rootContainer.topViewController as? LoginViewController
-
- loginController?.reset()
- }
+ if case .userLoggedOut = reason {
+ showLoginViewAfterLogout(dismissController: controller)
+ } else {
controller.dismiss(animated: true)
-
- case .pad:
- if case .userLoggedOut = reason {
- self.showSplitViewMaster(false, animated: true)
- }
-
- controller.dismiss(animated: true) {
- if case .userLoggedOut = reason {
- let rootContainerWrapper = self.makeLoginContainerController()
- rootContainerWrapper.setViewControllers([self.makeLoginController()], animated: false)
- self.rootContainer.present(rootContainerWrapper, animated: true)
- }
- }
-
- default:
- fatalError()
}
-
}
}
@@ -565,14 +628,18 @@ extension SceneDelegate: SelectLocationViewControllerDelegate {
private func selectLocationControllerDidSelectRelayLocation(_ relayLocation: RelayLocation) {
let relayConstraints = RelayConstraints(location: .only(relayLocation))
- TunnelManager.shared.setRelayConstraints(relayConstraints) { error in
- if let error = error {
- self.logger.error(chainedError: error, message: "Failed to update relay constraints")
- } else {
- self.logger.debug("Updated relay constraints: \(relayConstraints)")
+ TunnelManager.shared.setRelayConstraints(relayConstraints) {
+ TunnelManager.shared.startTunnel()
+ }
+ }
+}
- TunnelManager.shared.startTunnel()
- }
+// MARK: - RevokedDeviceViewControllerDelegate
+
+extension SceneDelegate: RevokedDeviceViewControllerDelegate {
+ func revokedDeviceControllerDidRequestLogout(_ controller: RevokedDeviceViewController) {
+ TunnelManager.shared.unsetAccount { [weak self] in
+ self?.showLoginViewAfterLogout(dismissController: nil)
}
}
}
@@ -640,11 +707,59 @@ extension SceneDelegate: TunnelObserver {
// no-op
}
- func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) {
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) {
// no-op
}
- func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) {
+ func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) {
+ guard deviceState == .revoked else { return }
+
+ switch UIDevice.current.userInterfaceIdiom {
+ case .phone:
+ guard let loginController = rootContainer.viewControllers.first as? LoginViewController else {
+ return
+ }
+
+ loginController.reset()
+
+ let viewControllers = [
+ loginController,
+ makeRevokedDeviceController()
+ ]
+
+ rootContainer.setViewControllers(viewControllers, animated: true)
+
+ case .pad:
+ guard let loginController = modalRootContainer.viewControllers.first as? LoginViewController else {
+ return
+ }
+
+ loginController.reset()
+
+ let viewControllers = [
+ loginController,
+ makeRevokedDeviceController()
+ ]
+
+ let didDismissSettings = {
+ self.showSplitViewMaster(false, animated: true)
+ self.presentModalRootContainerIfNeeded(animated: true)
+ }
+
+ modalRootContainer.setViewControllers(viewControllers, animated: isModalRootPresented)
+
+ if let settingsNavController = settingsNavController {
+ settingsNavController.dismiss(animated: true, completion: didDismissSettings)
+ } else {
+ didDismissSettings()
+ }
+
+ default:
+ fatalError()
+ }
+ }
+
+ func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) {
// no-op
}
}
@@ -654,9 +769,7 @@ extension SceneDelegate: TunnelObserver {
extension SceneDelegate: RelayCacheObserver {
func relayCache(_ relayCache: RelayCache.Tracker, didUpdateCachedRelays cachedRelays: RelayCache.CachedRelays) {
- DispatchQueue.main.async {
- self.selectLocationViewController?.setCachedRelays(cachedRelays)
- }
+ selectLocationViewController?.setCachedRelays(cachedRelays)
}
}
diff --git a/ios/MullvadVPN/SettingsDataSource.swift b/ios/MullvadVPN/SettingsDataSource.swift
index 551d78b2f2..203d446b5f 100644
--- a/ios/MullvadVPN/SettingsDataSource.swift
+++ b/ios/MullvadVPN/SettingsDataSource.swift
@@ -67,7 +67,7 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab
super.init()
TunnelManager.shared.addObserver(self)
- storedAccountData = TunnelManager.shared.tunnelSettings?.account
+ storedAccountData = TunnelManager.shared.deviceState.accountData
updateDataSnapshot()
}
@@ -85,7 +85,7 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab
private func updateDataSnapshot() {
var newSnapshot = DataSourceSnapshot<Section, Item>()
- if TunnelManager.shared.isAccountSet {
+ if TunnelManager.shared.deviceState.isLoggedIn {
newSnapshot.appendSections([.main])
newSnapshot.appendItems([.account, .preferences, .wireguardKey], in: .main)
}
@@ -121,7 +121,7 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab
value: "Account",
comment: ""
)
- cell.accountExpiryDate = TunnelManager.shared.accountExpiry
+ cell.accountExpiryDate = TunnelManager.shared.deviceState.accountData?.expiry
cell.accessibilityIdentifier = "AccountCell"
cell.disclosureType = .chevron
@@ -238,7 +238,7 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab
// no-op
}
- func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) {
+ func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) {
// no-op
}
@@ -246,8 +246,8 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab
// no-op
}
- func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) {
- let newAccountData = tunnelSettings?.account
+ func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) {
+ let newAccountData = deviceState.accountData
let oldAccountData = storedAccountData
storedAccountData = newAccountData
@@ -267,4 +267,8 @@ class SettingsDataSource: NSObject, TunnelObserver, UITableViewDataSource, UITab
updateDataSnapshot()
tableView?.reloadData()
}
+
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) {
+ // no-op
+ }
}
diff --git a/ios/MullvadVPN/SettingsManager/SettingsManager.swift b/ios/MullvadVPN/SettingsManager/SettingsManager.swift
index 41945b807d..54aa293615 100644
--- a/ios/MullvadVPN/SettingsManager/SettingsManager.swift
+++ b/ios/MullvadVPN/SettingsManager/SettingsManager.swift
@@ -16,128 +16,169 @@ struct LegacyTunnelSettings {
let tunnelSettings: TunnelSettingsV1
}
-let keychainServiceName = "Mullvad VPN"
+private let keychainServiceName = "Mullvad VPN"
-enum KeychainAccountName: String, CaseIterable {
+private enum Item: String, CaseIterable {
case settings = "Settings"
+ case deviceState = "DeviceState"
case lastUsedAccount = "LastUsedAccount"
}
-extension SettingsManager {
+struct StringDecodingError: LocalizedError {
+ let data: Data
- // MARK: -
+ var errorDescription: String? {
+ return "Failed to decode string from data."
+ }
+}
- static func getLastUsedAccount() throws -> String {
- var query = createDefaultAttributes(accountName: .lastUsedAccount)
- query[kSecReturnData] = true
+struct StringEncodingError: LocalizedError {
+ let string: String
- var result: CFTypeRef?
- let status = SecItemCopyMatching(query as CFDictionary, &result)
+ var errorDescription: String? {
+ return "Failed to encode string into data."
+ }
+}
- guard status == errSecSuccess else {
- throw KeychainError(code: status)
- }
+extension SettingsManager {
- let data = result as! Data
+ // MARK: - Lsat used account
- return String(data: data, encoding: .utf8)!
+ static func getLastUsedAccount() throws -> String {
+ let data = try readItemData(.lastUsedAccount)
+
+ if let string = String(data: data, encoding: .utf8) {
+ return string
+ } else {
+ throw StringDecodingError(data: data)
+ }
}
static func setLastUsedAccount(_ string: String?) throws {
- let query = createDefaultAttributes(accountName: .lastUsedAccount)
+ if let string = string {
+ guard let data = string.data(using: .utf8) else {
+ throw StringEncodingError(string: string)
+ }
- guard let string = string else {
- switch SecItemDelete(query as CFDictionary) {
- case errSecSuccess, errSecItemNotFound:
+ try addOrUpdateItem(.lastUsedAccount, data: data)
+ } else {
+ do {
+ try deleteItem(.lastUsedAccount)
+ } catch let error as KeychainError where error == .itemNotFound {
return
- case let status:
- throw KeychainError(code: status)
+ } catch {
+ throw error
}
}
+ }
- let data = string.data(using: .utf8)!
- var status = SecItemUpdate(
- query as CFDictionary,
- [kSecValueData: data] as CFDictionary
- )
+ // MARK: - Settings
- switch status {
- case errSecItemNotFound:
- var insert = query
- insert[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlock
- insert[kSecValueData] = data
+ static func readSettings() throws -> TunnelSettingsV2 {
+ let data = try readItemData(.settings)
- status = SecItemAdd(insert as CFDictionary, nil)
- if status != errSecSuccess {
- throw KeychainError(code: status)
- }
- case errSecSuccess:
- break
- default:
- throw KeychainError(code: status)
- }
+ return try JSONDecoder().decode(TunnelSettingsV2.self, from: data)
}
- // MARK: -
+ static func writeSettings(_ settings: TunnelSettingsV2) throws {
+ let data = try JSONEncoder().encode(settings)
- static func readSettings() throws -> TunnelSettingsV2 {
- var query = createDefaultAttributes(accountName: .settings)
- query[kSecReturnData] = true
+ try addOrUpdateItem(.settings, data: data)
+ }
- var result: CFTypeRef?
- let status = SecItemCopyMatching(query as CFDictionary, &result)
+ static func deleteSettings() throws {
+ try deleteItem(.settings)
+ }
- guard status == errSecSuccess else {
- throw KeychainError(code: status)
- }
+ // MARK: - Device state
- let data = result as! Data
+ static func readDeviceState() throws -> DeviceState {
+ let data = try readItemData(.deviceState)
- let decoder = JSONDecoder()
- return try decoder.decode(TunnelSettingsV2.self, from: data)
+ return try JSONDecoder().decode(DeviceState.self, from: data)
}
- static func writeSettings(_ settings: TunnelSettingsV2) throws {
- let encoder = JSONEncoder()
- let data = try encoder.encode(settings)
+ static func writeDeviceState(_ deviceState: DeviceState) throws {
+ let data = try JSONEncoder().encode(deviceState)
+
+ try addOrUpdateItem(.deviceState, data: data)
+ }
+
+
+ static func deleteDeviceState() throws {
+ try deleteItem(.deviceState)
+ }
+
+ // MARK: - Keychain helpers
- let query = createDefaultAttributes(accountName: .settings)
- var status = SecItemUpdate(
+ private static func addItem(_ item: Item, data: Data) throws {
+ var query = createDefaultAttributes(item: item)
+ query.merge(createAccessAttributes()) { current, _ in
+ return current
+ }
+ query[kSecValueData] = data
+
+ let status = SecItemAdd(query as CFDictionary, nil)
+ if status != errSecSuccess {
+ throw KeychainError(code: status)
+ }
+ }
+
+ private static func updateItem(_ item: Item, data: Data) throws {
+ let query = createDefaultAttributes(item: item)
+ let 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
+ if status != errSecSuccess {
+ throw KeychainError(code: status)
+ }
+ }
+ private static func addOrUpdateItem(_ item: Item, data: Data) throws {
+ do {
+ try updateItem(item, data: data)
+ } catch let error as KeychainError where error == .itemNotFound {
+ try addItem(item, data: data)
+ } catch {
+ throw error
+ }
+ }
- status = SecItemAdd(insert as CFDictionary, nil)
- if status != errSecSuccess {
- throw KeychainError(code: status)
- }
- case errSecSuccess:
- break
- default:
+ private static func readItemData(_ item: Item) throws -> Data {
+ var query = createDefaultAttributes(item: item)
+ query[kSecReturnData] = true
+
+ var result: CFTypeRef?
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
+
+ if status == errSecSuccess {
+ return result as? Data ?? Data()
+ } else {
throw KeychainError(code: status)
}
}
- static func deleteSettings() throws {
- let query = createDefaultAttributes(accountName: .settings)
+ private static func deleteItem(_ item: Item) throws {
+ let query = createDefaultAttributes(item: item)
let status = SecItemDelete(query as CFDictionary)
if status != errSecSuccess {
throw KeychainError(code: status)
}
}
- private static func createDefaultAttributes(accountName: KeychainAccountName) -> [CFString: Any] {
+ private static func createDefaultAttributes(item: Item) -> [CFString: Any] {
return [
kSecClass: kSecClassGenericPassword,
kSecAttrService: keychainServiceName,
- kSecAttrAccount: accountName.rawValue
+ kSecAttrAccount: item.rawValue
+ ]
+ }
+
+ private static func createAccessAttributes() -> [CFString: Any] {
+ return [
+ kSecAttrAccessGroup: ApplicationConfiguration.securityGroupIdentifier,
+ kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock
]
}
@@ -251,6 +292,6 @@ extension SettingsManager {
return false
}
- return KeychainAccountName(rawValue: accountNumber) == nil
+ return Item(rawValue: accountNumber) == nil
}
}
diff --git a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
index 942e3a27fd..d21194c92f 100644
--- a/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
+++ b/ios/MullvadVPN/SettingsManager/TunnelSettingsV2.swift
@@ -13,17 +13,11 @@ 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
+ var relayConstraints: RelayConstraints = RelayConstraints()
/// DNS settings.
- var dnsSettings: DNSSettings
+ var dnsSettings: DNSSettings = DNSSettings()
}
struct StoredAccountData: Codable, Equatable {
@@ -37,6 +31,44 @@ struct StoredAccountData: Codable, Equatable {
var expiry: Date
}
+enum DeviceState: Codable, Equatable {
+ case loggedIn(StoredAccountData, StoredDeviceData)
+ case loggedOut
+ case revoked
+
+ private enum LoggedInCodableKeys: String, CodingKey {
+ case _0 = "account"
+ case _1 = "device"
+ }
+
+ var isLoggedIn: Bool {
+ switch self {
+ case .loggedIn:
+ return true
+ case .loggedOut, .revoked:
+ return false
+ }
+ }
+
+ var accountData: StoredAccountData? {
+ switch self {
+ case .loggedIn(let accountData, _):
+ return accountData
+ case .loggedOut, .revoked:
+ return nil
+ }
+ }
+
+ var deviceData: StoredDeviceData? {
+ switch self {
+ case .loggedIn(_, let deviceData):
+ return deviceData
+ case .loggedOut, .revoked:
+ return nil
+ }
+ }
+}
+
struct StoredDeviceData: Codable, Equatable {
/// Device creation date.
var creationDate: Date
diff --git a/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift b/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift
index 131bc97873..0fc5818d59 100644
--- a/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift
@@ -9,12 +9,12 @@
import Foundation
import Logging
-class LoadTunnelConfigurationOperation: ResultOperation<(), TunnelManager.Error> {
+class LoadTunnelConfigurationOperation: ResultOperation<(), Error> {
private let logger = Logger(label: "LoadTunnelConfigurationOperation")
- private let state: TunnelManager.State
+ private let interactor: TunnelInteractor
- init(dispatchQueue: DispatchQueue, state: TunnelManager.State) {
- self.state = state
+ init(dispatchQueue: DispatchQueue, interactor: TunnelInteractor) {
+ self.interactor = interactor
super.init(dispatchQueue: dispatchQueue)
}
@@ -23,7 +23,7 @@ class LoadTunnelConfigurationOperation: ResultOperation<(), TunnelManager.Error>
TunnelProviderManagerType.loadAllFromPreferences { tunnels, error in
self.dispatchQueue.async {
if let error = error {
- self.finish(completion: .failure(.loadAllVPNConfigurations(error)))
+ self.finish(completion: .failure(error))
} else {
self.didLoadVPNConfigurations(tunnels: tunnels)
}
@@ -32,73 +32,101 @@ class LoadTunnelConfigurationOperation: ResultOperation<(), TunnelManager.Error>
}
private func didLoadVPNConfigurations(tunnels: [TunnelProviderManagerType]?) {
- var returnError: TunnelManager.Error?
- var tunnelSettings: TunnelSettingsV2?
- do {
- tunnelSettings = try SettingsManager.readSettings()
- } catch .itemNotFound as KeychainError {
- logger.debug("Settings not found in keychain.")
- } catch let error as DecodingError {
- logger.error(
- chainedError: AnyChainedError(error),
- message: "Cannot decode settings. Will attempt to delete them from keychain."
- )
-
- do {
- try SettingsManager.deleteSettings()
- } catch {
- returnError = .deleteSettings(error)
-
- logger.error(
- chainedError: AnyChainedError(error),
- message: "Failed to delete settings from keychain."
- )
- }
- } catch {
- returnError = .readSettings(error)
-
- logger.error(
- chainedError: AnyChainedError(error),
- message: "Unexpected error when reading settings."
- )
- }
+ let settingsResult = readSettings()
+ let deviceStateResult = readDeviceState()
let tunnel = tunnels?.first.map { tunnelProvider in
return Tunnel(tunnelProvider: tunnelProvider)
}
- if let tunnelSettings = tunnelSettings {
- state.tunnelSettings = tunnelSettings
- state.setTunnel(tunnel, shouldRefreshTunnelState: true)
- state.isLoadedConfiguration = true
+ let settings = settingsResult.flattenValue()
+ let deviceState = deviceStateResult.flattenValue()
+
+ interactor.setSettings(settings ?? TunnelSettingsV2(), persist: false)
+ interactor.setDeviceState(deviceState ?? .loggedOut, persist: false)
- finish(completion: .success(()))
+ if let tunnel = tunnel, deviceState == nil {
+ logger.debug("Remove orphaned VPN configuration.")
+
+ tunnel.removeFromPreferences { error in
+ if let error = error {
+ self.logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to remove VPN configuration."
+ )
+ }
+ self.finishOperation(tunnel: nil)
+ }
} else {
- let onFinish = {
- self.state.tunnelSettings = nil
- self.state.setTunnel(nil, shouldRefreshTunnelState: true)
- self.state.isLoadedConfiguration = returnError == nil
+ finishOperation(tunnel: tunnel)
+ }
+ }
+ private func finishOperation(tunnel: Tunnel?) {
+ interactor.setTunnel(tunnel, shouldRefreshTunnelState: true)
+ interactor.setConfigurationLoaded()
+
+ finish(completion: .success(()))
+ }
+
+ private func readSettings() -> Result<TunnelSettingsV2?, Error> {
+ return Result { try SettingsManager.readSettings() }
+ .flatMapError { error in
+ if let error = error as? KeychainError, error == .itemNotFound {
+ logger.debug("Settings not found in keychain.")
- self.finish(completion: returnError.map { .failure($0) } ?? .success(()))
+ return .success(nil)
+ } else if let error = error as? DecodingError {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Cannot decode settings. Will attempt to delete them from keychain."
+ )
+
+ return Result { try SettingsManager.deleteSettings() }
+ .mapError { error in
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to delete settings from keychain."
+ )
+
+ return error
+ }
+ .map { _ in
+ return nil
+ }
+ } else {
+ return .failure(error)
+ }
}
+ }
- if let tunnel = tunnel {
- logger.debug("Remove orphaned VPN configuration.")
+ private func readDeviceState() -> Result<DeviceState?, Error> {
+ return Result { try SettingsManager.readDeviceState() }
+ .flatMapError { error in
+ if let error = error as? KeychainError, error == .itemNotFound {
+ logger.debug("Device state not found in keychain.")
- tunnel.removeFromPreferences { error in
- self.dispatchQueue.async {
- if let error = error {
- self.logger.error(
+ return .success(nil)
+ } else if let error = error as? DecodingError {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Cannot decode device state. Will attempt to delete it from keychain."
+ )
+
+ return Result { try SettingsManager.deleteDeviceState() }
+ .mapError { error in
+ logger.error(
chainedError: AnyChainedError(error),
- message: "Failed to remove VPN configuration."
+ message: "Failed to delete device state from keychain."
)
+
+ return error
}
- onFinish()
- }
+ .map { _ in
+ return nil
+ }
+ } else {
+ return .failure(error)
}
- } else {
- onFinish()
}
- }
}
}
diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
index dee1ef6d6b..873968521b 100644
--- a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
@@ -11,36 +11,31 @@ import NetworkExtension
import Logging
class MapConnectionStatusOperation: AsyncOperation {
- typealias StartTunnelHandler = () -> Void
-
- private let state: TunnelManager.State
+ private let interactor: TunnelInteractor
private let connectionStatus: NEVPNStatus
- private var startTunnelHandler: StartTunnelHandler?
private var request: Cancellable?
private let logger = Logger(label: "TunnelManager.MapConnectionStatusOperation")
init(
queue: DispatchQueue,
- state: TunnelManager.State,
- connectionStatus: NEVPNStatus,
- startTunnelHandler: @escaping StartTunnelHandler
+ interactor: TunnelInteractor,
+ connectionStatus: NEVPNStatus
)
{
- self.state = state
+ self.interactor = interactor
self.connectionStatus = connectionStatus
- self.startTunnelHandler = startTunnelHandler
super.init(dispatchQueue: queue)
}
override func main() {
- guard let tunnel = state.tunnel else {
+ guard let tunnel = interactor.tunnel else {
finish()
return
}
- let tunnelState = state.tunnelStatus.state
+ let tunnelState = interactor.tunnelStatus.state
switch connectionStatus {
case .connecting:
@@ -48,7 +43,7 @@ class MapConnectionStatusOperation: AsyncOperation {
case .connecting(.some(_)):
break
default:
- state.tunnelStatus.state = .connecting(nil)
+ interactor.updateTunnelState(.connecting(nil))
}
updateTunnelRelayAndFinish(tunnel: tunnel) { relay in
@@ -76,13 +71,11 @@ class MapConnectionStatusOperation: AsyncOperation {
case .disconnecting(.reconnect):
logger.debug("Restart the tunnel on disconnect.")
- state.tunnelStatus.reset(to: .pendingReconnect)
-
- startTunnelHandler?()
- startTunnelHandler = nil
+ interactor.resetTunnelState(to: .pendingReconnect)
+ interactor.startTunnel()
default:
- state.tunnelStatus.reset(to: .disconnected)
+ interactor.resetTunnelState(to: .disconnected)
}
case .disconnecting:
@@ -90,11 +83,11 @@ class MapConnectionStatusOperation: AsyncOperation {
case .disconnecting:
break
default:
- state.tunnelStatus.reset(to: .disconnecting(.nothing))
+ interactor.resetTunnelState(to: .disconnecting(.nothing))
}
case .invalid:
- state.tunnelStatus.reset(to: .disconnected)
+ interactor.resetTunnelState(to: .disconnected)
@unknown default:
logger.debug("Unknown NEVPNStatus: \(connectionStatus.rawValue)")
@@ -117,7 +110,7 @@ class MapConnectionStatusOperation: AsyncOperation {
self.dispatchQueue.async {
if case .success(let packetTunnelStatus) = completion, !self.isCancelled {
- self.state.tunnelStatus.update(
+ self.interactor.updateTunnelStatus(
from: packetTunnelStatus,
mappingRelayToState: mapRelayToState
)
diff --git a/ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift b/ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift
index 61327ec409..d67f8e37e8 100644
--- a/ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/MigrateSettingsOperation.swift
@@ -200,13 +200,13 @@ class MigrateSettingsOperation: AsyncOperation {
logger.debug("Store new settings...")
// Create new settings.
- let newSettings = TunnelSettingsV2(
- account: StoredAccountData(
+ let newDeviceState = DeviceState.loggedIn(
+ StoredAccountData(
identifier: accountData.id,
number: settings.accountNumber,
expiry: accountData.expiry
),
- device: StoredDeviceData(
+ StoredDeviceData(
creationDate: device.created,
identifier: device.id,
name: device.name,
@@ -217,7 +217,10 @@ class MigrateSettingsOperation: AsyncOperation {
creationDate: privateKeyWithMetadata.creationDate,
privateKey: privateKeyWithMetadata.privateKey
)
- ),
+ )
+ )
+
+ let newSettings = TunnelSettingsV2(
relayConstraints: tunnelSettings.relayConstraints,
dnsSettings: interfaceData.dnsSettings
)
@@ -225,6 +228,7 @@ class MigrateSettingsOperation: AsyncOperation {
// Save settings.
do {
try SettingsManager.writeSettings(newSettings)
+ try SettingsManager.writeDeviceState(newDeviceState)
} catch {
logger.error(
chainedError: AnyChainedError(error),
diff --git a/ios/MullvadVPN/TunnelManager/ReconnectTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/ReconnectTunnelOperation.swift
index 0a2995d4c1..e48d17482a 100644
--- a/ios/MullvadVPN/TunnelManager/ReconnectTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/ReconnectTunnelOperation.swift
@@ -8,27 +8,26 @@
import Foundation
-class ReconnectTunnelOperation: ResultOperation<(), TunnelManager.Error> {
- private let state: TunnelManager.State
+class ReconnectTunnelOperation: ResultOperation<(), Error> {
+ private let interactor: TunnelInteractor
private let selectNewRelay: Bool
private var task: Cancellable?
init(
dispatchQueue: DispatchQueue,
- state: TunnelManager.State,
+ interactor: TunnelInteractor,
selectNewRelay: Bool
)
{
- self.state = state
+ self.interactor = interactor
self.selectNewRelay = selectNewRelay
super.init(dispatchQueue: dispatchQueue)
}
override func main() {
- guard let tunnel = self.state.tunnel,
- let relayConstraints = state.tunnelSettings?.relayConstraints else {
- finish(completion: .failure(.unsetTunnel))
+ guard let tunnel = interactor.tunnel else {
+ finish(completion: .failure(UnsetTunnelError()))
return
}
@@ -39,17 +38,17 @@ class ReconnectTunnelOperation: ResultOperation<(), TunnelManager.Error> {
let cachedRelays = try RelayCache.Tracker.shared.getCachedRelays()
selectorResult = try RelaySelector.evaluate(
relays: cachedRelays.relays,
- constraints: relayConstraints
+ constraints: interactor.settings.relayConstraints
)
}
task = tunnel.reconnectTunnel(
relaySelectorResult: selectorResult
) { [weak self] completion in
- self?.finish(completion: completion.mapError { .reloadTunnel($0) })
+ self?.finish(completion: completion)
}
} catch {
- finish(completion: .failure(.reloadTunnel(error)))
+ finish(completion: .failure(error))
}
}
diff --git a/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift b/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift
index d8a0a4fed1..ae5470b75e 100644
--- a/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift
@@ -10,8 +10,8 @@ import Foundation
import Logging
import class WireGuardKitTypes.PrivateKey
-class RotateKeyOperation: ResultOperation<Bool, TunnelManager.Error> {
- private let state: TunnelManager.State
+class RotateKeyOperation: ResultOperation<Bool, Error> {
+ private let interactor: TunnelInteractor
private let devicesProxy: REST.DevicesProxy
private var task: Cancellable?
@@ -21,31 +21,29 @@ class RotateKeyOperation: ResultOperation<Bool, TunnelManager.Error> {
init(
dispatchQueue: DispatchQueue,
- state: TunnelManager.State,
+ interactor: TunnelInteractor,
devicesProxy: REST.DevicesProxy,
- rotationInterval: TimeInterval?,
- completionHandler: @escaping CompletionHandler
+ rotationInterval: TimeInterval?
) {
- self.state = state
-
+ self.interactor = interactor
self.devicesProxy = devicesProxy
self.rotationInterval = rotationInterval
super.init(
dispatchQueue: dispatchQueue,
- completionQueue: dispatchQueue,
- completionHandler: completionHandler
+ completionQueue: nil,
+ completionHandler: nil
)
}
override func main() {
- guard let tunnelSettings = state.tunnelSettings else {
- finish(completion: .failure(.unsetAccount))
+ guard case .loggedIn(let accountData, let deviceData) = interactor.deviceState else {
+ finish(completion: .failure(InvalidDeviceStateError()))
return
}
if let rotationInterval = rotationInterval {
- let creationDate = tunnelSettings.device.wgKeyData.creationDate
+ let creationDate = deviceData.wgKeyData.creationDate
let nextRotationDate = creationDate.addingTimeInterval(rotationInterval)
if nextRotationDate > Date() {
@@ -65,14 +63,13 @@ class RotateKeyOperation: ResultOperation<Bool, TunnelManager.Error> {
let newPrivateKey = PrivateKey()
task = devicesProxy.rotateDeviceKey(
- accountNumber: tunnelSettings.account.number,
- identifier: tunnelSettings.device.identifier,
+ accountNumber: accountData.number,
+ identifier: deviceData.identifier,
publicKey: newPrivateKey.publicKey,
retryStrategy: .default
) { completion in
self.dispatchQueue.async {
self.didRotateKey(
- tunnelSettings: tunnelSettings,
newPrivateKey: newPrivateKey,
completion: completion
)
@@ -86,47 +83,39 @@ class RotateKeyOperation: ResultOperation<Bool, TunnelManager.Error> {
}
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."
- )
+ switch completion {
+ case .success(let device):
+ logger.debug("Successfully rotated device key. Persisting settings...")
- 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
- )
+ switch interactor.deviceState {
+ case .loggedIn(let accountData, var deviceData):
+ deviceData.update(from: device)
+ deviceData.wgKeyData = StoredWgKeyData(
+ creationDate: Date(),
+ privateKey: newPrivateKey
+ )
- try SettingsManager.writeSettings(newTunnelSettings)
+ interactor.setDeviceState(.loggedIn(accountData, deviceData), persist: true)
- state.tunnelSettings = newTunnelSettings
+ finish(completion: .success(true))
+ default:
+ finish(completion: .failure(InvalidDeviceStateError()))
+ }
- finish(completion: .success(true))
- } catch {
+ case .failure(let error):
logger.error(
chainedError: AnyChainedError(error),
- message: "Failed to write settings."
+ message: "Failed to rotate device key."
)
+ finish(completion: .failure(error))
- finish(completion: .failure(.writeSettings(error)))
+ case .cancelled:
+ finish(completion: .cancelled)
}
+
}
}
diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
index c92c6a1c0b..022264489d 100644
--- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
@@ -59,14 +59,11 @@ private struct SetAccountContext: OperationInputContext {
}
}
-class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Error> {
- typealias WillDeleteVPNConfigurationHandler = () -> Void
-
- private let state: TunnelManager.State
+class SetAccountOperation: ResultOperation<StoredAccountData?, Error> {
+ private let interactor: TunnelInteractor
private let accountsProxy: REST.AccountsProxy
private let devicesProxy: REST.DevicesProxy
private let action: SetAccountAction
- private var willDeleteVPNConfigurationHandler: WillDeleteVPNConfigurationHandler?
private let logger = Logger(label: "SetAccountOperation")
private let operationQueue = AsyncOperationQueue()
@@ -75,36 +72,37 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err
init(
dispatchQueue: DispatchQueue,
- state: TunnelManager.State,
+ interactor: TunnelInteractor,
accountsProxy: REST.AccountsProxy,
devicesProxy: REST.DevicesProxy,
- action: SetAccountAction,
- willDeleteVPNConfigurationHandler: @escaping WillDeleteVPNConfigurationHandler
+ action: SetAccountAction
)
{
- self.state = state
+ self.interactor = interactor
self.accountsProxy = accountsProxy
self.devicesProxy = devicesProxy
self.action = action
- self.willDeleteVPNConfigurationHandler = willDeleteVPNConfigurationHandler
super.init(dispatchQueue: dispatchQueue)
}
override func main() {
var deleteDeviceOperation: AsyncOperation?
- if let tunnelSettings = state.tunnelSettings {
- let operation = getDeleteDeviceOperation(tunnelSettings: tunnelSettings)
+ if case .loggedIn(let accountData, let deviceData) = interactor.deviceState {
+ let operation = getDeleteDeviceOperation(
+ accounNumber: accountData.number,
+ deviceIdentifier: deviceData.identifier
+ )
deleteDeviceOperation = operation
}
- let deleteSettingsOperation = getDeleteSettingsOperation()
- deleteSettingsOperation.addCondition(
+ let unsetDeviceStateOperation = getUnsetDeviceStateOperation()
+ unsetDeviceStateOperation.addCondition(
NoFailedDependenciesCondition(ignoreCancellations: false)
)
if let deleteDeviceOperation = deleteDeviceOperation {
- deleteSettingsOperation.addDependency(deleteDeviceOperation)
+ unsetDeviceStateOperation.addDependency(deleteDeviceOperation)
}
let setupAccountOperations = getAccountDataOperation()
@@ -112,7 +110,7 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err
accountOperation.addCondition(
NoFailedDependenciesCondition(ignoreCancellations: false)
)
- accountOperation.addDependency(deleteSettingsOperation)
+ accountOperation.addDependency(unsetDeviceStateOperation)
let createDeviceOperation = getCreateDeviceOperation()
createDeviceOperation.addCondition(
@@ -144,7 +142,7 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err
return [accountOperation, createDeviceOperation, saveSettingsOperation]
} ?? []
- var enqueueOperations: [Operation] = [deleteDeviceOperation, deleteSettingsOperation]
+ var enqueueOperations: [Operation] = [deleteDeviceOperation, unsetDeviceStateOperation]
.compactMap { $0 }
enqueueOperations.append(contentsOf: setupAccountOperations)
@@ -173,8 +171,8 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err
return
}
- let errors = children.compactMap { operation -> TunnelManager.Error? in
- return (operation as? AsyncOperation)?.error as? TunnelManager.Error
+ let errors = children.compactMap { operation -> Error? in
+ return (operation as? AsyncOperation)?.error
}
if let error = errors.first {
@@ -184,9 +182,7 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err
}
}
- private func getAccountDataOperation()
- -> ResultOperation<StoredAccountData, TunnelManager.Error>?
- {
+ private func getAccountDataOperation() -> ResultOperation<StoredAccountData, Error>? {
switch action {
case .new:
return getCreateAccountOperation()
@@ -199,41 +195,30 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err
}
}
- private func getCreateAccountOperation()
- -> ResultBlockOperation<StoredAccountData, TunnelManager.Error>
- {
- let operation = ResultBlockOperation<
- StoredAccountData,
- TunnelManager.Error
- >(dispatchQueue: dispatchQueue)
+ private func getCreateAccountOperation() -> ResultBlockOperation<StoredAccountData, Error> {
+ let operation = ResultBlockOperation<StoredAccountData, Error>(dispatchQueue: dispatchQueue)
operation.setExecutionBlock { operation in
self.logger.debug("Create new account...")
let task = self.accountsProxy.createAccount(retryStrategy: .default) { completion in
- let mappedCompletion = completion.mapError { error -> TunnelManager.Error in
+ let mappedCompletion = completion.mapError { error -> Error in
self.logger.error(
chainedError: AnyChainedError(error),
message: "Failed to create new account."
)
+ return error
+ }.map { newAccountData -> StoredAccountData in
+ self.logger.debug("Created new account.")
- return .createAccount(error)
- }
-
- guard let newAccountData = mappedCompletion.value else {
- operation.finish(completion: mappedCompletion.assertNoSuccess())
- return
+ return StoredAccountData(
+ identifier: newAccountData.id,
+ number: newAccountData.number,
+ expiry: newAccountData.expiry
+ )
}
- self.logger.debug("Created new account.")
-
- let storedAccountData = StoredAccountData(
- identifier: newAccountData.id,
- number: newAccountData.number,
- expiry: newAccountData.expiry
- )
-
- operation.finish(completion: .success(storedAccountData))
+ operation.finish(completion: mappedCompletion)
}
operation.addCancellationBlock {
@@ -245,11 +230,9 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err
}
private func getExistingAccountOperation(accountNumber: String)
- -> ResultOperation<StoredAccountData, TunnelManager.Error>
+ -> ResultOperation<StoredAccountData, Error>
{
- let operation = ResultBlockOperation<StoredAccountData, TunnelManager.Error>(
- dispatchQueue: dispatchQueue
- )
+ let operation = ResultBlockOperation<StoredAccountData, Error>(dispatchQueue: dispatchQueue)
operation.setExecutionBlock { operation in
self.logger.debug("Request account data...")
@@ -258,29 +241,23 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err
accountNumber: accountNumber,
retryStrategy: .default
) { completion in
- let mappedCompletion = completion.mapError { error -> TunnelManager.Error in
+ let mappedCompletion = completion.mapError { error -> Error in
self.logger.error(
chainedError: AnyChainedError(error),
message: "Failed to receive account data."
)
+ return error
+ }.map { accountData -> StoredAccountData in
+ self.logger.debug("Received account data.")
- return .getAccountData(error)
- }
-
- guard let accountData = mappedCompletion.value else {
- operation.finish(completion: mappedCompletion.assertNoSuccess())
- return
+ return StoredAccountData(
+ identifier: accountData.id,
+ number: accountNumber,
+ expiry: accountData.expiry
+ )
}
- self.logger.debug("Received account data.")
-
- let storedAccountData = StoredAccountData(
- identifier: accountData.id,
- number: accountNumber,
- expiry: accountData.expiry
- )
-
- operation.finish(completion: .success(storedAccountData))
+ operation.finish(completion: mappedCompletion)
}
operation.addCancellationBlock {
@@ -291,10 +268,10 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err
return operation
}
- private func getDeleteDeviceOperation(tunnelSettings: TunnelSettingsV2)
- -> ResultBlockOperation<Void, TunnelManager.Error>
+ private func getDeleteDeviceOperation(accounNumber: String, deviceIdentifier: String)
+ -> ResultBlockOperation<Void, Error>
{
- let operation = ResultBlockOperation<Void, TunnelManager.Error>(
+ let operation = ResultBlockOperation<Void, Error>(
dispatchQueue: dispatchQueue
)
@@ -302,28 +279,26 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err
self.logger.debug("Delete current device...")
let task = self.devicesProxy.deleteDevice(
- accountNumber: tunnelSettings.account.number,
- identifier: tunnelSettings.device.identifier,
+ accountNumber: accounNumber,
+ identifier: deviceIdentifier,
retryStrategy: .default
) { completion in
- let mappedCompletion = completion.mapError { error -> TunnelManager.Error in
- self.logger.error(chainedError: error, message: "Failed to delete device.")
-
- return .deleteDevice(error)
- }
-
- guard let isDeleted = mappedCompletion.value else {
- operation.finish(completion: mappedCompletion.assertNoSuccess())
- return
- }
-
- if isDeleted {
- self.logger.debug("Deleted device.")
- } else {
- self.logger.debug("Device is already deleted.")
- }
+ let mappedCompletion = completion
+ .mapError { error -> Error in
+ self.logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to delete device."
+ )
+ return error
+ }.map { isDeleted in
+ if isDeleted {
+ self.logger.debug("Deleted device.")
+ } else {
+ self.logger.debug("Device is already deleted.")
+ }
+ }
- operation.finish(completion: .success(()))
+ operation.finish(completion: mappedCompletion)
}
operation.addCancellationBlock {
@@ -334,47 +309,25 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err
return operation
}
- private func getDeleteSettingsOperation() -> ResultBlockOperation<Void, TunnelManager.Error> {
- let deleteSettingsOperation = ResultBlockOperation<Void, TunnelManager.Error>(
- dispatchQueue: dispatchQueue
- )
-
- deleteSettingsOperation.setExecutionBlock { operation in
- self.logger.debug("Delete settings.")
-
- do {
- try SettingsManager.deleteSettings()
- } catch .itemNotFound as KeychainError {
- self.logger.debug("Settings are already deleted.")
- } catch {
- self.logger.error(
- chainedError: AnyChainedError(error),
- message: "Failed to delete settings."
- )
- operation.finish(completion: .failure(.deleteSettings(error)))
- return
- }
-
+ private func getUnsetDeviceStateOperation() -> AsyncBlockOperation {
+ return AsyncBlockOperation(dispatchQueue: dispatchQueue) { operation in
// Tell the caller to unsubscribe from VPN status notifications.
- self.willDeleteVPNConfigurationHandler?()
- self.willDeleteVPNConfigurationHandler = nil
+ self.interactor.prepareForVPNConfigurationDeletion()
- // Reset tunnel state to disconnected
- self.state.tunnelStatus.reset(to: .disconnected)
-
- // Remove tunnel settins
- self.state.tunnelSettings = nil
+ // Reset tunnel and device state.
+ self.interactor.resetTunnelState(to: .disconnected)
+ self.interactor.setDeviceState(.loggedOut, persist: true)
// Finish immediately if tunnel provider is not set.
- guard let tunnel = self.state.tunnel else {
- operation.finish(completion: .success(()))
+ guard let tunnel = self.interactor.tunnel else {
+ operation.finish()
return
}
- // Remove VPN configuration
+ // Remove VPN configuration.
tunnel.removeFromPreferences { error in
self.dispatchQueue.async {
- // Ignore error but log it
+ // Ignore error but log it.
if let error = error {
self.logger.error(
chainedError: AnyChainedError(error),
@@ -382,23 +335,21 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err
)
}
- self.state.setTunnel(nil, shouldRefreshTunnelState: false)
+ self.interactor.setTunnel(nil, shouldRefreshTunnelState: false)
- operation.finish(completion: .success(()))
+ operation.finish()
}
}
}
-
- return deleteSettingsOperation
}
private func getCreateDeviceOperation()
- -> TransformOperation<StoredAccountData, (PrivateKey, REST.Device), TunnelManager.Error>
+ -> TransformOperation<StoredAccountData, (PrivateKey, REST.Device), Error>
{
let createDeviceOperation = TransformOperation<
StoredAccountData,
(PrivateKey, REST.Device),
- TunnelManager.Error
+ Error
>(dispatchQueue: dispatchQueue)
createDeviceOperation.setExecutionBlock { storedAccountData, operation in
@@ -431,9 +382,9 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err
.map { device in
return (privateKey, device)
}
- .mapError { error -> TunnelManager.Error in
+ .mapError { error -> Error in
self.logger.error(chainedError: error, message: "Failed to create device.")
- return .createDevice(error)
+ return error
}
operation.finish(completion: mappedCompletion)
@@ -448,21 +399,19 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err
}
private func getSaveSettingsOperation()
- -> TransformOperation<SetAccountResult, StoredAccountData, TunnelManager.Error>
+ -> TransformOperation<SetAccountResult, StoredAccountData, Error>
{
let saveSettingsOperation = TransformOperation<
- SetAccountResult,
- StoredAccountData,
- TunnelManager.Error
+ SetAccountResult, StoredAccountData, Error
>(dispatchQueue: dispatchQueue)
saveSettingsOperation.setExecutionBlock { input in
self.logger.debug("Saving settings...")
let device = input.device
- let tunnelSettings = TunnelSettingsV2(
- account: input.accountData,
- device: StoredDeviceData(
+ let newDeviceState = DeviceState.loggedIn(
+ input.accountData,
+ StoredDeviceData(
creationDate: device.created,
identifier: device.id,
name: device.name,
@@ -473,25 +422,13 @@ class SetAccountOperation: ResultOperation<StoredAccountData?, TunnelManager.Err
creationDate: Date(),
privateKey: input.privateKey
)
- ),
- relayConstraints: RelayConstraints(),
- dnsSettings: DNSSettings()
+ )
)
- do {
- try SettingsManager.writeSettings(tunnelSettings)
-
- self.state.tunnelSettings = tunnelSettings
+ self.interactor.setSettings(TunnelSettingsV2(), persist: true)
+ self.interactor.setDeviceState(newDeviceState, persist: true)
- return input.accountData
- } catch {
- self.logger.error(
- chainedError: AnyChainedError(error),
- message: "Failed to write settings."
- )
-
- throw TunnelManager.Error.writeSettings(error)
- }
+ return input.accountData
}
return saveSettingsOperation
diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
index 2ffa3cb9d6..cb3a738490 100644
--- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
@@ -8,22 +8,21 @@
import Foundation
import NetworkExtension
+import Logging
-class StartTunnelOperation: ResultOperation<(), TunnelManager.Error> {
+class StartTunnelOperation: ResultOperation<(), Error> {
typealias EncodeErrorHandler = (Error) -> Void
- private let state: TunnelManager.State
- private var encodeErrorHandler: EncodeErrorHandler?
+ private let interactor: TunnelInteractor
+ private let logger = Logger(label: "StartTunnelOperation")
init(
dispatchQueue: DispatchQueue,
- state: TunnelManager.State,
- encodeErrorHandler: @escaping EncodeErrorHandler,
+ interactor: TunnelInteractor,
completionHandler: @escaping CompletionHandler
)
{
- self.state = state
- self.encodeErrorHandler = encodeErrorHandler
+ self.interactor = interactor
super.init(
dispatchQueue: dispatchQueue,
@@ -33,118 +32,119 @@ class StartTunnelOperation: ResultOperation<(), TunnelManager.Error> {
}
override func main() {
- guard let tunnelSettings = state.tunnelSettings else {
- finish(completion: .failure(.unsetAccount))
+ guard case .loggedIn = interactor.deviceState else {
+ finish(completion: .failure(InvalidDeviceStateError()))
return
}
- switch state.tunnelStatus.state {
+ switch interactor.tunnelStatus.state {
case .disconnecting(.nothing):
- state.tunnelStatus.state = .disconnecting(.reconnect)
+ interactor.updateTunnelState(.disconnecting(.reconnect))
finish(completion: .success(()))
case .disconnected, .pendingReconnect:
- guard let cachedRelays = try? RelayCache.Tracker.shared.getCachedRelays() else {
- finish(completion: .failure(.readRelays))
- return
- }
+ do {
+ let cachedRelays = try RelayCache.Tracker.shared.getCachedRelays()
+ let selectorResult = try RelaySelector.evaluate(
+ relays: cachedRelays.relays,
+ constraints: interactor.settings.relayConstraints
+ )
- didReceiveRelays(
- tunnelSettings: tunnelSettings,
- cachedRelays: cachedRelays
- )
+ makeTunnelProviderAndStartTunnel(selectorResult: selectorResult) { error in
+ self.finish(completion: OperationCompletion(error: error))
+ }
+ } catch {
+ finish(completion: .failure(error))
+ }
default:
- // Do not attempt to start the tunnel in all other cases.
finish(completion: .success(()))
}
}
- private func didReceiveRelays(tunnelSettings: TunnelSettingsV2, cachedRelays: RelayCache.CachedRelays) {
- guard let selectorResult = try? RelaySelector.evaluate(
- relays: cachedRelays.relays,
- constraints: tunnelSettings.relayConstraints
- ) else {
- finish(completion: .failure(.cannotSatisfyRelayConstraints))
- return
- }
-
- Self.makeTunnelProvider { makeTunnelProviderResult in
+ private func makeTunnelProviderAndStartTunnel(
+ selectorResult: RelaySelectorResult,
+ completionHandler: @escaping (Error?) -> Void
+ ) {
+ Self.makeTunnelProvider { result 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)
- }
+ do {
+ let tunnelProvider = try result.get()
- self.finish(completion: OperationCompletion(result: startTunnelResult))
+ try self.startTunnel(
+ tunnelProvider: tunnelProvider,
+ selectorResult: selectorResult
+ )
- case .failure(let error):
- self.finish(completion: .failure(error))
+ completionHandler(nil)
+ } catch {
+ completionHandler(error)
}
}
}
}
- private func startTunnel(tunnelProvider: TunnelProviderManagerType, selectorResult: RelaySelectorResult) throws {
+ private func startTunnel(
+ tunnelProvider: TunnelProviderManagerType,
+ selectorResult: RelaySelectorResult
+ ) throws {
var tunnelOptions = PacketTunnelOptions()
do {
try tunnelOptions.setSelectorResult(selectorResult)
} catch {
- encodeErrorHandler?(error)
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to encode the selector result."
+ )
}
- encodeErrorHandler = nil
-
- state.setTunnel(Tunnel(tunnelProvider: tunnelProvider), shouldRefreshTunnelState: false)
- state.tunnelStatus.reset(to: .connecting(selectorResult.packetTunnelRelay))
+ interactor.setTunnel(Tunnel(tunnelProvider: tunnelProvider), shouldRefreshTunnelState: false)
+ interactor.resetTunnelState(to: .connecting(selectorResult.packetTunnelRelay))
try tunnelProvider.connection.startVPNTunnel(options: tunnelOptions.rawOptions())
}
- private class func makeTunnelProvider(completionHandler: @escaping (Result<TunnelProviderManagerType, TunnelManager.Error>) -> Void) {
+ private class func makeTunnelProvider(completionHandler: @escaping (Result<TunnelProviderManagerType, Error>) -> Void) {
TunnelProviderManagerType.loadAllFromPreferences { tunnelProviders, error in
if let error = error {
- completionHandler(.failure(.loadAllVPNConfigurations(error)))
+ completionHandler(.failure(error))
return
}
- let protocolConfig = NETunnelProviderProtocol()
- protocolConfig.providerBundleIdentifier = ApplicationConfiguration.packetTunnelExtensionIdentifier
- protocolConfig.serverAddress = ""
-
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
+ configureTunnelProvider(tunnelProvider)
tunnelProvider.saveToPreferences { error in
if let error = error {
- completionHandler(.failure(.saveVPNConfiguration(error)))
- return
- }
-
- // Refresh connection status after saving the tunnel preferences.
- // Basically it's only necessary to do for new instances of
- // `NETunnelProviderManager`, but we do that for the existing ones too
- // for simplicity as it has no side effects.
- tunnelProvider.loadFromPreferences { error in
- if let error = error {
- completionHandler(.failure(.reloadVPNConfiguration(error)))
- } else {
- completionHandler(.success(tunnelProvider))
+ completionHandler(.failure(error))
+ } else {
+ // Refresh connection status after saving the tunnel preferences.
+ // Basically it's only necessary to do for new instances of
+ // `NETunnelProviderManager`, but we do that for the existing ones too
+ // for simplicity as it has no side effects.
+ tunnelProvider.loadFromPreferences { error in
+ completionHandler(error.map { .failure($0) } ?? .success(tunnelProvider))
}
}
}
}
}
+
+ private class func configureTunnelProvider(_ tunnelProvider: TunnelProviderManagerType) {
+ let protocolConfig = NETunnelProviderProtocol()
+ protocolConfig.providerBundleIdentifier = ApplicationConfiguration.packetTunnelExtensionIdentifier
+ protocolConfig.serverAddress = ""
+
+ tunnelProvider.isEnabled = true
+ tunnelProvider.localizedDescription = "WireGuard"
+ tunnelProvider.protocolConfiguration = protocolConfig
+
+ let alwaysOnRule = NEOnDemandRuleConnect()
+ alwaysOnRule.interfaceTypeMatch = .any
+ tunnelProvider.onDemandRules = [alwaysOnRule]
+ tunnelProvider.isOnDemandEnabled = true
+ }
}
diff --git a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
index 4ca000562d..bd0501f114 100644
--- a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
@@ -8,16 +8,16 @@
import Foundation
-class StopTunnelOperation: ResultOperation<(), TunnelManager.Error> {
- private let state: TunnelManager.State
+class StopTunnelOperation: ResultOperation<(), Error> {
+ private let interactor: TunnelInteractor
init(
dispatchQueue: DispatchQueue,
- state: TunnelManager.State,
+ interactor: TunnelInteractor,
completionHandler: @escaping CompletionHandler
)
{
- self.state = state
+ self.interactor = interactor
super.init(
dispatchQueue: dispatchQueue,
@@ -27,25 +27,25 @@ class StopTunnelOperation: ResultOperation<(), TunnelManager.Error> {
}
override func main() {
- guard let tunnel = state.tunnel else {
- finish(completion: .failure(.unsetTunnel))
- return
- }
-
- switch state.tunnelStatus.state {
+ switch interactor.tunnelStatus.state {
case .disconnecting(.reconnect):
- state.tunnelStatus.state = .disconnecting(.nothing)
+ interactor.updateTunnelState(.disconnecting(.nothing))
finish(completion: .success(()))
case .connected, .connecting, .reconnecting:
+ guard let tunnel = interactor.tunnel else {
+ finish(completion: .failure(UnsetTunnelError()))
+ return
+ }
+
// Disable on-demand when stopping the tunnel to prevent it from coming back up
tunnel.isOnDemandEnabled = false
tunnel.saveToPreferences { error in
self.dispatchQueue.async {
if let error = error {
- self.finish(completion: .failure(.saveVPNConfiguration(error)))
+ self.finish(completion: .failure(error))
} else {
tunnel.stop()
self.finish(completion: .success(()))
diff --git a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift
new file mode 100644
index 0000000000..b3d2b7b495
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift
@@ -0,0 +1,45 @@
+//
+// TunnelInteractor.swift
+// MullvadVPN
+//
+// Created by pronebird on 05/07/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+protocol TunnelInteractor {
+
+ // MARK: - Tunnel manipulation
+
+ var tunnel: Tunnel? { get }
+ func setTunnel(_ tunnel: Tunnel?, shouldRefreshTunnelState: Bool)
+
+ // MARK: - Tunnel status manipulation
+
+ var tunnelStatus: TunnelStatus { get }
+
+ func setTunnelStatus(_ tunnelStatus: TunnelStatus)
+ func updateTunnelStatus(
+ from packetTunnelStatus: PacketTunnelStatus,
+ mappingRelayToState mapper: (PacketTunnelRelay?) -> TunnelState?
+ )
+
+ // MARK: - Tunnel state
+
+ func updateTunnelState(_ state: TunnelState)
+ func resetTunnelState(to state: TunnelState)
+
+ // MARK: - Configuration
+
+ var isConfigurationLoaded: Bool { get }
+ var settings: TunnelSettingsV2 { get }
+ var deviceState: DeviceState { get }
+
+ func setConfigurationLoaded()
+ func setSettings(_ settings: TunnelSettingsV2, persist: Bool)
+ func setDeviceState(_ deviceState: DeviceState, persist: Bool)
+
+ func startTunnel()
+ func prepareForVPNConfigurationDeletion()
+}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
index 6b82105427..0b1236f743 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
@@ -38,12 +38,16 @@ enum TunnelManagerConfiguration {
/// A class that provides a convenient interface for VPN tunnels configuration, manipulation and
/// monitoring.
-final class TunnelManager: TunnelManagerStateDelegate {
- /// Operation categories
- private enum OperationCategory {
- static let manageTunnelProvider = "TunnelManager.manageTunnelProvider"
- static let changeTunnelSettings = "TunnelManager.changeTunnelSettings"
- static let tunnelStateUpdate = "TunnelManager.tunnelStateUpdate"
+final class TunnelManager {
+ private enum OperationCategory: String {
+ case manageTunnel
+ case deviceStateUpdate
+ case settingsUpdate
+ case tunnelStateUpdate
+
+ var category: String {
+ return "TunnelManager.\(rawValue)"
+ }
}
static let shared: TunnelManager = {
@@ -59,19 +63,18 @@ final class TunnelManager: TunnelManagerStateDelegate {
private let devicesProxy: REST.DevicesProxy
private let logger = Logger(label: "TunnelManager")
- private let stateQueue = DispatchQueue(label: "TunnelManager.stateQueue")
+ private var nslock = NSRecursiveLock()
private let operationQueue = AsyncOperationQueue()
+ private let internalQueue = DispatchQueue(label: "TunnelManager.internalQueue")
private var statusObserver: TunnelStatusBlockObserver?
private var lastMapConnectionStatusOperation: Operation?
private let observerList = ObserverList<TunnelObserver>()
- private let state: TunnelManager.State
-
private var privateKeyRotationTimer: DispatchSourceTimer?
private var lastKeyRotationData: (
attempt: Date,
- completion: OperationCompletion<Bool, TunnelManager.Error>
+ completion: OperationCompletion<Bool, Error>
)?
private var isRunningPeriodicPrivateKeyRotation = false
@@ -79,116 +82,71 @@ final class TunnelManager: TunnelManagerStateDelegate {
private var isPolling = false
private var lastConnectingDate: Date?
- var isLoadedConfiguration: Bool {
- return state.isLoadedConfiguration
- }
-
- 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
- }
+ private var _isConfigurationLoaded = false
+ private var _deviceState: DeviceState = .loggedOut
+ private var _tunnelSettings = TunnelSettingsV2()
- var tunnelSettings: TunnelSettingsV2? {
- return state.tunnelSettings
- }
+ private var _tunnel: Tunnel?
+ private var _tunnelStatus = TunnelStatus()
- var tunnelState: TunnelState {
- return state.tunnelStatus.state
- }
+ // MARK: - Initialization
private init(accountsProxy: REST.AccountsProxy, devicesProxy: REST.DevicesProxy) {
self.accountsProxy = accountsProxy
self.devicesProxy = devicesProxy
- self.state = TunnelManager.State(delegateQueue: stateQueue)
- self.state.delegate = self
self.operationQueue.name = "TunnelManager.operationQueue"
- self.operationQueue.underlyingQueue = stateQueue
+ self.operationQueue.underlyingQueue = internalQueue
}
// MARK: - Periodic private key rotation
func startPeriodicPrivateKeyRotation() {
- stateQueue.async {
- guard !self.isRunningPeriodicPrivateKeyRotation else { return }
-
- self.logger.debug("Start periodic private key rotation.")
+ nslock.lock()
+ defer { nslock.unlock() }
- self.isRunningPeriodicPrivateKeyRotation = true
- self.updatePrivateKeyRotationTimer()
- }
- }
-
- func stopPeriodicPrivateKeyRotation() {
- stateQueue.async {
- guard self.isRunningPeriodicPrivateKeyRotation else { return }
+ guard !isRunningPeriodicPrivateKeyRotation else { return }
- self.logger.debug("Stop periodic private key rotation.")
+ logger.debug("Start periodic private key rotation.")
- self.isRunningPeriodicPrivateKeyRotation = false
- self.updatePrivateKeyRotationTimer()
- }
- }
+ isRunningPeriodicPrivateKeyRotation = true
+ updatePrivateKeyRotationTimer()
- func getNextKeyRotationDate() -> Date? {
- return stateQueue.sync {
- return _getNextKeyRotationDate()
- }
+ nslock.unlock()
}
- private func updatePrivateKeyRotationTimer() {
- dispatchPrecondition(condition: .onQueue(stateQueue))
-
- privateKeyRotationTimer?.cancel()
- privateKeyRotationTimer = nil
-
- guard self.isRunningPeriodicPrivateKeyRotation else { return }
-
- guard let scheduleDate = _getNextKeyRotationDate() else { return }
-
- let timer = DispatchSource.makeTimerSource(queue: stateQueue)
-
- timer.setEventHandler { [weak self] in
- guard let self = self else { return }
-
- _ = self.rotatePrivateKey(forceRotate: false) { _ in
- // no-op
- }
- }
+ func stopPeriodicPrivateKeyRotation() {
+ nslock.lock()
+ defer { nslock.unlock() }
- timer.schedule(wallDeadline: .now() + scheduleDate.timeIntervalSinceNow)
- timer.activate()
+ guard isRunningPeriodicPrivateKeyRotation else { return }
- privateKeyRotationTimer = timer
+ logger.debug("Stop periodic private key rotation.")
- logger.debug("Schedule next private key rotation at \(scheduleDate.logFormatDate()).")
+ isRunningPeriodicPrivateKeyRotation = false
+ updatePrivateKeyRotationTimer()
}
- private func _getNextKeyRotationDate() -> Date? {
- guard let tunnelSettings = state.tunnelSettings else {
+ func getNextKeyRotationDate() -> Date? {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ guard case .loggedIn(_, let deviceData) = deviceState else {
return nil
}
if case .some(let (lastAttemptDate, completion)) = lastKeyRotationData {
- // Do not rotate the key when logged out.
- if case .unsetAccount = completion.error {
+ if completion.error is InvalidDeviceStateError {
+ return nil
+ }
+
+ if completion.error is RevokedDeviceError {
return nil
}
// Do not rotate the key if account or device is not found.
- if case .rotateKey(.unhandledResponse(_, let serverErrorResponse)) = completion.error,
- serverErrorResponse?.code == .invalidAccount ||
- serverErrorResponse?.code == .deviceNotFound {
+ if let restError = completion.error as? REST.Error,
+ restError.compareErrorCode(.invalidAccount) ||
+ restError.compareErrorCode(.deviceNotFound) {
return nil
}
@@ -203,21 +161,49 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
// Rotate at long intervals otherwise.
- let date = tunnelSettings.device.wgKeyData.creationDate
+ let date = deviceData.wgKeyData.creationDate
.addingTimeInterval(TunnelManagerConfiguration.privateKeyRotationInterval)
return max(date, Date())
}
- private func setFinishedKeyRotation(_ completion: OperationCompletion<Bool, TunnelManager.Error>) {
- dispatchPrecondition(condition: .onQueue(stateQueue))
+ private func updatePrivateKeyRotationTimer() {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ privateKeyRotationTimer?.cancel()
+ privateKeyRotationTimer = nil
+
+ guard isRunningPeriodicPrivateKeyRotation,
+ let scheduleDate = getNextKeyRotationDate() else { return }
+
+ let timer = DispatchSource.makeTimerSource(queue: .main)
+
+ timer.setEventHandler { [weak self] in
+ _ = self?.rotatePrivateKey(forceRotate: false) { _ in
+ // no-op
+ }
+ }
+
+ timer.schedule(wallDeadline: .now() + scheduleDate.timeIntervalSinceNow)
+ timer.activate()
+
+ privateKeyRotationTimer = timer
+
+ logger.debug("Schedule next private key rotation at \(scheduleDate.logFormatDate()).")
+ }
+
+ private func setFinishedKeyRotation(_ completion: OperationCompletion<Bool, Error>) {
+ nslock.lock()
+ defer { nslock.unlock() }
lastKeyRotationData = (Date(), completion)
updatePrivateKeyRotationTimer()
}
private func resetKeyRotationData() {
- dispatchPrecondition(condition: .onQueue(stateQueue))
+ nslock.lock()
+ defer { nslock.unlock() }
lastKeyRotationData = nil
updatePrivateKeyRotationTimer()
@@ -226,32 +212,31 @@ final class TunnelManager: TunnelManagerStateDelegate {
// MARK: - Public methods
- func loadConfiguration(completionHandler: @escaping (TunnelManager.Error?) -> Void) {
+ func loadConfiguration(completionHandler: @escaping (Error?) -> Void) {
let migrateSettingsOperation = MigrateSettingsOperation(
- dispatchQueue: stateQueue,
+ dispatchQueue: internalQueue,
accountsProxy: accountsProxy,
devicesProxy: devicesProxy
)
let loadTunnelOperation = LoadTunnelConfigurationOperation(
- dispatchQueue: stateQueue,
- state: state
+ dispatchQueue: internalQueue,
+ interactor: TunnelInteractorProxy(self)
)
- loadTunnelOperation.completionQueue = stateQueue
+ loadTunnelOperation.completionQueue = .main
loadTunnelOperation.completionHandler = { [weak self] completion in
guard let self = self else { return }
- dispatchPrecondition(condition: .onQueue(self.stateQueue))
-
if case .failure(let error) = completion {
- self.logger.error(chainedError: error, message: "Failed to load tunnel.")
+ self.logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to load configuration."
+ )
}
self.updatePrivateKeyRotationTimer()
- DispatchQueue.main.async {
- completionHandler(completion.error)
- }
+ completionHandler(completion.error)
}
loadTunnelOperation.addDependency(migrateSettingsOperation)
@@ -264,156 +249,131 @@ final class TunnelManager: TunnelManagerStateDelegate {
)
groupOperation.addCondition(
- MutuallyExclusive(category: OperationCategory.manageTunnelProvider)
+ MutuallyExclusive(category: OperationCategory.manageTunnel.category)
+ )
+ groupOperation.addCondition(
+ MutuallyExclusive(category: OperationCategory.deviceStateUpdate.category)
)
groupOperation.addCondition(
- MutuallyExclusive(category: OperationCategory.changeTunnelSettings)
+ MutuallyExclusive(category: OperationCategory.settingsUpdate.category)
)
operationQueue.addOperation(groupOperation)
}
func refreshTunnelStatus() {
- stateQueue.async {
- self.logger.debug("Refresh tunnel status due to application becoming active.")
- self._refreshTunnelStatus()
- }
+ logger.debug("Refresh tunnel status due to application becoming active.")
+ _refreshTunnelStatus()
}
func startTunnel() {
let operation = StartTunnelOperation(
- dispatchQueue: stateQueue,
- state: state,
- encodeErrorHandler: { [weak self] error in
- guard let self = self else { return }
-
- dispatchPrecondition(condition: .onQueue(self.stateQueue))
-
- self.logger.error(chainedError: AnyChainedError(error), message: "Failed to encode tunnel options")
- },
+ dispatchQueue: internalQueue,
+ interactor: TunnelInteractorProxy(self),
completionHandler: { [weak self] completion in
- guard let self = self else { return }
+ guard let self = self, let error = completion.error else { return }
- dispatchPrecondition(condition: .onQueue(self.stateQueue))
+ self.logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to start the tunnel."
+ )
+
+ DispatchQueue.main.async {
+ let tunnelError = StartTunnelError(underlyingError: error)
- if case .failure(let error) = completion {
- self.logger.error(chainedError: error, message: "Failed to start the tunnel.")
+ self.observerList.forEach { observer in
+ observer.tunnelManager(self, didFailWithError: tunnelError)
+ }
}
})
operation.addObserver(BackgroundObserver(name: "Start tunnel", cancelUponExpiration: true))
- operation.addCondition(MutuallyExclusive(category: OperationCategory.manageTunnelProvider))
+ operation.addCondition(MutuallyExclusive(category: OperationCategory.manageTunnel.category))
operationQueue.addOperation(operation)
}
func stopTunnel() {
let operation = StopTunnelOperation(
- dispatchQueue: stateQueue,
- state: state
+ dispatchQueue: internalQueue,
+ interactor: TunnelInteractorProxy(self)
) { [weak self] completion in
guard let self = self, let error = completion.error else { return }
- // Pass tunnel failure to observers
+ self.logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to stop the tunnel."
+ )
+
DispatchQueue.main.async {
+ let tunnelError = StopTunnelError(underlyingError: error)
+
self.observerList.forEach { observer in
- observer.tunnelManager(self, didFailWithError: error)
+ observer.tunnelManager(self, didFailWithError: tunnelError)
}
}
}
operation.addObserver(BackgroundObserver(name: "Stop tunnel", cancelUponExpiration: true))
- operation.addCondition(MutuallyExclusive(category: OperationCategory.manageTunnelProvider))
+ operation.addCondition(MutuallyExclusive(category: OperationCategory.manageTunnel.category))
operationQueue.addOperation(operation)
}
func reconnectTunnel(
selectNewRelay: Bool,
- completionHandler: ((OperationCompletion<(), TunnelManager.Error>) -> Void)? = nil
+ completionHandler: ((OperationCompletion<(), Error>) -> Void)? = nil
)
{
let operation = ReconnectTunnelOperation(
- dispatchQueue: stateQueue,
- state: state,
+ dispatchQueue: internalQueue,
+ interactor: TunnelInteractorProxy(self),
selectNewRelay: selectNewRelay
)
+ operation.completionQueue = .main
operation.completionHandler = { [weak self] completion in
- guard let self = self else { return }
-
- dispatchPrecondition(condition: .onQueue(self.stateQueue))
-
- if let error = completion.error {
- self.logger.error(chainedError: error, message: "Failed to reconnect the tunnel.")
- }
-
- // Refresh tunnel status only when connecting or reasserting to pick up the next relay,
- // since both states may persist for a long period of time until the tunnel is fully
- // connected.
- switch self.tunnelState {
- case .connecting, .reconnecting:
- self.logger.debug("Refresh tunnel status due to reconnect.")
- self._refreshTunnelStatus()
-
- default:
- break
- }
+ self?.didReconnectTunnel(completion: completion)
- DispatchQueue.main.async {
- completionHandler?(completion)
- }
+ completionHandler?(completion)
}
- operation.completionQueue = stateQueue
operation.addObserver(
BackgroundObserver(name: "Reconnect tunnel", cancelUponExpiration: true)
)
operation.addCondition(
- MutuallyExclusive(category: OperationCategory.manageTunnelProvider)
+ MutuallyExclusive(category: OperationCategory.manageTunnel.category)
)
operationQueue.addOperation(operation)
}
- func setAccount(action: SetAccountAction, completionHandler: @escaping (OperationCompletion<StoredAccountData?, TunnelManager.Error>) -> Void) {
+ func setAccount(action: SetAccountAction, completionHandler: @escaping (OperationCompletion<StoredAccountData?, Error>) -> Void) {
let operation = SetAccountOperation(
- dispatchQueue: stateQueue,
- state: state,
+ dispatchQueue: internalQueue,
+ interactor: TunnelInteractorProxy(self),
accountsProxy: accountsProxy,
devicesProxy: devicesProxy,
- action: action,
- 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
- })
+ action: action
+ )
- operation.completionQueue = stateQueue
+ operation.completionQueue = .main
operation.completionHandler = { [weak self] completion in
- guard let self = self else { return }
-
- self.resetKeyRotationData()
+ self?.resetKeyRotationData()
- DispatchQueue.main.async {
- completionHandler(completion)
- }
+ completionHandler(completion)
}
operation.addObserver(BackgroundObserver(name: action.taskName, cancelUponExpiration: true))
operation.addCondition(
- MutuallyExclusive(category: OperationCategory.manageTunnelProvider)
+ MutuallyExclusive(category: OperationCategory.manageTunnel.category)
+ )
+ operation.addCondition(
+ MutuallyExclusive(category: OperationCategory.deviceStateUpdate.category)
)
operation.addCondition(
- MutuallyExclusive(category: OperationCategory.changeTunnelSettings)
+ MutuallyExclusive(category: OperationCategory.settingsUpdate.category)
)
operationQueue.addOperation(operation)
@@ -425,10 +385,10 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
}
- func updateAccountData(_ completionHandler: ((TunnelManager.Error?) -> Void)? = nil) {
+ func updateAccountData(_ completionHandler: ((Error?) -> Void)? = nil) {
let operation = UpdateAccountDataOperation(
- dispatchQueue: stateQueue,
- state: state,
+ dispatchQueue: internalQueue,
+ interactor: TunnelInteractorProxy(self),
accountsProxy: accountsProxy
)
@@ -442,28 +402,33 @@ final class TunnelManager: TunnelManagerStateDelegate {
)
operation.addCondition(
- MutuallyExclusive(category: OperationCategory.changeTunnelSettings)
+ MutuallyExclusive(category: OperationCategory.deviceStateUpdate.category)
)
operationQueue.addOperation(operation)
}
- func updateDeviceData(_ completionHandler: @escaping (OperationCompletion<StoredDeviceData, TunnelManager.Error>) -> Void) -> Cancellable {
+ func updateDeviceData(_ completionHandler: @escaping (OperationCompletion<StoredDeviceData, Error>) -> Void) -> Cancellable {
let operation = UpdateDeviceDataOperation(
- dispatchQueue: stateQueue,
- state: state,
+ dispatchQueue: internalQueue,
+ interactor: TunnelInteractorProxy(self),
devicesProxy: devicesProxy
)
operation.completionQueue = .main
- operation.completionHandler = completionHandler
+ operation.completionHandler = { [weak self] completion in
+ if completion.error is RevokedDeviceError {
+ self?.didDetectDeviceRevoked()
+ }
+ completionHandler(completion)
+ }
operation.addObserver(
BackgroundObserver(name: "Update device data", cancelUponExpiration: true)
)
operation.addCondition(
- MutuallyExclusive(category: OperationCategory.changeTunnelSettings)
+ MutuallyExclusive(category: OperationCategory.deviceStateUpdate.category)
)
operationQueue.addOperation(operation)
@@ -473,7 +438,7 @@ final class TunnelManager: TunnelManagerStateDelegate {
func rotatePrivateKey(
forceRotate: Bool,
- completionHandler: @escaping (OperationCompletion<Bool, TunnelManager.Error>) -> Void
+ completionHandler: @escaping (OperationCompletion<Bool, Error>) -> Void
) -> Cancellable {
var rotationInterval: TimeInterval?
if !forceRotate {
@@ -481,14 +446,16 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
let operation = RotateKeyOperation(
- dispatchQueue: stateQueue,
- state: state,
+ dispatchQueue: internalQueue,
+ interactor: TunnelInteractorProxy(self),
devicesProxy: devicesProxy,
rotationInterval: rotationInterval
- ) { [weak self] completion in
+ )
+
+ operation.completionQueue = .main
+ operation.completionHandler = { [weak self] completion in
guard let self = self else { return }
- dispatchPrecondition(condition: .onQueue(self.stateQueue))
self.setFinishedKeyRotation(completion)
switch completion {
@@ -498,16 +465,15 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to rotate private key.")
+ self.logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to rotate private key."
+ )
- DispatchQueue.main.async {
- completionHandler(completion)
- }
+ completionHandler(completion)
case .cancelled:
- DispatchQueue.main.async {
- completionHandler(completion)
- }
+ completionHandler(completion)
}
}
@@ -516,7 +482,7 @@ final class TunnelManager: TunnelManagerStateDelegate {
)
operation.addCondition(
- MutuallyExclusive(category: OperationCategory.changeTunnelSettings)
+ MutuallyExclusive(category: OperationCategory.deviceStateUpdate.category)
)
operationQueue.addOperation(operation)
@@ -524,21 +490,21 @@ final class TunnelManager: TunnelManagerStateDelegate {
return operation
}
- func setRelayConstraints(_ newConstraints: RelayConstraints, completionHandler: @escaping (TunnelManager.Error?) -> Void) {
- scheduleTunnelSettingsUpdate(
+ func setRelayConstraints(_ newConstraints: RelayConstraints, completionHandler: (() -> Void)? = nil) {
+ scheduleSettingsUpdate(
taskName: "Set relay constraints",
- modificationBlock: { tunnelSettings in
- tunnelSettings.relayConstraints = newConstraints
+ modificationBlock: { settings in
+ settings.relayConstraints = newConstraints
},
completionHandler: completionHandler
)
}
- func setDNSSettings(_ newDNSSettings: DNSSettings, completionHandler: @escaping (TunnelManager.Error?) -> Void) {
- scheduleTunnelSettingsUpdate(
+ func setDNSSettings(_ newDNSSettings: DNSSettings, completionHandler: (() -> Void)? = nil) {
+ scheduleSettingsUpdate(
taskName: "Set DNS settings",
- modificationBlock: { tunnelSettings in
- tunnelSettings.dnsSettings = newDNSSettings
+ modificationBlock: { settings in
+ settings.dnsSettings = newDNSSettings
},
completionHandler: completionHandler
)
@@ -558,46 +524,125 @@ final class TunnelManager: TunnelManagerStateDelegate {
observerList.remove(observer)
}
- // MARK: - TunnelManagerStateDelegate
+ // MARK: - TunnelInteractor
+
+ var isConfigurationLoaded: Bool {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ return _isConfigurationLoaded
+ }
+
+ fileprivate var tunnel: Tunnel? {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ return _tunnel
+ }
+
+ var tunnelStatus: TunnelStatus {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ return _tunnelStatus
+ }
+
+ var settings: TunnelSettingsV2 {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ return _tunnelSettings
+ }
+
+ var deviceState: DeviceState {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ return _deviceState
+ }
+
+ fileprivate func setConfigurationLoaded() {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ guard !_isConfigurationLoaded else {
+ return
+ }
+
+ _isConfigurationLoaded = true
- func tunnelManagerState(
- _ state: State,
- didChangeLoadedConfiguration isLoadedConfiguration: Bool
- )
- {
DispatchQueue.main.async {
self.observerList.forEach { observer in
- if isLoadedConfiguration {
- observer.tunnelManagerDidLoadConfiguration(self)
- }
+ observer.tunnelManagerDidLoadConfiguration(self)
}
}
}
- func tunnelManagerState(
- _ state: TunnelManager.State,
- didChangeTunnelSettings newTunnelSettings: TunnelSettingsV2?
- )
- {
- DispatchQueue.main.async {
- self.observerList.forEach { observer in
- observer.tunnelManager(self, didUpdateTunnelSettings: newTunnelSettings)
- }
+ fileprivate func setTunnel(_ tunnel: Tunnel?, shouldRefreshTunnelState: Bool) {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ if let tunnel = tunnel {
+ subscribeVPNStatusObserver(tunnel: tunnel)
+ } else {
+ unsubscribeVPNStatusObserver()
+ }
+
+ _tunnel = tunnel
+
+ // Update the existing state
+ if shouldRefreshTunnelState {
+ logger.debug("Refresh tunnel status for new tunnel.")
+ _refreshTunnelStatus()
}
}
- func tunnelManagerState(
- _ state: TunnelManager.State,
- didChangeTunnelStatus newTunnelStatus: TunnelStatus
+ fileprivate func updateTunnelState(_ state: TunnelState) {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ var updatedStatus = _tunnelStatus
+ updatedStatus.state = state
+ setTunnelStatus(updatedStatus)
+ }
+
+ fileprivate func updateTunnelStatus(
+ from packetTunnelStatus: PacketTunnelStatus,
+ mappingRelayToState mapper: (PacketTunnelRelay?) -> TunnelState?
)
{
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ var updatedStatus = _tunnelStatus
+ updatedStatus.update(from: packetTunnelStatus, mappingRelayToState: mapper)
+ setTunnelStatus(updatedStatus)
+ }
+
+ fileprivate func resetTunnelStatus(to state: TunnelState) {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ var updatedStatus = _tunnelStatus
+ updatedStatus.reset(to: state)
+ setTunnelStatus(updatedStatus)
+ }
+
+ fileprivate func setTunnelStatus(_ newTunnelStatus: TunnelStatus) {
+ nslock.lock()
+ defer { nslock.unlock() }
+
logger.info("Status: \(newTunnelStatus).")
+ _tunnelStatus = newTunnelStatus
+
switch newTunnelStatus.state {
case .connecting, .reconnecting:
// Start polling tunnel status to keep the relay information up to date
// while the tunnel process is trying to connect.
- startPollingTunnelStatus(connectingDate: newTunnelStatus.packetTunnelStatus.connectingDate)
+ startPollingTunnelStatus(
+ connectingDate: newTunnelStatus.packetTunnelStatus.connectingDate
+ )
case .pendingReconnect, .connected, .disconnecting, .disconnected:
// Stop polling tunnel status once connection moved to final state.
@@ -611,34 +656,117 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
}
- func tunnelManagerState(
- _ state: TunnelManager.State,
- didChangeTunnelProvider newTunnelObject: Tunnel?,
- shouldRefreshTunnelState: Bool
- )
- {
- dispatchPrecondition(condition: .onQueue(stateQueue))
+ fileprivate func setSettings(_ settings: TunnelSettingsV2, persist: Bool) {
+ nslock.lock()
+ defer { nslock.unlock() }
- // Register for tunnel connection status changes
- if let newTunnelObject = newTunnelObject {
- subscribeVPNStatusObserver(tunnel: newTunnelObject)
- } else {
- unsubscribeVPNStatusObserver()
+ let shouldCallDelegate = _tunnelSettings != settings && _isConfigurationLoaded
+
+ _tunnelSettings = settings
+
+ if persist {
+ do {
+ try SettingsManager.writeSettings(settings)
+ } catch {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to write settings."
+ )
+ }
}
- // Update the existing state
- if shouldRefreshTunnelState {
- logger.debug("Refresh tunnel status for new tunnel.")
- _refreshTunnelStatus()
+ if shouldCallDelegate {
+ DispatchQueue.main.async {
+ self.observerList.forEach { observer in
+ observer.tunnelManager(self, didUpdateTunnelSettings: settings)
+ }
+ }
+ }
+ }
+
+ fileprivate func setDeviceState(_ deviceState: DeviceState, persist: Bool) {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ let shouldCallDelegate = _deviceState != deviceState && _isConfigurationLoaded
+
+ _deviceState = deviceState
+
+ if persist {
+ do {
+ try SettingsManager.writeDeviceState(deviceState)
+ } catch {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to write device state."
+ )
+ }
+ }
+
+ if shouldCallDelegate {
+ DispatchQueue.main.async {
+ self.observerList.forEach { observer in
+ observer.tunnelManager(self, didUpdateDeviceState: deviceState)
+ }
+ }
}
}
// MARK: - Private methods
+ fileprivate func prepareForVPNConfigurationDeletion() {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ // Unregister from receiving VPN connection status changes
+ unsubscribeVPNStatusObserver()
+
+ // Cancel last VPN status mapping operation
+ lastMapConnectionStatusOperation?.cancel()
+ lastMapConnectionStatusOperation = nil
+ }
+
+ private func didDetectDeviceRevoked() {
+ scheduleDeviceStateUpdate(
+ taskName: "Set device revoked",
+ modificationBlock: { deviceState in
+ deviceState = .revoked
+ },
+ completionHandler: nil
+ )
+ }
+
+ private func didReconnectTunnel(completion: OperationCompletion<(), Error>) {
+ nslock.lock()
+ defer { nslock.unlock() }
+
+ if let error = completion.error {
+ logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to reconnect the tunnel."
+ )
+ }
+
+ // Refresh tunnel status only when connecting or reasserting to pick up the next relay,
+ // since both states may persist for a long period of time until the tunnel is fully
+ // connected.
+ switch tunnelStatus.state {
+ case .connecting, .reconnecting:
+ logger.debug("Refresh tunnel status due to reconnect.")
+ _refreshTunnelStatus()
+
+ default:
+ break
+ }
+ }
+
private func subscribeVPNStatusObserver(tunnel: Tunnel) {
+ nslock.lock()
+ defer { nslock.unlock() }
+
unsubscribeVPNStatusObserver()
- statusObserver = tunnel.addBlockObserver(queue: stateQueue) { [weak self] tunnel, status in
+ statusObserver = tunnel.addBlockObserver(queue: internalQueue) { [weak self] tunnel, status in
guard let self = self else { return }
self.logger.debug("VPN connection status changed to \(status).")
@@ -647,14 +775,18 @@ final class TunnelManager: TunnelManagerStateDelegate {
}
private func unsubscribeVPNStatusObserver() {
+ nslock.lock()
+ defer { nslock.unlock() }
+
statusObserver?.invalidate()
statusObserver = nil
}
private func _refreshTunnelStatus() {
- dispatchPrecondition(condition: .onQueue(stateQueue))
+ nslock.lock()
+ defer { nslock.unlock() }
- if let connectionStatus = state.tunnel?.status {
+ if let connectionStatus = _tunnel?.status {
updateTunnelStatus(connectionStatus)
}
}
@@ -663,21 +795,18 @@ final class TunnelManager: TunnelManagerStateDelegate {
/// Collects the `PacketTunnelStatus` from the tunnel via IPC if needed before assigning
/// the `tunnelStatus`.
private func updateTunnelStatus(_ connectionStatus: NEVPNStatus) {
- dispatchPrecondition(condition: .onQueue(stateQueue))
+ nslock.lock()
+ defer { nslock.unlock() }
let operation = MapConnectionStatusOperation(
- queue: stateQueue,
- state: state,
+ queue: internalQueue,
+ interactor: TunnelInteractorProxy(self),
connectionStatus: connectionStatus
- ) { [weak self] in
- guard let self = self else { return }
-
- dispatchPrecondition(condition: .onQueue(self.stateQueue))
-
- self.startTunnel()
- }
+ )
- operation.addCondition(MutuallyExclusive(category: OperationCategory.tunnelStateUpdate))
+ operation.addCondition(
+ MutuallyExclusive(category: OperationCategory.tunnelStateUpdate.category)
+ )
// Cancel last VPN status mapping operation
lastMapConnectionStatusOperation?.cancel()
@@ -686,53 +815,71 @@ final class TunnelManager: TunnelManagerStateDelegate {
operationQueue.addOperation(operation)
}
- 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 let currentSettings = self.tunnelSettings else {
- operation.finish(completion: .failure(.unsetAccount))
- return
- }
+ private func scheduleSettingsUpdate(
+ taskName: String,
+ modificationBlock: @escaping (inout TunnelSettingsV2) -> Void,
+ completionHandler: (() -> Void)?
+ )
+ {
+ let operation = AsyncBlockOperation(dispatchQueue: internalQueue) {
+ let currentSettings = self._tunnelSettings
+ var updatedSettings = self._tunnelSettings
- do {
- var updatedSettings = currentSettings
+ modificationBlock(&updatedSettings)
- modificationBlock(&updatedSettings)
+ // Select new relay only when relay constraints change.
+ let currentConstraints = currentSettings.relayConstraints
+ let updatedConstraints = updatedSettings.relayConstraints
+ let selectNewRelay = currentConstraints != updatedConstraints
- // Select new relay only when relay constraints change.
- let currentConstraints = currentSettings.relayConstraints
- let updatedConstraints = updatedSettings.relayConstraints
- let selectNewRelay = currentConstraints != updatedConstraints
+ self.setSettings(updatedSettings, persist: true)
+ self.reconnectTunnel(selectNewRelay: selectNewRelay, completionHandler: nil)
+ }
- try SettingsManager.writeSettings(updatedSettings)
+ operation.completionBlock = {
+ DispatchQueue.main.async {
+ completionHandler?()
+ }
+ }
- self.state.tunnelSettings = updatedSettings
- self.reconnectTunnel(selectNewRelay: selectNewRelay, completionHandler: nil)
+ operation.addObserver(BackgroundObserver(name: taskName, cancelUponExpiration: false))
+ operation.addCondition(
+ MutuallyExclusive(category: OperationCategory.settingsUpdate.category)
+ )
- operation.finish(completion: .success(()))
- } catch {
- self.logger.error(
- chainedError: AnyChainedError(error),
- message: "Failed to write settings."
- )
+ operationQueue.addOperation(operation)
+ }
- operation.finish(completion: .failure(.writeSettings(error)))
- }
+ private func scheduleDeviceStateUpdate(
+ taskName: String,
+ modificationBlock: @escaping (inout DeviceState) -> Void,
+ completionHandler: (() -> Void)?
+ )
+ {
+ let operation = AsyncBlockOperation(dispatchQueue: internalQueue) {
+ var deviceState = self.deviceState
+
+ modificationBlock(&deviceState)
+
+ self.setDeviceState(deviceState, persist: true)
+ self.reconnectTunnel(selectNewRelay: false, completionHandler: nil)
}
- operation.completionQueue = .main
- operation.completionHandler = { completion in
- completionHandler(completion.error)
+ operation.completionBlock = {
+ DispatchQueue.main.async {
+ completionHandler?()
+ }
}
- operation.addObserver(BackgroundObserver(name: taskName, cancelUponExpiration: true))
- operation.addCondition(MutuallyExclusive(category: OperationCategory.changeTunnelSettings))
+ operation.addObserver(BackgroundObserver(name: taskName, cancelUponExpiration: false))
+ operation.addCondition(
+ MutuallyExclusive(category: OperationCategory.deviceStateUpdate.category)
+ )
operationQueue.addOperation(operation)
}
- // MARK: - Tunnel status polling.
+ // MARK: - Tunnel status polling
private func computeNextPollDateAndRepeatInterval(connectingDate: Date?) -> (Date, TimeInterval) {
let delay, repeating: TimeInterval
@@ -775,7 +922,7 @@ final class TunnelManager: TunnelManagerStateDelegate {
let (fireDate, repeating) = computeNextPollDateAndRepeatInterval(connectingDate: connectingDate)
logger.debug("Start polling tunnel status at \(fireDate.logFormatDate()) every \(repeating) second(s).")
- let timer = DispatchSource.makeTimerSource(queue: stateQueue)
+ let timer = DispatchSource.makeTimerSource(queue: .main)
timer.setEventHandler { [weak self] in
guard let self = self else { return }
@@ -810,37 +957,112 @@ final class TunnelManager: TunnelManagerStateDelegate {
// MARK: - AppStore payment observer
extension TunnelManager: AppStorePaymentObserver {
- func appStorePaymentManager(_ manager: AppStorePaymentManager,
- transaction: SKPaymentTransaction?,
- payment: SKPayment,
- accountToken: String?,
- didFailWithError error: AppStorePaymentManager.Error
+ 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
+ func appStorePaymentManager(
+ _ manager: AppStorePaymentManager,
+ transaction: SKPaymentTransaction,
+ accountToken: String,
+ didFinishWithResponse response: REST.CreateApplePaymentResponse
)
{
- scheduleTunnelSettingsUpdate(
+ scheduleDeviceStateUpdate(
taskName: "Update account expiry after in-app purchase",
- modificationBlock: { tunnelSettings in
- if tunnelSettings.account.number == accountToken {
- tunnelSettings.account.expiry = response.newExpiry
+ modificationBlock: { deviceState in
+ switch deviceState {
+ case .loggedIn(var accountData, let deviceData):
+ if accountData.number == accountToken {
+ accountData.expiry = response.newExpiry
+ deviceState = .loggedIn(accountData, deviceData)
+ }
+
+ case .loggedOut, .revoked:
+ break
}
},
- completionHandler: { error in
- guard let error = error else { return }
-
- self.logger.error(
- chainedError: error,
- message: "Failed to update account expiry after in-app purchase"
- )
- }
+ completionHandler: nil
)
}
}
+
+private struct TunnelInteractorProxy: TunnelInteractor {
+ private let tunnelManager: TunnelManager
+
+ init(_ tunnelManager: TunnelManager) {
+ self.tunnelManager = tunnelManager
+ }
+
+ var tunnel: Tunnel? {
+ return tunnelManager.tunnel
+ }
+
+ func setTunnel(_ tunnel: Tunnel?, shouldRefreshTunnelState: Bool) {
+ tunnelManager.setTunnel(tunnel, shouldRefreshTunnelState: shouldRefreshTunnelState)
+ }
+
+ var tunnelStatus: TunnelStatus {
+ return tunnelManager.tunnelStatus
+ }
+
+ func setTunnelStatus(_ tunnelStatus: TunnelStatus) {
+ tunnelManager.setTunnelStatus(tunnelStatus)
+ }
+
+ func updateTunnelStatus(
+ from packetTunnelStatus: PacketTunnelStatus,
+ mappingRelayToState mapper: (PacketTunnelRelay?) -> TunnelState?
+ )
+ {
+ tunnelManager.updateTunnelStatus(from: packetTunnelStatus, mappingRelayToState: mapper)
+ }
+
+ func updateTunnelState(_ state: TunnelState) {
+ tunnelManager.updateTunnelState(state)
+ }
+
+ func resetTunnelState(to state: TunnelState) {
+ tunnelManager.resetTunnelStatus(to: state)
+ }
+
+ var isConfigurationLoaded: Bool {
+ return tunnelManager.isConfigurationLoaded
+ }
+
+ var settings: TunnelSettingsV2 {
+ return tunnelManager.settings
+ }
+
+ var deviceState: DeviceState {
+ return tunnelManager.deviceState
+ }
+
+ func setConfigurationLoaded() {
+ tunnelManager.setConfigurationLoaded()
+ }
+
+ func setSettings(_ settings: TunnelSettingsV2, persist: Bool) {
+ tunnelManager.setSettings(settings, persist: persist)
+ }
+
+ func setDeviceState(_ deviceState: DeviceState, persist: Bool) {
+ tunnelManager.setDeviceState(deviceState, persist: persist)
+ }
+
+ func startTunnel() {
+ tunnelManager.startTunnel()
+ }
+
+ func prepareForVPNConfigurationDeletion() {
+ tunnelManager.prepareForVPNConfigurationDeletion()
+ }
+
+}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManagerErrors.swift b/ios/MullvadVPN/TunnelManager/TunnelManagerErrors.swift
new file mode 100644
index 0000000000..77c09cdc9c
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/TunnelManagerErrors.swift
@@ -0,0 +1,75 @@
+//
+// TunnelManagerErrors.swift
+// MullvadVPN
+//
+// Created by pronebird on 07/09/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import NetworkExtension
+
+struct UnsetTunnelError: LocalizedError {
+ var errorDescription: String? {
+ return NSLocalizedString(
+ "UNSET_TUNNEL_ERROR",
+ tableName: "TunnelManager",
+ value: "Tunnel is unset.",
+ comment: ""
+ )
+ }
+}
+
+struct InvalidDeviceStateError: LocalizedError {
+ var errorDescription: String? {
+ return NSLocalizedString(
+ "INVALID_DEVICE_STATE_ERROR",
+ tableName: "TunnelManager",
+ value: "Invalid device state.",
+ comment: ""
+ )
+ }
+}
+
+struct RevokedDeviceError: LocalizedError {
+ var errorDescription: String? {
+ return NSLocalizedString(
+ "REVOKED_DEVICE_ERROR",
+ tableName: "TunnelManager",
+ value: "Device is revoked.",
+ comment: ""
+ )
+ }
+}
+
+struct StartTunnelError: LocalizedError {
+ var errorDescription: String? {
+ return NSLocalizedString(
+ "START_TUNNEL_ERROR",
+ tableName: "TunnelManager",
+ value: "Failed to start the tunnel.",
+ comment: ""
+ )
+ }
+
+ let underlyingError: Error
+ init(underlyingError: Error) {
+ self.underlyingError = underlyingError
+ }
+}
+
+struct StopTunnelError: LocalizedError {
+ var errorDescription: String? {
+ return NSLocalizedString(
+ "STOP_TUNNEL_ERROR",
+ tableName: "TunnelManager",
+ value: "Failed to stop the tunnel.",
+ comment: ""
+ )
+ }
+
+ let underlyingError: Error
+ init(underlyingError: Error) {
+ self.underlyingError = underlyingError
+ }
+}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelObserver.swift b/ios/MullvadVPN/TunnelManager/TunnelObserver.swift
index 93ab9d8849..7a3c4f910f 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelObserver.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelObserver.swift
@@ -1,6 +1,6 @@
//
// TunnelObserver.swift
-// TunnelObserver
+// MullvadVPN
//
// Created by pronebird on 19/08/2021.
// Copyright © 2021 Mullvad VPN AB. All rights reserved.
@@ -11,6 +11,12 @@ import Foundation
protocol TunnelObserver: AnyObject {
func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager)
func tunnelManager(_ manager: TunnelManager, didUpdateTunnelState tunnelState: TunnelState)
- func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?)
- func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error)
+ func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState)
+
+ func tunnelManager(
+ _ manager: TunnelManager,
+ didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2
+ )
+
+ func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error)
}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelState.swift b/ios/MullvadVPN/TunnelManager/TunnelState.swift
index ee65346f82..bfbc0da3e9 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelState.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelState.swift
@@ -91,6 +91,15 @@ enum TunnelState: Equatable, CustomStringConvertible {
return "reconnecting to \(tunnelRelay.hostname)"
}
}
+
+ var isSecured: Bool {
+ switch self {
+ case .reconnecting, .connecting, .connected:
+ return true
+ case .pendingReconnect, .disconnecting, .disconnected:
+ return false
+ }
+ }
}
/// A enum that describes the action to perform after disconnect
diff --git a/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift b/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift
index ae0f0dea7a..25669631c5 100644
--- a/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift
@@ -9,39 +9,36 @@
import Foundation
import Logging
-class UpdateAccountDataOperation: ResultOperation<Void, TunnelManager.Error> {
+class UpdateAccountDataOperation: ResultOperation<Void, Error> {
private let logger = Logger(label: "UpdateAccountDataOperation")
- private let state: TunnelManager.State
+ private let interactor: TunnelInteractor
private let accountsProxy: REST.AccountsProxy
private var task: Cancellable?
init(
dispatchQueue: DispatchQueue,
- state: TunnelManager.State,
+ interactor: TunnelInteractor,
accountsProxy: REST.AccountsProxy
)
{
- self.state = state
+ self.interactor = interactor
self.accountsProxy = accountsProxy
super.init(dispatchQueue: dispatchQueue)
}
override func main() {
- guard let tunnelSettings = state.tunnelSettings else {
- finish(completion: .failure(.unsetAccount))
+ guard case .loggedIn(let accountData, _) = interactor.deviceState else {
+ finish(completion: .failure(InvalidDeviceStateError()))
return
}
task = accountsProxy.getAccountData(
- accountNumber: tunnelSettings.account.number,
+ accountNumber: accountData.number,
retryStrategy: .default
) { completion in
self.dispatchQueue.async {
- self.didReceiveAccountData(
- tunnelSettings: tunnelSettings,
- completion: completion
- )
+ self.didReceiveAccountData(completion: completion)
}
}
}
@@ -52,36 +49,28 @@ class UpdateAccountDataOperation: ResultOperation<Void, TunnelManager.Error> {
}
private func didReceiveAccountData(
- tunnelSettings: TunnelSettingsV2,
completion: OperationCompletion<REST.AccountData, REST.Error>
- )
- {
- let mappedCompletion = completion.mapError { error -> TunnelManager.Error in
+ ) {
+ let mappedCompletion = completion.mapError { error -> 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
- }
+ return error
+ }.tryMap { accountData in
+ switch interactor.deviceState {
+ case .loggedIn(var storedAccountData, let storedDeviceData):
+ storedAccountData.expiry = accountData.expiry
- do {
- var newTunnelSettings = tunnelSettings
- newTunnelSettings.account.expiry = accountData.expiry
- try SettingsManager.writeSettings(newTunnelSettings)
+ let newDeviceState = DeviceState.loggedIn(storedAccountData, storedDeviceData)
- finish(completion: .success(()))
- } catch {
- self.logger.error(
- chainedError: AnyChainedError(error),
- message: "Failed to save account data."
- )
+ interactor.setDeviceState(newDeviceState, persist: true)
- finish(completion: .failure(.writeSettings(error)))
+ default:
+ throw InvalidDeviceStateError()
+ }
}
+
+ finish(completion: mappedCompletion)
}
}
diff --git a/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift b/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift
index 6c0fd08823..3c7968371f 100644
--- a/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift
@@ -10,40 +10,37 @@ 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
+class UpdateDeviceDataOperation: ResultOperation<StoredDeviceData, Error> {
+ private let interactor: TunnelInteractor
private let devicesProxy: REST.DevicesProxy
private var task: Cancellable?
init(
dispatchQueue: DispatchQueue,
- state: TunnelManager.State,
+ interactor: TunnelInteractor,
devicesProxy: REST.DevicesProxy
)
{
- self.state = state
+ self.interactor = interactor
self.devicesProxy = devicesProxy
super.init(dispatchQueue: dispatchQueue)
}
override func main() {
- guard let tunnelSettings = state.tunnelSettings else {
- finish(completion: .failure(.unsetAccount))
+ guard case .loggedIn(let accountData, let deviceData) = interactor.deviceState else {
+ finish(completion: .failure(InvalidDeviceStateError()))
return
}
task = devicesProxy.getDevice(
- accountNumber: tunnelSettings.account.number,
- identifier: tunnelSettings.device.identifier,
+ accountNumber: accountData.number,
+ identifier: deviceData.identifier,
retryStrategy: .default,
completion: { [weak self] completion in
self?.dispatchQueue.async {
self?.didReceiveDeviceResponse(
- tunnelSettings: tunnelSettings,
completion: completion
)
}
@@ -55,42 +52,27 @@ class UpdateDeviceDataOperation: ResultOperation<StoredDeviceData, TunnelManager
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)
- }
+ private func didReceiveDeviceResponse(completion: OperationCompletion<REST.Device?, REST.Error>)
+ {
+ let mappedCompletion = completion.tryMap { device -> StoredDeviceData in
+ guard let device = device else {
+ throw RevokedDeviceError()
}
- guard let device = mappedCompletion.value else {
- finish(completion: mappedCompletion.assertNoSuccess())
- return
- }
-
- do {
- var newTunnelSettings = tunnelSettings
- newTunnelSettings.device.update(from: device)
-
- try SettingsManager.writeSettings(newTunnelSettings)
+ switch interactor.deviceState {
+ case .loggedIn(let storedAccount, var storedDevice):
+ storedDevice.update(from: device)
+ let newDeviceState = DeviceState.loggedIn(storedAccount, storedDevice)
+ interactor.setDeviceState(newDeviceState, persist: true)
- finish(completion: .success(newTunnelSettings.device))
- } catch {
- logger.error(
- chainedError: AnyChainedError(error),
- message: "Failed to write settings."
- )
+ return storedDevice
- finish(completion: .failure(.writeSettings(error)))
+ default:
+ throw InvalidDeviceStateError()
+ }
}
+
+ finish(completion: mappedCompletion)
}
}
diff --git a/ios/MullvadVPN/WireguardKeysViewController.swift b/ios/MullvadVPN/WireguardKeysViewController.swift
index e772290cd2..0e29a11d64 100644
--- a/ios/MullvadVPN/WireguardKeysViewController.swift
+++ b/ios/MullvadVPN/WireguardKeysViewController.swift
@@ -85,7 +85,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
contentView.verifyKeyButton.addTarget(self, action: #selector(handleVerifyKey(_:)), for: .touchUpInside)
TunnelManager.shared.addObserver(self)
- updatePublicKey(deviceData: TunnelManager.shared.device, animated: false)
+ updatePublicKey(deviceData: TunnelManager.shared.deviceState.deviceData, animated: false)
startPublicKeyPeriodicUpdate()
}
@@ -94,7 +94,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
let interval = DispatchTimeInterval.seconds(creationDateRefreshInterval)
let timerSource = DispatchSource.makeTimerSource(queue: .main)
timerSource.setEventHandler { [weak self] () -> Void in
- self?.updatePublicKey(deviceData: TunnelManager.shared.device, animated: true)
+ self?.updatePublicKey(deviceData: TunnelManager.shared.deviceState.deviceData, animated: true)
}
timerSource.schedule(deadline: .now() + interval, repeating: interval)
timerSource.activate()
@@ -112,20 +112,24 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
// no-op
}
- func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2?) {
- updatePublicKey(deviceData: tunnelSettings?.device, animated: true)
+ func tunnelManager(_ manager: TunnelManager, didUpdateTunnelSettings tunnelSettings: TunnelSettingsV2) {
+ // no-op
}
- func tunnelManager(_ manager: TunnelManager, didFailWithError error: TunnelManager.Error) {
+ func tunnelManager(_ manager: TunnelManager, didUpdateDeviceState deviceState: DeviceState) {
+ updatePublicKey(deviceData: deviceState.deviceData, animated: true)
+ }
+
+ func tunnelManager(_ manager: TunnelManager, didFailWithError error: Error) {
// no-op
}
// MARK: - Actions
private func copyPublicKey() {
- guard let tunnelSettings = TunnelManager.shared.tunnelSettings else { return }
+ guard let deviceData = TunnelManager.shared.deviceState.deviceData else { return }
- UIPasteboard.general.string = tunnelSettings.device.wgKeyData.privateKey.publicKey.base64Key
+ UIPasteboard.general.string = deviceData.wgKeyData.privateKey.publicKey.base64Key
setPublicKeyTitle(
string: NSLocalizedString(
@@ -137,7 +141,10 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
animated: true)
let workItem = DispatchWorkItem { [weak self] in
- self?.updatePublicKey(deviceData: TunnelManager.shared.device, animated: true)
+ self?.updatePublicKey(
+ deviceData: TunnelManager.shared.deviceState.deviceData,
+ animated: true
+ )
}
DispatchQueue.main.asyncAfter(wallDeadline: .now() + .seconds(3), execute: workItem)
@@ -240,7 +247,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
self.updateViewState(.verifiedKey(true))
case .failure(let error):
- if case .deviceRevoked = error {
+ if error is RevokedDeviceError {
self.updateViewState(.verifiedKey(false))
} else {
self.showKeyVerificationFailureAlert(error)
@@ -266,8 +273,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
}
}
- private func showKeyVerificationFailureAlert(_ error: TunnelManager.Error) {
- let reason = error.errorChainDescription ?? ""
+ private func showKeyVerificationFailureAlert(_ error: Error) {
let errorDescription = String(
format: NSLocalizedString(
"VERIFY_KEY_FAILURE_ALERT_MESSAGE",
@@ -275,7 +281,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
value: "Failed to verify the WireGuard key: %@",
comment: ""
),
- reason
+ error.localizedDescription
)
let alertController = UIAlertController(
@@ -304,7 +310,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
alertPresenter.enqueue(alertController, presentingController: self)
}
- private func showKeyRegenerationFailureAlert(_ error: TunnelManager.Error) {
+ private func showKeyRegenerationFailureAlert(_ error: Error) {
let alertController = UIAlertController(
title: NSLocalizedString(
"REGENERATE_KEY_FAILURE_ALERT_TITLE",
@@ -312,7 +318,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
value: "Cannot regenerate the key",
comment: ""
),
- message: error.errorChainDescription,
+ message: error.localizedDescription,
preferredStyle: .alert
)
alertController.addAction(
diff --git a/ios/PacketTunnel/PacketTunnelConfiguration.swift b/ios/PacketTunnel/PacketTunnelConfiguration.swift
index 8493ffbf6f..69de080868 100644
--- a/ios/PacketTunnel/PacketTunnelConfiguration.swift
+++ b/ios/PacketTunnel/PacketTunnelConfiguration.swift
@@ -11,6 +11,7 @@ import WireGuardKit
import protocol Network.IPAddress
struct PacketTunnelConfiguration {
+ var deviceState: DeviceState
var tunnelSettings: TunnelSettingsV2
var selectorResult: RelaySelectorResult
}
@@ -34,15 +35,19 @@ extension PacketTunnelConfiguration {
return peerConfig
}
- var interfaceConfig = InterfaceConfiguration(
- privateKey: tunnelSettings.device.wgKeyData.privateKey
- )
+ var interfaceConfig: InterfaceConfiguration
+
+ switch deviceState {
+ case .loggedIn(_, let device):
+ interfaceConfig = InterfaceConfiguration(privateKey: device.wgKeyData.privateKey)
+ interfaceConfig.addresses = [device.ipv4Address, device.ipv6Address]
+ interfaceConfig.dns = dnsServers.map { DNSServer(address: $0) }
+
+ case .loggedOut, .revoked:
+ interfaceConfig = InterfaceConfiguration(privateKey: PrivateKey())
+ }
+
interfaceConfig.listenPort = 0
- interfaceConfig.dns = dnsServers.map { DNSServer(address: $0) }
- interfaceConfig.addresses = [
- tunnelSettings.device.ipv4Address,
- tunnelSettings.device.ipv6Address
- ]
return TunnelConfiguration(name: nil, interface: interfaceConfig, peers: peerConfigs)
}
diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift
index 67ce4db94a..01a3da6e03 100644
--- a/ios/PacketTunnel/PacketTunnelProvider.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider.swift
@@ -334,6 +334,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
private func makeConfiguration(_ appSelectorResult: RelaySelectorResult? = nil)
throws -> PacketTunnelConfiguration
{
+ let deviceState = try SettingsManager.readDeviceState()
let tunnelSettings = try SettingsManager.readSettings()
let selectorResult = try appSelectorResult
?? Self.selectRelayEndpoint(
@@ -341,6 +342,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
)
return PacketTunnelConfiguration(
+ deviceState: deviceState,
tunnelSettings: tunnelSettings,
selectorResult: selectorResult
)