summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2021-12-13 10:05:44 +0100
committerAndrej Mihajlov <and@mullvad.net>2022-02-01 10:03:11 +0100
commit6f39c06c93a1dabe748ea4a1b5bb2d15f7a9a67a (patch)
tree090224dd871667009772dfc8cd5e415a11883634
parent92f9f62174e18aeee0151c0ec7ea927b835d4e08 (diff)
downloadmullvadvpn-6f39c06c93a1dabe748ea4a1b5bb2d15f7a9a67a.tar.xz
mullvadvpn-6f39c06c93a1dabe748ea4a1b5bb2d15f7a9a67a.zip
Break tunnel management on operations
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj50
-rw-r--r--ios/MullvadVPN/Account.swift50
-rw-r--r--ios/MullvadVPN/AccountViewController.swift39
-rw-r--r--ios/MullvadVPN/AddressCache/AddressCacheStore.swift2
-rw-r--r--ios/MullvadVPN/AppDelegate.swift145
-rw-r--r--ios/MullvadVPN/ConnectViewController.swift2
-rw-r--r--ios/MullvadVPN/DisplayChainedError.swift11
-rw-r--r--ios/MullvadVPN/Operations/OperationCompletion.swift33
-rw-r--r--ios/MullvadVPN/PreferencesViewController.swift6
-rw-r--r--ios/MullvadVPN/REST/RESTClient.swift1
-rw-r--r--ios/MullvadVPN/RelayCache/RelayCacheTracker.swift11
-rw-r--r--ios/MullvadVPN/Result+UIBackgroundFetchResult.swift15
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider.swift50
-rw-r--r--ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift26
-rw-r--r--ios/MullvadVPN/TunnelManager/LoadTunnelOperation.swift236
-rw-r--r--ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift141
-rw-r--r--ios/MullvadVPN/TunnelManager/RegenerateTunnelPrivateKeyOperation.swift98
-rw-r--r--ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift122
-rw-r--r--ios/MullvadVPN/TunnelManager/RotatePrivateKeyOperation.swift123
-rw-r--r--ios/MullvadVPN/TunnelManager/SetAccountOperation.swift233
-rw-r--r--ios/MullvadVPN/TunnelManager/SetTunnelSettingsOperation.swift64
-rw-r--r--ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift190
-rw-r--r--ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift71
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelInfo.swift2
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift1167
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManagerError.swift5
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManagerState.swift107
-rw-r--r--ios/MullvadVPN/WireguardAssociatedAddresses.swift16
-rw-r--r--ios/MullvadVPN/WireguardKeysViewController.swift16
-rw-r--r--ios/MullvadVPN/en.lproj/Account.strings6
30 files changed, 1997 insertions, 1041 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 22676a1a6c..fe92e0b56c 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
+ 5801C9A527A14B2A0031566A /* TunnelManagerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5801C9A427A14B2A0031566A /* TunnelManagerState.swift */; };
5806766B27048E3C00C858CB /* AnyOptional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337026D2BE9C00CC316B /* AnyOptional.swift */; };
5806766C27048E3E00C858CB /* AnyOptional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E1337026D2BE9C00CC316B /* AnyOptional.swift */; };
5806766D27048E5500C858CB /* KeychainMatchLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDFC24533A5500CB0F5B /* KeychainMatchLimit.swift */; };
@@ -93,6 +94,7 @@
5840250222B1124600E4CFEC /* IPAddress+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250022B1124600E4CFEC /* IPAddress+Codable.swift */; };
5840250422B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */; };
5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */; };
+ 5840BE35279EDB16002836BA /* OperationCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840BE34279EDB16002836BA /* OperationCompletion.swift */; };
584592612639B4A200EF967F /* ConsentContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584592602639B4A200EF967F /* ConsentContentView.swift */; };
5846226526E0D9630035F7C2 /* ProductsRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */; };
5846226726E0DF960035F7C2 /* Promise+OperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846226626E0DF960035F7C2 /* Promise+OperationQueue.swift */; };
@@ -106,6 +108,7 @@
584789BE264D4A2A000E45FB /* le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 584789B7264D4A2A000E45FB /* le_root_cert.cer */; };
584789E026529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */; };
584789EC2652A1A2000E45FB /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 584789EB2652A1A2000E45FB /* Logging */; };
+ 584B17AB27637DE40057F3B8 /* ReloadTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584B17AA27637DE40057F3B8 /* ReloadTunnelOperation.swift */; };
584D26BF270C550B004EA533 /* AnyIPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584D26BE270C550B004EA533 /* AnyIPAddress.swift */; };
584D26C0270C550E004EA533 /* AnyIPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584D26BE270C550B004EA533 /* AnyIPAddress.swift */; };
584D26C2270C8542004EA533 /* SettingsStaticTextFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584D26C1270C8542004EA533 /* SettingsStaticTextFooterView.swift */; };
@@ -158,7 +161,6 @@
5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */; };
5868585524054096000B8131 /* AppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868585424054096000B8131 /* AppButton.swift */; };
5868BD33261DCD2600E6027F /* CustomSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5868BD32261DCD2600E6027F /* CustomSplitViewController.swift */; };
- 586AA296234B696B00502875 /* WireguardAssociatedAddresses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B8743122B25A7600015324 /* WireguardAssociatedAddresses.swift */; };
586ADD4723FC13F400CE9E87 /* countries.geo.json in Resources */ = {isa = PBXBuildFile; fileRef = 586ADD4523FC13F400CE9E87 /* countries.geo.json */; };
5871FB8325498CA20051A0A4 /* Swizzle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB8225498CA20051A0A4 /* Swizzle.swift */; };
5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */; };
@@ -190,6 +192,9 @@
587EB672271451E300123C75 /* PreferencesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587EB671271451E300123C75 /* PreferencesViewModel.swift */; };
587EB6742714520600123C75 /* PreferencesDataSourceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587EB6732714520600123C75 /* PreferencesDataSourceDelegate.swift */; };
5883A09E266A5AF7003EFFCB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 587B7543266922BF00DEF7E9 /* Localizable.strings */; };
+ 588527B2276B3F0700BAA373 /* LoadTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588527B1276B3F0700BAA373 /* LoadTunnelOperation.swift */; };
+ 588527B4276B4F2F00BAA373 /* SetAccountOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588527B3276B4F2F00BAA373 /* SetAccountOperation.swift */; };
+ 588527B6276B58B300BAA373 /* SetTunnelSettingsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588527B5276B58B300BAA373 /* SetTunnelSettingsOperation.swift */; };
58871D1E25D535A3002297FA /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 58871D1D25D535A3002297FA /* WireGuardKit */; };
58871D2325D535D2002297FA /* IPAddressRange+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */; };
5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD82227B11080051EB06 /* SelectLocationCell.swift */; };
@@ -232,7 +237,6 @@
58B3F30F2742708B00A2DD38 /* HeaderBarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B3F30E2742708B00A2DD38 /* HeaderBarButton.swift */; };
58B43C1925F77DB60002C8C3 /* ConnectMainContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B43C1825F77DB60002C8C3 /* ConnectMainContentView.swift */; };
58B67B482602079E008EF58E /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CD422AFBA39009B9D8E /* RelaySelector.swift */; };
- 58B8743222B25A7600015324 /* WireguardAssociatedAddresses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B8743122B25A7600015324 /* WireguardAssociatedAddresses.swift */; };
58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B93A1226C3F13600A55733 /* TunnelState.swift */; };
58B93A1826C54D7E00A55733 /* Locking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA692D23E99EFF009DC256 /* Locking.swift */; };
58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B993B02608A34500BA7811 /* LoginContentView.swift */; };
@@ -275,6 +279,11 @@
58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */; };
58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EF581025D69DB400AEBA94 /* StatusImageView.swift */; };
58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */; };
+ 58F2E144276A13F300A79513 /* StartTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E143276A13F300A79513 /* StartTunnelOperation.swift */; };
+ 58F2E146276A2C9900A79513 /* StopTunnelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */; };
+ 58F2E148276A307400A79513 /* MapConnectionStatusOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */; };
+ 58F2E14A276A43AA00A79513 /* RegenerateTunnelPrivateKeyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E149276A43AA00A79513 /* RegenerateTunnelPrivateKeyOperation.swift */; };
+ 58F2E14C276A61C000A79513 /* RotatePrivateKeyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F2E14B276A61C000A79513 /* RotatePrivateKeyOperation.swift */; };
58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F3C0A3249CB069003E76BE /* HeaderBarView.swift */; };
58F3C0A624A50157003E76BE /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 58F3C0A524A50155003E76BE /* relays.json */; };
58F3C0A724A50C02003E76BE /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 58F3C0A524A50155003E76BE /* relays.json */; };
@@ -367,6 +376,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 5801C9A427A14B2A0031566A /* TunnelManagerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelManagerState.swift; sourceTree = "<group>"; };
5807E2BF2432038B00F5FF30 /* String+Split.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Split.swift"; sourceTree = "<group>"; };
5807E2C1243203D000F5FF30 /* StringTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringTests.swift; sourceTree = "<group>"; };
58095C4A2760B4F200890776 /* AddressCacheStoreError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCacheStoreError.swift; sourceTree = "<group>"; };
@@ -414,6 +424,7 @@
583DA21325FA4B5C00318683 /* LocationDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDataSource.swift; sourceTree = "<group>"; };
5840250022B1124600E4CFEC /* IPAddress+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IPAddress+Codable.swift"; sourceTree = "<group>"; };
5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadEndpoint.swift; sourceTree = "<group>"; };
+ 5840BE34279EDB16002836BA /* OperationCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationCompletion.swift; sourceTree = "<group>"; };
584592602639B4A200EF967F /* ConsentContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentContentView.swift; sourceTree = "<group>"; };
5845F841236CBACD00B2D93C /* TunnelIPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelIPC.swift; sourceTree = "<group>"; };
5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsRequestOperation.swift; sourceTree = "<group>"; };
@@ -425,6 +436,7 @@
5846227626E22A7C0035F7C2 /* AppStorePaymentManagerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorePaymentManagerDelegate.swift; sourceTree = "<group>"; };
584789B7264D4A2A000E45FB /* le_root_cert.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = le_root_cert.cer; sourceTree = "<group>"; };
584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLPinningURLSessionDelegate.swift; sourceTree = "<group>"; };
+ 584B17AA27637DE40057F3B8 /* ReloadTunnelOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReloadTunnelOperation.swift; sourceTree = "<group>"; };
584B26F3237434D00073B10E /* RelaySelectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorTests.swift; sourceTree = "<group>"; };
584D26BE270C550B004EA533 /* AnyIPAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyIPAddress.swift; sourceTree = "<group>"; };
584D26C1270C8542004EA533 /* SettingsStaticTextFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStaticTextFooterView.swift; sourceTree = "<group>"; };
@@ -479,6 +491,9 @@
587EB66F27143B6500123C75 /* DataSourceSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSourceSnapshot.swift; sourceTree = "<group>"; };
587EB671271451E300123C75 /* PreferencesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewModel.swift; sourceTree = "<group>"; };
587EB6732714520600123C75 /* PreferencesDataSourceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesDataSourceDelegate.swift; sourceTree = "<group>"; };
+ 588527B1276B3F0700BAA373 /* LoadTunnelOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadTunnelOperation.swift; sourceTree = "<group>"; };
+ 588527B3276B4F2F00BAA373 /* SetAccountOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetAccountOperation.swift; sourceTree = "<group>"; };
+ 588527B5276B58B300BAA373 /* SetTunnelSettingsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetTunnelSettingsOperation.swift; sourceTree = "<group>"; };
5888AD82227B11080051EB06 /* SelectLocationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationCell.swift; sourceTree = "<group>"; };
5888AD86227B17950051EB06 /* SelectLocationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationViewController.swift; sourceTree = "<group>"; };
588CD8BA275A0A0B00CF902E /* RESTRequestAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTRequestAdapter.swift; sourceTree = "<group>"; };
@@ -506,7 +521,6 @@
58B0A2A4238EE67E00BC001D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
58B3F30E2742708B00A2DD38 /* HeaderBarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderBarButton.swift; sourceTree = "<group>"; };
58B43C1825F77DB60002C8C3 /* ConnectMainContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectMainContentView.swift; sourceTree = "<group>"; };
- 58B8743122B25A7600015324 /* WireguardAssociatedAddresses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireguardAssociatedAddresses.swift; sourceTree = "<group>"; };
58B93A1226C3F13600A55733 /* TunnelState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelState.swift; sourceTree = "<group>"; };
58B993B02608A34500BA7811 /* LoginContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginContentView.swift; sourceTree = "<group>"; };
58B9EB122488ED2100095626 /* AlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPresenter.swift; sourceTree = "<group>"; };
@@ -553,6 +567,11 @@
58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportSubmissionOverlayView.swift; sourceTree = "<group>"; };
58EF581025D69DB400AEBA94 /* StatusImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusImageView.swift; sourceTree = "<group>"; };
58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerActivityIndicatorView.swift; sourceTree = "<group>"; };
+ 58F2E143276A13F300A79513 /* StartTunnelOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartTunnelOperation.swift; sourceTree = "<group>"; };
+ 58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopTunnelOperation.swift; sourceTree = "<group>"; };
+ 58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapConnectionStatusOperation.swift; sourceTree = "<group>"; };
+ 58F2E149276A43AA00A79513 /* RegenerateTunnelPrivateKeyOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegenerateTunnelPrivateKeyOperation.swift; sourceTree = "<group>"; };
+ 58F2E14B276A61C000A79513 /* RotatePrivateKeyOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RotatePrivateKeyOperation.swift; sourceTree = "<group>"; };
58F3C0A3249CB069003E76BE /* HeaderBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderBarView.swift; sourceTree = "<group>"; };
58F3C0A524A50155003E76BE /* relays.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = relays.json; sourceTree = "<group>"; };
58F558DC2695B85E00F630D0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Consent.strings; sourceTree = "<group>"; };
@@ -660,6 +679,7 @@
5820675D26E6839900655B05 /* PresentAlertOperation.swift */,
5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */,
5846226926E0E6FA0035F7C2 /* ReceiptRefreshOperation.swift */,
+ 5840BE34279EDB16002836BA /* OperationCompletion.swift */,
);
path = Operations;
sourceTree = "<group>";
@@ -685,9 +705,19 @@
587C575226D2615F005EF767 /* PacketTunnelOptions.swift */,
58FB866026EB677F00F188BC /* TunnelInfo.swift */,
5835B7CB233B76CB0096D79F /* TunnelManager.swift */,
+ 5801C9A427A14B2A0031566A /* TunnelManagerState.swift */,
5820676326E771DB00655B05 /* TunnelManagerError.swift */,
5823FA5326CE49F600283BF8 /* TunnelObserver.swift */,
58B93A1226C3F13600A55733 /* TunnelState.swift */,
+ 588527B1276B3F0700BAA373 /* LoadTunnelOperation.swift */,
+ 584B17AA27637DE40057F3B8 /* ReloadTunnelOperation.swift */,
+ 588527B3276B4F2F00BAA373 /* SetAccountOperation.swift */,
+ 58F2E143276A13F300A79513 /* StartTunnelOperation.swift */,
+ 58F2E145276A2C9900A79513 /* StopTunnelOperation.swift */,
+ 58F2E147276A307400A79513 /* MapConnectionStatusOperation.swift */,
+ 58F2E149276A43AA00A79513 /* RegenerateTunnelPrivateKeyOperation.swift */,
+ 58F2E14B276A61C000A79513 /* RotatePrivateKeyOperation.swift */,
+ 588527B5276B58B300BAA373 /* SetTunnelSettingsOperation.swift */,
);
path = TunnelManager;
sourceTree = "<group>";
@@ -955,7 +985,6 @@
5856D13627450A8A00DFD627 /* UIImage+TintColor.swift */,
585CA70E25F8C44600B47C62 /* UIMetrics.swift */,
58FD5BF12424F7D700112C88 /* UserInterfaceInteractionRestriction.swift */,
- 58B8743122B25A7600015324 /* WireguardAssociatedAddresses.swift */,
58F7CA872692E34000FC59FD /* WireguardKeysContentView.swift */,
5877152F23981F7B001F8237 /* WireguardKeysViewController.swift */,
);
@@ -1338,6 +1367,7 @@
58095C512760BBB500890776 /* AddressCacheTracker.swift in Sources */,
584D26C6270C8741004EA533 /* SettingsDNSTextCell.swift in Sources */,
585DA87D26B0254000B8C587 /* RelayCacheIO.swift in Sources */,
+ 58F2E148276A307400A79513 /* MapConnectionStatusOperation.swift in Sources */,
58BA693123EADA6A009DC256 /* SimulatorTunnelProvider.swift in Sources */,
587C575326D2615F005EF767 /* PacketTunnelOptions.swift in Sources */,
58E1336D26D2BE7500CC316B /* AnyResult.swift in Sources */,
@@ -1349,6 +1379,7 @@
588CD8BB275A0A0B00CF902E /* RESTRequestAdapter.swift in Sources */,
582BB1B52295780F0055B6EF /* AccountExpiry.swift in Sources */,
582BB1B3229574F40055B6EF /* SettingsAccountCell.swift in Sources */,
+ 588527B2276B3F0700BAA373 /* LoadTunnelOperation.swift in Sources */,
585DA88426B0270700B8C587 /* ServerRelaysResponse.swift in Sources */,
5875960726F36B3A00BF6711 /* TunnelIPCError.swift in Sources */,
58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */,
@@ -1369,10 +1400,12 @@
5806766C27048E3E00C858CB /* AnyOptional.swift in Sources */,
58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */,
5850367F25A481D800A43E93 /* IPAddressRange+Codable.swift in Sources */,
+ 58F2E14C276A61C000A79513 /* RotatePrivateKeyOperation.swift in Sources */,
5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */,
58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */,
5846227326E22A160035F7C2 /* AppStorePaymentObserver.swift in Sources */,
58FAEDEF245069C700CB0F5B /* KeychainAttributes.swift in Sources */,
+ 58F2E146276A2C9900A79513 /* StopTunnelOperation.swift in Sources */,
585DA87A26B024F900B8C587 /* RelayCacheError.swift in Sources */,
5856D13727450A8A00DFD627 /* UIImage+TintColor.swift in Sources */,
58CB0EE024B86751001EF0D8 /* RESTClient.swift in Sources */,
@@ -1400,6 +1433,7 @@
587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */,
5806767927048E8800C858CB /* Keychain.swift in Sources */,
5846227526E22A350035F7C2 /* AnyAppStorePaymentObserver.swift in Sources */,
+ 588527B4276B4F2F00BAA373 /* SetAccountOperation.swift in Sources */,
585CA70F25F8C44600B47C62 /* UIMetrics.swift in Sources */,
58095C592762155700890776 /* RESTRetryStrategy.swift in Sources */,
5840250422B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */,
@@ -1408,12 +1442,13 @@
5820674E26E6510200655B05 /* REST.swift in Sources */,
5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */,
58F7CA882692E34000FC59FD /* WireguardKeysContentView.swift in Sources */,
+ 5801C9A527A14B2A0031566A /* TunnelManagerState.swift in Sources */,
58095C4B2760B4F200890776 /* AddressCacheStoreError.swift in Sources */,
5868585524054096000B8131 /* AppButton.swift in Sources */,
58781CC922AE7CA8009B9D8E /* RelayConstraints.swift in Sources */,
584E96BC240FD4DA00D3334F /* Location.swift in Sources */,
581503A124D6F01F00C9C50E /* LogRotation.swift in Sources */,
- 58B8743222B25A7600015324 /* WireguardAssociatedAddresses.swift in Sources */,
+ 584B17AB27637DE40057F3B8 /* ReloadTunnelOperation.swift in Sources */,
5820676426E771DB00655B05 /* TunnelManagerError.swift in Sources */,
585B4B8726D9098900555C4C /* TunnelErrorNotificationProvider.swift in Sources */,
5846226726E0DF960035F7C2 /* Promise+OperationQueue.swift in Sources */,
@@ -1456,6 +1491,7 @@
588DD76B26FCB49E006F6233 /* Cancellable.swift in Sources */,
58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */,
5857F24324C8662600CF6F47 /* SelectLocationHeaderView.swift in Sources */,
+ 5840BE35279EDB16002836BA /* OperationCompletion.swift in Sources */,
58EE2E3A272FF814003BFF93 /* SettingsDataSource.swift in Sources */,
58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */,
581503A624D6F4AE00C9C50E /* Logging.swift in Sources */,
@@ -1463,9 +1499,11 @@
58FB865526E8BF3100F188BC /* AppStorePaymentManagerError.swift in Sources */,
58FD5BF42428C67600112C88 /* InAppPurchaseButton.swift in Sources */,
580EE22424B3243100F9D8A1 /* AsyncBlockOperation.swift in Sources */,
+ 588527B6276B58B300BAA373 /* SetTunnelSettingsOperation.swift in Sources */,
58293FAE2510CA58005D0BB5 /* ProblemReportViewController.swift in Sources */,
58B9EB152489139B00095626 /* DisplayChainedError.swift in Sources */,
587B753F2668E5A700DEF7E9 /* NotificationContainerView.swift in Sources */,
+ 58F2E144276A13F300A79513 /* StartTunnelOperation.swift in Sources */,
5868BD33261DCD2600E6027F /* CustomSplitViewController.swift in Sources */,
5806766E27048E5600C858CB /* KeychainMatchLimit.swift in Sources */,
58CCA01E2242787B004F3011 /* AccountTextField.swift in Sources */,
@@ -1491,6 +1529,7 @@
58561C99239A5D1500BD6B5E /* IPEndpoint.swift in Sources */,
58FD5BF22424F7D700112C88 /* UserInterfaceInteractionRestriction.swift in Sources */,
5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */,
+ 58F2E14A276A43AA00A79513 /* RegenerateTunnelPrivateKeyOperation.swift in Sources */,
58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */,
58F840B22464491D0044E708 /* ChainedError.swift in Sources */,
58FAEDFF24533A7000CB0F5B /* KeychainReturn.swift in Sources */,
@@ -1531,7 +1570,6 @@
5840250222B1124600E4CFEC /* IPAddress+Codable.swift in Sources */,
5820675C26E6576800655B05 /* RelayCache.swift in Sources */,
58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */,
- 586AA296234B696B00502875 /* WireguardAssociatedAddresses.swift in Sources */,
585DA89A26B0329200B8C587 /* TunnelConnectionInfo.swift in Sources */,
585DA88526B0270700B8C587 /* ServerRelaysResponse.swift in Sources */,
5806767627048E7D00C858CB /* Promise+Result.swift in Sources */,
diff --git a/ios/MullvadVPN/Account.swift b/ios/MullvadVPN/Account.swift
index 76d4138e2f..02f16c4fc8 100644
--- a/ios/MullvadVPN/Account.swift
+++ b/ios/MullvadVPN/Account.swift
@@ -150,20 +150,23 @@ class Account {
}
/// Perform the logout by erasing the account token and expiry from the application preferences.
- func logout() -> Result<(), Account.Error>.Promise {
- return TunnelManager.shared.unsetAccount()
- .mapError { error in
- return Account.Error.tunnelConfiguration(error)
+ func logout() -> Promise<Void> {
+ return Promise { resolver in
+ TunnelManager.shared.unsetAccount {
+ resolver.resolve(value: ())
}
- .receive(on: .main)
- .onSuccess { _ in
- self.removeFromPreferences()
- self.observerList.forEach { (observer) in
- observer.accountDidLogout(self)
- }
+ }
+ .receive(on: .main)
+ .then { _ -> () in
+ self.removeFromPreferences()
+ self.observerList.forEach { (observer) in
+ observer.accountDidLogout(self)
}
- .block(on: dispatchQueue)
- .receive(on: .main)
+
+ return ()
+ }
+ .block(on: dispatchQueue)
+ .receive(on: .main)
}
/// Forget that user was logged in, but do not attempt to unset account in `TunnelManager`.
@@ -206,16 +209,21 @@ class Account {
}
}
- private func setupTunnel(accountToken: String, expiry: Date) -> Result<(), Error>.Promise {
- return TunnelManager.shared.setAccount(accountToken: accountToken)
- .receive(on: .main)
- .mapError { error in
- return Error.tunnelConfiguration(error)
- }
- .onSuccess { _ in
- self.token = accountToken
- self.expiry = expiry
+ private func setupTunnel(accountToken: String, expiry: Date) -> Result<(), Account.Error>.Promise {
+ return Promise { resolver in
+ TunnelManager.shared.setAccount(accountToken: accountToken) { error in
+ dispatchPrecondition(condition: .onQueue(.main))
+
+ if let error = error {
+ resolver.resolve(value: .failure(Account.Error.tunnelConfiguration(error)))
+ } else {
+ self.token = accountToken
+ self.expiry = expiry
+
+ resolver.resolve(value: .success(()))
+ }
}
+ }
}
private func removeFromPreferences() {
diff --git a/ios/MullvadVPN/AccountViewController.swift b/ios/MullvadVPN/AccountViewController.swift
index bb1ff3b62f..305c37cc06 100644
--- a/ios/MullvadVPN/AccountViewController.swift
+++ b/ios/MullvadVPN/AccountViewController.swift
@@ -296,47 +296,14 @@ class AccountViewController: UIViewController, AppStorePaymentObserver, AccountO
alertPresenter.enqueue(alertController, presentingController: self) {
Account.shared.logout()
.receive(on: .main, after: .seconds(1), timerType: .deadline)
- .then { result in
- return Promise { resolver in
- alertController.dismiss(animated: true) {
- resolver.resolve(value: result)
- }
+ .observe { _ in
+ alertController.dismiss(animated: true) {
+ self.delegate?.accountViewControllerDidLogout(self)
}
}
- .onSuccess { _ in
- self.delegate?.accountViewControllerDidLogout(self)
- }
- .onFailure { error in
- self.logger.error(chainedError: error, message: "Failed to log out")
-
- self.showLogoutFailure(error)
- }
- .observe { _ in }
}
}
- private func showLogoutFailure(_ error: Account.Error) {
- let errorAlertController = UIAlertController(
- title: NSLocalizedString(
- "LOGOUT_FAILURE_ALERT_TITLE",
- tableName: "Account",
- value: "Failed to log out",
- comment: "Title for logout failure alert"
- ),
- message: error.errorChainDescription,
- preferredStyle: .alert
- )
- errorAlertController.addAction(
- UIAlertAction(title: NSLocalizedString(
- "LOGOUT_FAILURE_ALERT_OK_ACTION",
- tableName: "Account",
- value: "OK",
- comment: "Message for logout failure alert"
- ), style: .cancel)
- )
- alertPresenter.enqueue(errorAlertController, presentingController: self)
- }
-
// MARK: - AccountObserver
func account(_ account: Account, didUpdateExpiry expiry: Date) {
diff --git a/ios/MullvadVPN/AddressCache/AddressCacheStore.swift b/ios/MullvadVPN/AddressCache/AddressCacheStore.swift
index ef479858e3..37bf2f2cd7 100644
--- a/ios/MullvadVPN/AddressCache/AddressCacheStore.swift
+++ b/ios/MullvadVPN/AddressCache/AddressCacheStore.swift
@@ -170,7 +170,7 @@ extension AddressCache {
newEndpoints.remove(at: index)
newEndpoints.insert(currentEndpoint, at: 0)
}
-
+
self.cachedAddresses = CachedAddresses(
updatedAt: Date(),
endpoints: newEndpoints
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index 04f0f78dca..3a673b17aa 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -105,13 +105,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
// Load tunnels
- TunnelManager.shared.loadTunnel(accountToken: Account.shared.token)
- .receive(on: .main)
- .onSuccess { _ in
- self.relayConstraints = TunnelManager.shared.tunnelInfo?.tunnelSettings.relayConstraints
- self.didFinishInitialization()
- }
- .onFailure { error in
+ TunnelManager.shared.loadTunnel(accountToken: Account.shared.token) { error in
+ dispatchPrecondition(condition: .onQueue(.main))
+
+ if let error = error {
self.logger?.error(chainedError: error, message: "Failed to load tunnels")
switch error {
@@ -130,8 +127,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
default:
fatalError("Unexpected error coming from loadTunnel()")
}
+ } else {
+ self.relayConstraints = TunnelManager.shared.tunnelInfo?.tunnelSettings.relayConstraints
+ self.didFinishInitialization()
}
- .observe { _ in }
+ }
// Show the window
self.window?.makeKeyAndVisible()
@@ -177,42 +177,88 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
logger?.info("Start background refresh")
- _ = addressCacheTracker.updateEndpoints { addressCacheUpdateResult in
+ var addressCacheFetchResult: UIBackgroundFetchResult?
+ var relaysFetchResult: UIBackgroundFetchResult?
+ var rotatePrivateKeyFetchResult: UIBackgroundFetchResult?
+
+ let operationQueue = OperationQueue()
+
+ let updateAddressCacheOperation = AsyncBlockOperation { operation in
+ _ = self.addressCacheTracker.updateEndpoints { result in
+ addressCacheFetchResult = result.backgroundFetchResult
+ operation.finish()
+ }
+ }
+
+ let updateRelaysOperation = AsyncBlockOperation { operation in
RelayCache.Tracker.shared.updateRelays()
- .then { fetchRelaysResult -> Promise<UIBackgroundFetchResult> in
- switch fetchRelaysResult {
+ .observe { completion in
+ switch completion.unwrappedValue {
case .success(let result):
self.logger?.debug("Finished updating relays: \(result)")
case .failure(let error):
self.logger?.error(chainedError: error, message: "Failed to update relays")
+ case .none:
+ break
}
- return TunnelManager.shared.rotatePrivateKey()
- .then { rotationResult -> UIBackgroundFetchResult in
- switch rotationResult {
- case .success(let result):
- self.logger?.debug("Finished rotating the key: \(result)")
- case .failure(let error):
- self.logger?.error(chainedError: error, message: "Failed to rotate the key")
- }
+ relaysFetchResult = completion.unwrappedValue?.backgroundFetchResult
- return addressCacheUpdateResult.backgroundFetchResult
- .combine(with: [fetchRelaysResult.backgroundFetchResult, rotationResult.backgroundFetchResult])
- }
+ operation.finish()
}
- .receive(on: .main)
- .observe { completion in
- switch completion {
- case .finished(let backgroundFetchResult):
- self.logger?.info("Finish background refresh with \(backgroundFetchResult)")
- completionHandler(backgroundFetchResult)
+ }
- case .cancelled:
- self.logger?.info("Finish background refresh with cancelled promise")
- completionHandler(.failed)
+ let rotatePrivateKeyOperation = AsyncBlockOperation { operation in
+ guard !operation.isCancelled else {
+ operation.finish()
+ return
+ }
+
+ TunnelManager.shared.rotatePrivateKey { rotationResult, error in
+ if let error = error {
+ self.logger?.error(chainedError: error, message: "Failed to rotate the key")
+
+ rotatePrivateKeyFetchResult = .failed
+ } else if let rotationResult = rotationResult {
+ self.logger?.debug("Finished rotating the key: \(rotationResult)")
+
+ switch rotationResult {
+ case .throttled:
+ rotatePrivateKeyFetchResult = .noData
+
+ case .finished:
+ rotatePrivateKeyFetchResult = .newData
}
}
+
+ operation.finish()
+ }
}
+
+ rotatePrivateKeyOperation.addDependency(updateRelaysOperation)
+ rotatePrivateKeyOperation.addDependency(updateAddressCacheOperation)
+
+ let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "AppDelegate.performFetch") {
+ operationQueue.cancelAllOperations()
+ }
+
+ rotatePrivateKeyOperation.completionBlock = {
+ DispatchQueue.main.async {
+ let operationResults = [addressCacheFetchResult, relaysFetchResult, rotatePrivateKeyFetchResult].compactMap { $0 }
+ let initialResult = operationResults.first ?? .failed
+ let backgroundFetchResult = operationResults.reduce(initialResult) { partialResult, other in
+ return partialResult.combine(with: other)
+ }
+
+ self.logger?.info("Finish background refresh with \(backgroundFetchResult)")
+
+ completionHandler(backgroundFetchResult)
+
+ UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
+ }
+ }
+
+ operationQueue.addOperations([updateAddressCacheOperation, updateRelaysOperation, rotatePrivateKeyOperation], waitUntilFinished: false)
}
// MARK: - Private
@@ -228,13 +274,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
- if case .finished(let result) = TunnelManager.shared.scheduleBackgroundTask().await() {
- switch result {
- case .success:
- self.logger?.debug("Scheduled private key rotation task")
- case .failure(let error):
- self.logger?.error(chainedError: error, message: "Could not schedule private key rotation task")
- }
+ switch TunnelManager.shared.scheduleBackgroundTask() {
+ case .success:
+ self.logger?.debug("Scheduled private key rotation task")
+ case .failure(let error):
+ self.logger?.error(chainedError: error, message: "Could not schedule private key rotation task")
}
do {
@@ -244,6 +288,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
} catch {
self.logger?.error(chainedError: AnyChainedError(error), message: "Could not schedule address cache update task")
}
+
}
private func didFinishInitialization() {
@@ -497,7 +542,7 @@ extension AppDelegate: RootContainerViewControllerDelegate {
switch TunnelManager.shared.tunnelState {
case .connected, .connecting, .reconnecting:
- TunnelManager.shared.reconnectTunnel()
+ TunnelManager.shared.reconnectTunnel(completionHandler: nil)
case .disconnecting, .disconnected:
TunnelManager.shared.startTunnel()
case .pendingReconnect:
@@ -658,22 +703,16 @@ extension AppDelegate: SelectLocationViewControllerDelegate {
private func selectLocationControllerDidSelectRelayLocation(_ relayLocation: RelayLocation) {
let relayConstraints = RelayConstraints(location: .only(relayLocation))
- TunnelManager.shared.setRelayConstraints(relayConstraints)
- .receive(on: .main)
- .observe { completion in
- guard let result = completion.unwrappedValue else { return }
-
- self.relayConstraints = relayConstraints
+ TunnelManager.shared.setRelayConstraints(relayConstraints) { error in
+ self.relayConstraints = relayConstraints
- switch result {
- case .success:
- self.logger?.debug("Updated relay constraints: \(relayConstraints)")
- TunnelManager.shared.startTunnel()
-
- case .failure(let error):
- self.logger?.error(chainedError: error, message: "Failed to update relay constraints")
- }
+ 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.startTunnel()
}
+ }
}
}
diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift
index 4e87ff489c..03ca5a2563 100644
--- a/ios/MullvadVPN/ConnectViewController.swift
+++ b/ios/MullvadVPN/ConnectViewController.swift
@@ -345,7 +345,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen
}
@objc func handleReconnect(_ sender: Any) {
- TunnelManager.shared.reconnectTunnel()
+ TunnelManager.shared.reconnectTunnel(completionHandler: nil)
}
@objc func handleSelectLocation(_ sender: Any) {
diff --git a/ios/MullvadVPN/DisplayChainedError.swift b/ios/MullvadVPN/DisplayChainedError.swift
index 86ebb94b1f..ce03bf5348 100644
--- a/ios/MullvadVPN/DisplayChainedError.swift
+++ b/ios/MullvadVPN/DisplayChainedError.swift
@@ -261,6 +261,17 @@ extension TunnelManager.Error: DisplayChainedError {
case .backgroundTaskScheduler:
// This error is never displayed anywhere
return nil
+
+ case .reloadTunnel(let error):
+ return String(
+ format: NSLocalizedString(
+ "RELOAD_TUNNEL_ERROR",
+ tableName: "TunnelManager",
+ value: "Failed to reload tunnel: %@",
+ comment: ""
+ ),
+ error.localizedDescription
+ )
}
}
}
diff --git a/ios/MullvadVPN/Operations/OperationCompletion.swift b/ios/MullvadVPN/Operations/OperationCompletion.swift
new file mode 100644
index 0000000000..8c115540e3
--- /dev/null
+++ b/ios/MullvadVPN/Operations/OperationCompletion.swift
@@ -0,0 +1,33 @@
+//
+// OperationCompletion.swift
+// MullvadVPN
+//
+// Created by pronebird on 24/01/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+enum OperationCompletion<Success, Failure: Error> {
+ case cancelled
+ case success(Success)
+ case failure(Failure)
+
+ var error: Failure? {
+ if case .failure(let error) = self {
+ return error
+ } else {
+ return nil
+ }
+ }
+
+ init(result: Result<Success, Failure>) {
+ switch result {
+ case .success(let value):
+ self = .success(value)
+
+ case .failure(let error):
+ self = .failure(error)
+ }
+ }
+}
diff --git a/ios/MullvadVPN/PreferencesViewController.swift b/ios/MullvadVPN/PreferencesViewController.swift
index db26bb6a4b..a5cc0dc06c 100644
--- a/ios/MullvadVPN/PreferencesViewController.swift
+++ b/ios/MullvadVPN/PreferencesViewController.swift
@@ -68,11 +68,11 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel
func preferencesDataSource(_ dataSource: PreferencesDataSource, didChangeViewModel dataModel: PreferencesViewModel) {
let dnsSettings = dataModel.asDNSSettings()
- TunnelManager.shared.setDNSSettings(dnsSettings)
- .onFailure { [weak self] error in
+ TunnelManager.shared.setDNSSettings(dnsSettings) { [weak self] error in
+ if let error = error {
self?.logger.error(chainedError: error, message: "Failed to save DNS settings")
}
- .observe { _ in }
+ }
}
// MARK: - TunnelObserver
diff --git a/ios/MullvadVPN/REST/RESTClient.swift b/ios/MullvadVPN/REST/RESTClient.swift
index bc9f49301f..b58822d4cd 100644
--- a/ios/MullvadVPN/REST/RESTClient.swift
+++ b/ios/MullvadVPN/REST/RESTClient.swift
@@ -209,7 +209,6 @@ extension REST {
}
}
-
func replaceWireguardKey(token: String, oldPublicKey: PublicKey, newPublicKey: PublicKey) -> REST.RequestAdapter<WireguardAddressesResponse> {
return makeAdapter { endpoint, completionHandler in
var request = self.createURLRequestWithEndpoint(endpoint: endpoint, method: .post, path: "replace-wireguard-key")
diff --git a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
index 06ad5f1f85..d97b958d85 100644
--- a/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
+++ b/ios/MullvadVPN/RelayCache/RelayCacheTracker.swift
@@ -122,6 +122,17 @@ extension RelayCache {
.requestBackgroundTime(taskName: "RelayCacheTracker.updateRelays")
}
+ func read(completionHandler: @escaping (Result<CachedRelays, RelayCache.Error>) -> Void) {
+ stateQueue.async {
+ let result = RelayCache.IO.readWithFallback(
+ cacheFileURL: self.cacheFileURL,
+ preBundledRelaysFileURL: self.prebundledRelaysFileURL
+ )
+
+ completionHandler(result)
+ }
+ }
+
func read() -> Result<CachedRelays, RelayCache.Error>.Promise {
return Promise.deferred {
return RelayCache.IO.readWithFallback(
diff --git a/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift b/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift
index 49aaa40df7..544c06dafe 100644
--- a/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift
+++ b/ios/MullvadVPN/Result+UIBackgroundFetchResult.swift
@@ -8,21 +8,6 @@
import UIKit
-extension Result where Success == TunnelManager.KeyRotationResult {
- var backgroundFetchResult: UIBackgroundFetchResult {
- switch self.asConcreteType() {
- case .success(.finished):
- return .newData
-
- case .success(.throttled):
- return .noData
-
- case .failure:
- return .failed
- }
- }
-}
-
extension AddressCache.CacheUpdateResult {
var backgroundFetchResult: UIBackgroundFetchResult {
switch self {
diff --git a/ios/MullvadVPN/SimulatorTunnelProvider.swift b/ios/MullvadVPN/SimulatorTunnelProvider.swift
index 5637651a40..0503e72b5b 100644
--- a/ios/MullvadVPN/SimulatorTunnelProvider.swift
+++ b/ios/MullvadVPN/SimulatorTunnelProvider.swift
@@ -45,56 +45,6 @@ extension NEVPNConnection: VPNConnectionProtocol {}
extension NETunnelProviderSession: VPNTunnelProviderSessionProtocol {}
extension NETunnelProviderManager: VPNTunnelProviderManagerProtocol {}
-extension VPNTunnelProviderManagerProtocol {
- static func loadAllFromPreferences() -> Result<[SelfType]?, Error>.Promise {
- return Result<[SelfType]?, Error>.Promise { resolver in
- Self.loadAllFromPreferences { tunnels, error in
- if let error = error {
- resolver.resolve(value: .failure(error))
- } else {
- resolver.resolve(value: .success(tunnels))
- }
- }
- }
- }
-
- func loadFromPreferences() -> Result<(), Error>.Promise {
- return Result<(), Error>.Promise { resolver in
- loadFromPreferences { error in
- if let error = error {
- resolver.resolve(value: .failure(error))
- } else {
- resolver.resolve(value: .success(()))
- }
- }
- }
- }
-
- func saveToPreferences() -> Result<(), Error>.Promise {
- return Result<(), Error>.Promise { resolver in
- saveToPreferences { error in
- if let error = error {
- resolver.resolve(value: .failure(error))
- } else {
- resolver.resolve(value: .success(()))
- }
- }
- }
- }
-
- func removeFromPreferences() -> Result<(), Error>.Promise {
- return Result<(), Error>.Promise { resolver in
- removeFromPreferences { error in
- if let error = error {
- resolver.resolve(value: .failure(error))
- } else {
- resolver.resolve(value: .success(()))
- }
- }
- }
- }
-}
-
#if targetEnvironment(simulator)
// MARK: - NEPacketTunnelProvider stubs
diff --git a/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift b/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift
index 6ccc641d46..4589eeb45f 100644
--- a/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift
+++ b/ios/MullvadVPN/TunnelIPC/TunnelIPCSession.swift
@@ -18,25 +18,21 @@ extension TunnelIPC {
tunnelProviderSession = tunnelProvider.connection as! VPNTunnelProviderSessionProtocol
}
- func reloadTunnelSettings() -> Result<(), Error>.Promise {
- return Result<(), Error>.Promise { resolver in
- self.send(message: .reloadTunnelSettings) { result in
- resolver.resolve(value: result)
- }
+ func reloadTunnelSettings(completionHandler: @escaping (TunnelIPC.Error?) -> Void) {
+ send(message: .reloadTunnelSettings) { result in
+ completionHandler(result.error)
}
}
- func getTunnelConnectionInfo() -> Result<TunnelConnectionInfo?, Error>.Promise {
- return Result<TunnelConnectionInfo?, Error>.Promise { resolver in
- self.send(message: .tunnelConnectionInfo) { result in
- resolver.resolve(value: result)
- }
+ func getTunnelConnectionInfo(completionHandler: @escaping (Result<TunnelConnectionInfo?, TunnelIPC.Error>) -> Void) {
+ send(message: .tunnelConnectionInfo) { result in
+ completionHandler(result)
}
}
// MARK: - Private
- private func send(message: TunnelIPC.Request, completionHandler: @escaping (Result<(), Error>) -> Void) {
+ private func send(message: TunnelIPC.Request, completionHandler: @escaping (Result<(), TunnelIPC.Error>) -> Void) {
sendWithoutDecoding(message: message) { (result) in
let result = result.map { _ in () }
@@ -44,10 +40,10 @@ extension TunnelIPC {
}
}
- private func send<T>(message: TunnelIPC.Request, completionHandler: @escaping (Result<T, Error>) -> Void) where T: Codable
+ private func send<T>(message: TunnelIPC.Request, completionHandler: @escaping (Result<T, TunnelIPC.Error>) -> Void) where T: Codable
{
sendWithoutDecoding(message: message) { (result) in
- let result = result.flatMap { (data) -> Result<T, Error> in
+ let result = result.flatMap { (data) -> Result<T, TunnelIPC.Error> in
guard let data = data else {
return .failure(.nilResponse)
}
@@ -62,7 +58,7 @@ extension TunnelIPC {
}
}
- private func sendWithoutDecoding(message: TunnelIPC.Request, completionHandler: @escaping (Result<Data?, Error>) -> Void) {
+ private func sendWithoutDecoding(message: TunnelIPC.Request, completionHandler: @escaping (Result<Data?, TunnelIPC.Error>) -> Void) {
do {
let data = try TunnelIPC.Coding.encodeRequest(message)
@@ -74,7 +70,7 @@ extension TunnelIPC {
}
}
- private func sendProviderMessage(_ messageData: Data, completionHandler: @escaping (Result<Data?, Error>) -> Void) {
+ private func sendProviderMessage(_ messageData: Data, completionHandler: @escaping (Result<Data?, TunnelIPC.Error>) -> Void) {
do {
try tunnelProviderSession.sendProviderMessage(messageData) { response in
completionHandler(.success(response))
diff --git a/ios/MullvadVPN/TunnelManager/LoadTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/LoadTunnelOperation.swift
new file mode 100644
index 0000000000..8cdb64bcab
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/LoadTunnelOperation.swift
@@ -0,0 +1,236 @@
+//
+// LoadTunnelOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 16/12/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Logging
+
+class LoadTunnelOperation: AsyncOperation {
+ typealias CompletionHandler = (OperationCompletion<(), TunnelManager.Error>) -> Void
+
+ private let queue: DispatchQueue
+ private let accountToken: String?
+ private let state: TunnelManager.State
+ private var completionHandler: CompletionHandler?
+
+ private let logger = Logger(label: "TunnelManager.LoadTunnelOperation")
+
+ init(queue: DispatchQueue, state: TunnelManager.State, accountToken: String?, completionHandler: @escaping CompletionHandler) {
+ self.queue = queue
+ self.state = state
+ self.accountToken = accountToken
+ self.completionHandler = completionHandler
+ }
+
+ override func main() {
+ queue.async {
+ self.execute { completion in
+ self.completionHandler?(completion)
+ self.completionHandler = nil
+
+ self.finish()
+ }
+ }
+ }
+
+ private func execute(completionHandler: @escaping CompletionHandler) {
+ guard !isCancelled else {
+ completionHandler(.cancelled)
+ return
+ }
+
+ // Migrate the tunnel settings if needed
+ if let accountToken = accountToken {
+ let migrationResult = migrateTunnelSettings(accountToken: accountToken)
+
+ if case .failure(let migrationError) = migrationResult {
+ completionHandler(.failure(migrationError))
+ return
+ }
+ }
+
+ TunnelProviderManagerType.loadAllFromPreferences { tunnels, error in
+ self.queue.async {
+ if let error = error {
+ completionHandler(.failure(.loadAllVPNConfigurations(error)))
+ } else {
+ self.didLoadVPNConfigurations(tunnels: tunnels, completionHandler: completionHandler)
+ }
+ }
+ }
+ }
+
+ private func didLoadVPNConfigurations(tunnels: [TunnelProviderManagerType]?, completionHandler: @escaping CompletionHandler) {
+ if let tunnelProvider = tunnels?.first {
+ if let accountToken = accountToken {
+ // Case 1: tunnel exists and account token is set.
+ // Verify that tunnel can access the configuration via the persistent keychain reference
+ // stored in `passwordReference` field of VPN configuration.
+ handleTunnelConsistency(tunnelProvider: tunnelProvider, accountToken: accountToken, completionHandler: completionHandler)
+ } else {
+ // Case 2: tunnel exists but account token is unset.
+ // Remove the orphaned tunnel.
+ tunnelProvider.removeFromPreferences { error in
+ self.queue.async {
+ if let error = error {
+ completionHandler(.failure(.removeInconsistentVPNConfiguration(error)))
+ } else {
+ completionHandler(.success(()))
+ }
+ }
+ }
+ }
+ } else {
+ if let accountToken = accountToken {
+ // Case 3: tunnel does not exist but the account token is set.
+ // Verify that tunnel settings exists in keychain.
+ let tunnelSettingsResult = TunnelSettingsManager.load(searchTerm: .accountToken(accountToken))
+ .mapError { TunnelManager.Error.readTunnelSettings($0) }
+
+ if case .success(let keychainEntry) = tunnelSettingsResult {
+ let tunnelInfo = TunnelInfo(
+ token: keychainEntry.accountToken,
+ tunnelSettings: keychainEntry.tunnelSettings
+ )
+
+ state.tunnelInfo = tunnelInfo
+ }
+
+ completionHandler(OperationCompletion(result: tunnelSettingsResult.map { _ in () }))
+ } else {
+ // Case 4: no tunnels exist and account token is unset.
+ completionHandler(.success(()))
+ }
+ }
+ }
+
+ private func handleTunnelConsistency(tunnelProvider: TunnelProviderManagerType, accountToken: String, completionHandler: @escaping CompletionHandler) {
+ let verificationResult = verifyTunnel(tunnelProvider: tunnelProvider, expectedAccountToken: accountToken)
+ let tunnelSettingsResult = TunnelSettingsManager.load(searchTerm: .accountToken(accountToken))
+ .mapError { TunnelManager.Error.readTunnelSettings($0) }
+
+ switch (verificationResult, tunnelSettingsResult) {
+ case (.success(true), .success(let keychainEntry)):
+ let tunnelInfo = TunnelInfo(token: accountToken, tunnelSettings: keychainEntry.tunnelSettings)
+
+ state.tunnelInfo = tunnelInfo
+ state.setTunnelProvider(tunnelProvider, shouldRefreshTunnelState: true)
+
+ completionHandler(.success(()))
+
+ // Remove the tunnel with corrupt configuration.
+ // It will be re-created upon the first attempt to connect the tunnel.
+ case (.success(false), .success(let keychainEntry)):
+ tunnelProvider.removeFromPreferences { error in
+ self.queue.async {
+ if let error = error {
+ completionHandler(.failure(.removeInconsistentVPNConfiguration(error)))
+ } else {
+ let tunnelInfo = TunnelInfo(token: accountToken, tunnelSettings: keychainEntry.tunnelSettings)
+ self.state.tunnelInfo = tunnelInfo
+
+ completionHandler(.success(()))
+ }
+ }
+ }
+
+ // Remove the tunnel when failed to verify it but successfuly loaded the tunnel
+ // settings.
+ case (.failure(let verificationError), .success(let keychainEntry)):
+ logger.error(chainedError: verificationError, message: "Failed to verify the tunnel but successfully loaded the tunnel settings. Removing the tunnel.")
+
+ // Remove the tunnel with corrupt configuration.
+ // It will be re-created upon the first attempt to connect the tunnel.
+ tunnelProvider.removeFromPreferences { error in
+ self.queue.async {
+ if let error = error {
+ completionHandler(.failure(.removeInconsistentVPNConfiguration(error)))
+ } else {
+ let tunnelInfo = TunnelInfo(token: accountToken, tunnelSettings: keychainEntry.tunnelSettings)
+ self.state.tunnelInfo = tunnelInfo
+
+ completionHandler(.success(()))
+ }
+ }
+ }
+
+ // Remove the tunnel when failed to verify the tunnel and load tunnel settings.
+ case (.failure(let verificationError), .failure(_)):
+ logger.error(chainedError: verificationError, message: "Failed to verify the tunnel and load tunnel settings. Removing the tunnel.")
+
+ tunnelProvider.removeFromPreferences { error in
+ self.queue.async {
+ if let error = error {
+ completionHandler(.failure(.removeInconsistentVPNConfiguration(error)))
+ } else {
+ completionHandler(.failure(verificationError))
+ }
+ }
+ }
+
+ // Remove the tunnel when the app is not able to read tunnel settings
+ case (.success(_), .failure(let settingsReadError)):
+ logger.error(chainedError: settingsReadError, message: "Failed to load tunnel settings. Removing the tunnel.")
+
+ tunnelProvider.removeFromPreferences { error in
+ self.queue.async {
+ if let error = error {
+ completionHandler(.failure(.removeInconsistentVPNConfiguration(error)))
+ } else {
+ completionHandler(.failure(settingsReadError))
+ }
+ }
+ }
+ }
+ }
+
+ private func verifyTunnel(tunnelProvider: TunnelProviderManagerType, expectedAccountToken accountToken: String) -> Result<Bool, TunnelManager.Error> {
+ // Check that the VPN configuration points to the same account token
+ guard let username = tunnelProvider.protocolConfiguration?.username, username == accountToken else {
+ logger.warning("The token assigned to the VPN configuration does not match the logged in account.")
+ return .success(false)
+ }
+
+ // Check that the passwordReference, containing the keychain reference for tunnel
+ // configuration, is set.
+ guard let keychainReference = tunnelProvider.protocolConfiguration?.passwordReference else {
+ logger.warning("VPN configuration is missing the passwordReference.")
+ return .success(false)
+ }
+
+ // Verify that the keychain reference points to the existing entry in Keychain.
+ // Bad reference is possible when migrating the user data from one device to the other.
+ return TunnelSettingsManager.exists(searchTerm: .persistentReference(keychainReference))
+ .mapError { (error) -> TunnelManager.Error in
+ logger.error(chainedError: error, message: "Failed to verify the persistent keychain reference for tunnel settings.")
+
+ return .readTunnelSettings(error)
+ }
+ }
+
+ private func migrateTunnelSettings(accountToken: String) -> Result<Bool, TunnelManager.Error> {
+ let result = TunnelSettingsManager
+ .migrateKeychainEntry(searchTerm: .accountToken(accountToken))
+ .mapError { (error) -> TunnelManager.Error in
+ return .migrateTunnelSettings(error)
+ }
+
+ switch result {
+ case .success(let migrated):
+ if migrated {
+ logger.info("Migrated Keychain tunnel configuration.")
+ } else {
+ logger.info("Tunnel settings are up to date. No migration needed.")
+ }
+
+ case .failure(let error):
+ logger.error(chainedError: error)
+ }
+
+ return result
+ }
+}
diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
new file mode 100644
index 0000000000..88002f7212
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
@@ -0,0 +1,141 @@
+//
+// MapConnectionStatusOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 15/12/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import NetworkExtension
+import Logging
+
+class MapConnectionStatusOperation: AsyncOperation {
+ typealias StartTunnelHandler = () -> Void
+
+ private let queue: DispatchQueue
+ private let state: TunnelManager.State
+ private let connectionStatus: NEVPNStatus
+ private var startTunnelHandler: StartTunnelHandler?
+
+ private let logger = Logger(label: "TunnelManager.MapConnectionStatusOperation")
+
+ init(queue: DispatchQueue, state: TunnelManager.State, connectionStatus: NEVPNStatus, startTunnelHandler: @escaping StartTunnelHandler) {
+ self.queue = queue
+ self.state = state
+ self.connectionStatus = connectionStatus
+ self.startTunnelHandler = startTunnelHandler
+ }
+
+ override func main() {
+ queue.async {
+ self.execute()
+ }
+ }
+
+ override func cancel() {
+ super.cancel()
+
+ queue.async {
+ // Finish immediately if cancelled during execution.
+ if self.isExecuting {
+ self.finish()
+ }
+ }
+ }
+
+ private func execute() {
+ guard let tunnelProvider = state.tunnelProvider, !isCancelled else {
+ finish()
+ return
+ }
+
+ let tunnelState = state.tunnelState
+
+ switch connectionStatus {
+ case .connecting:
+ switch tunnelState {
+ case .connecting(.some(_)):
+ logger.debug("Ignore repeating connecting state.")
+ default:
+ state.tunnelState = .connecting(nil)
+ }
+
+ case .reasserting:
+ requestTunnelRelay(from: tunnelProvider) { [weak self] result in
+ guard let self = self else { return }
+
+ if case .success(.some(let connectionInfo)) = result, !self.isCancelled {
+ self.state.tunnelState = .reconnecting(connectionInfo)
+ }
+
+ self.finish()
+ }
+
+ return
+
+ case .connected:
+ requestTunnelRelay(from: tunnelProvider) { [weak self] result in
+ guard let self = self else { return }
+
+ if case .success(.some(let connectionInfo)) = result, !self.isCancelled {
+ self.state.tunnelState = .connected(connectionInfo)
+ }
+
+ self.finish()
+ }
+
+ return
+
+ case .disconnected:
+ switch tunnelState {
+ case .pendingReconnect:
+ logger.debug("Ignore disconnected state when pending reconnect.")
+
+ case .disconnecting(.reconnect):
+ logger.debug("Restart the tunnel on disconnect.")
+
+ state.tunnelState = .pendingReconnect
+
+ startTunnelHandler?()
+ startTunnelHandler = nil
+
+ default:
+ state.tunnelState = .disconnected
+ }
+
+ case .disconnecting:
+ switch tunnelState {
+ case .disconnecting:
+ break
+ default:
+ state.tunnelState = .disconnecting(.nothing)
+ }
+
+ case .invalid:
+ state.tunnelState = .disconnected
+
+ @unknown default:
+ logger.debug("Unknown NEVPNStatus: \(connectionStatus.rawValue)")
+ }
+
+ finish()
+ }
+
+ private func requestTunnelRelay(from tunnelProvider: TunnelProviderManagerType, completionHandler: @escaping (Result<TunnelConnectionInfo?, TunnelIPC.Error>?) -> Void) {
+ guard tunnelProvider.connection.status == .reasserting || tunnelProvider.connection.status == .connected else {
+ completionHandler(nil)
+ return
+ }
+
+ let ipcSession = TunnelIPC.Session(from: tunnelProvider)
+
+ ipcSession.getTunnelConnectionInfo { [weak self] result in
+ guard let self = self else { return }
+
+ self.queue.async {
+ completionHandler(result)
+ }
+ }
+ }
+}
diff --git a/ios/MullvadVPN/TunnelManager/RegenerateTunnelPrivateKeyOperation.swift b/ios/MullvadVPN/TunnelManager/RegenerateTunnelPrivateKeyOperation.swift
new file mode 100644
index 0000000000..8d26fa6b05
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/RegenerateTunnelPrivateKeyOperation.swift
@@ -0,0 +1,98 @@
+//
+// RegeneratePrivateKeyOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 15/12/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+class RegeneratePrivateKeyOperation: AsyncOperation {
+ typealias CompletionHandler = (OperationCompletion<(), TunnelManager.Error>) -> Void
+
+ private let queue: DispatchQueue
+ private let state: TunnelManager.State
+ private let restClient: REST.Client
+ private var completionHandler: CompletionHandler?
+ private var restRequest: Cancellable?
+
+ init(queue: DispatchQueue, state: TunnelManager.State, restClient: REST.Client, completionHandler: @escaping CompletionHandler) {
+ self.queue = queue
+ self.state = state
+ self.restClient = restClient
+ self.completionHandler = completionHandler
+ }
+
+ override func main() {
+ queue.async {
+ self.execute { [weak self] completion in
+ guard let self = self else { return }
+
+ self.completionHandler?(completion)
+ self.completionHandler = nil
+
+ self.finish()
+ }
+ }
+ }
+
+ override func cancel() {
+ super.cancel()
+
+ queue.async {
+ self.restRequest?.cancel()
+ }
+ }
+
+ private func execute(completionHandler: @escaping CompletionHandler) {
+ guard !self.isCancelled else {
+ completionHandler(.cancelled)
+ return
+ }
+
+ guard let tunnelInfo = state.tunnelInfo else {
+ completionHandler(.failure(.missingAccount))
+ return
+ }
+
+ let newPrivateKey = PrivateKeyWithMetadata()
+ let oldPublicKey = tunnelInfo.tunnelSettings.interface.publicKey
+
+ let restRequestAdapter = self.restClient.replaceWireguardKey(
+ token: tunnelInfo.token,
+ oldPublicKey: oldPublicKey,
+ newPublicKey: newPrivateKey.publicKey
+ )
+
+ restRequest = restRequestAdapter.execute(retryStrategy: .default) { restResult in
+ self.queue.async {
+ let saveResult = Self.handleResponse(accountToken: tunnelInfo.token, newPrivateKey: newPrivateKey, result: restResult)
+
+ if case .success(let newTunnelSettings) = saveResult {
+ self.state.tunnelInfo?.tunnelSettings = newTunnelSettings
+ }
+
+ completionHandler(OperationCompletion(result: saveResult.map { _ in () }))
+ }
+ }
+ }
+
+ private class func handleResponse(accountToken: String, newPrivateKey: PrivateKeyWithMetadata, result: Result<REST.WireguardAddressesResponse, REST.Error>) -> Result<TunnelSettings, TunnelManager.Error> {
+ return result.flatMapError { restError in
+ return .failure(.replaceWireguardKey(restError))
+ }
+ .flatMap { associatedAddresses in
+ return TunnelSettingsManager.update(searchTerm: .accountToken(accountToken)) { newTunnelSettings in
+ newTunnelSettings.interface.privateKey = newPrivateKey
+ newTunnelSettings.interface.addresses = [
+ associatedAddresses.ipv4Address,
+ associatedAddresses.ipv6Address
+ ]
+ }.mapError { error -> TunnelManager.Error in
+ return .updateTunnelSettings(error)
+ }
+ }
+ }
+
+}
diff --git a/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift
new file mode 100644
index 0000000000..c1740c50d3
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/ReloadTunnelOperation.swift
@@ -0,0 +1,122 @@
+//
+// ReloadTunnelOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 10/12/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import NetworkExtension
+
+class ReloadTunnelOperation: AsyncOperation {
+ typealias CompletionHandler = (OperationCompletion<(), TunnelManager.Error>) -> Void
+
+ private let queue: DispatchQueue
+ private let state: TunnelManager.State
+ private var completionHandler: CompletionHandler?
+ private var statusObserver: NSObjectProtocol?
+
+ init(queue: DispatchQueue, state: TunnelManager.State, completionHandler: @escaping CompletionHandler) {
+ self.queue = queue
+ self.state = state
+ self.completionHandler = completionHandler
+ }
+
+ override func main() {
+ queue.async {
+ self.execute { [weak self] completion in
+ self?.completeOperation(completion: completion)
+ }
+ }
+ }
+
+ override func cancel() {
+ super.cancel()
+
+ queue.async {
+ self.removeStatusObserver()
+
+ if self.isExecuting {
+ self.completeOperation(completion: .cancelled)
+ }
+ }
+ }
+
+ private func completeOperation(completion: OperationCompletion<(), TunnelManager.Error>) {
+ completionHandler?(completion)
+ completionHandler = nil
+
+ finish()
+ }
+
+ private func execute(completionHandler: @escaping CompletionHandler) {
+ guard !isCancelled else {
+ completionHandler(.cancelled)
+ return
+ }
+
+ guard let tunnelProvider = self.state.tunnelProvider else {
+ completionHandler(.failure(.missingAccount))
+ return
+ }
+
+ let ipcSession = TunnelIPC.Session(from: tunnelProvider)
+
+ // Add observer
+ statusObserver = NotificationCenter.default.addObserver(
+ forName: .NEVPNStatusDidChange,
+ object: tunnelProvider.connection,
+ queue: nil) { [weak self] notification in
+ guard let self = self else { return }
+ guard let connection = notification.object as? VPNConnectionProtocol else { return }
+
+ self.queue.async {
+ self.handleStatus(connection.status, ipcSession: ipcSession, completionHandler: completionHandler)
+ }
+ }
+
+ // Run initial check
+ handleStatus(tunnelProvider.connection.status, ipcSession: ipcSession, completionHandler: completionHandler)
+ }
+
+ private func handleStatus(_ status: NEVPNStatus, ipcSession: TunnelIPC.Session, completionHandler: @escaping CompletionHandler) {
+ guard !isCancelled else {
+ completionHandler(.cancelled)
+ return
+ }
+
+ switch status {
+ case .connected:
+ removeStatusObserver()
+
+ ipcSession.reloadTunnelSettings { [weak self] error in
+ guard let self = self else { return }
+
+ self.queue.async {
+ completionHandler(error.map { .failure(.reloadTunnel($0)) } ?? .success(()))
+ }
+ }
+
+ case .connecting, .reasserting:
+ // wait for transition to complete
+ break
+
+ case .invalid, .disconnecting, .disconnected:
+ removeStatusObserver()
+ completionHandler(.success(()))
+
+ @unknown default:
+ break
+ }
+ }
+
+ private func removeStatusObserver() {
+ if let statusObserver = statusObserver {
+ NotificationCenter.default.removeObserver(statusObserver)
+
+ self.statusObserver = nil
+ }
+ }
+
+}
diff --git a/ios/MullvadVPN/TunnelManager/RotatePrivateKeyOperation.swift b/ios/MullvadVPN/TunnelManager/RotatePrivateKeyOperation.swift
new file mode 100644
index 0000000000..1ec6d5cc2a
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/RotatePrivateKeyOperation.swift
@@ -0,0 +1,123 @@
+//
+// RotatePrivateKeyOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 15/12/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+class RotatePrivateKeyOperation: AsyncOperation {
+ typealias CompletionHandler = (OperationCompletion<TunnelManager.KeyRotationResult, TunnelManager.Error>) -> Void
+
+ private let queue: DispatchQueue
+ private let state: TunnelManager.State
+ private let restClient: REST.Client
+ private let rotationInterval: TimeInterval
+ private var completionHandler: CompletionHandler?
+ private var restRequest: Cancellable?
+
+ init(queue: DispatchQueue,
+ state: TunnelManager.State,
+ restClient: REST.Client,
+ rotationInterval: TimeInterval,
+ completionHandler: @escaping CompletionHandler)
+ {
+ self.queue = queue
+ self.state = state
+ self.restClient = restClient
+ self.rotationInterval = rotationInterval
+ self.completionHandler = completionHandler
+ }
+
+ override func main() {
+ queue.async {
+ self.execute { completion in
+ self.completionHandler?(completion)
+ self.completionHandler = nil
+
+ self.finish()
+ }
+ }
+ }
+
+ override func cancel() {
+ super.cancel()
+
+ queue.async {
+ self.restRequest?.cancel()
+ }
+ }
+
+ private func execute(completionHandler: @escaping CompletionHandler) {
+ guard !isCancelled else {
+ completionHandler(.cancelled)
+ return
+ }
+
+ guard let tunnelInfo = state.tunnelInfo else {
+ completionHandler(.failure(.missingAccount))
+ return
+ }
+
+ let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate
+ let timeInterval = Date().timeIntervalSince(creationDate)
+
+ guard timeInterval >= rotationInterval else {
+ completionHandler(.success(.throttled(creationDate)))
+ return
+ }
+
+ let newPrivateKey = PrivateKeyWithMetadata()
+ let oldPublicKey = tunnelInfo.tunnelSettings.interface.publicKey
+
+ let requestAdapter = self.restClient.replaceWireguardKey(
+ token: tunnelInfo.token,
+ oldPublicKey: oldPublicKey,
+ newPublicKey: newPrivateKey.publicKey
+ )
+
+ restRequest = requestAdapter.execute(retryStrategy: .default) { result in
+ self.queue.async {
+ self.didRotatePrivateKey(
+ result: result,
+ accountToken: tunnelInfo.token,
+ newPrivateKey: newPrivateKey,
+ completionHandler: completionHandler
+ )
+ }
+ }
+ }
+
+ private func didRotatePrivateKey(result: Result<REST.WireguardAddressesResponse, REST.Error>, accountToken: String, newPrivateKey: PrivateKeyWithMetadata, completionHandler: @escaping CompletionHandler) {
+ let saveResult = Self.handleResponse(accountToken: accountToken, newPrivateKey: newPrivateKey, result: result)
+
+ switch saveResult {
+ case .success(let tunnelSettings):
+ state.tunnelInfo?.tunnelSettings = tunnelSettings
+
+ completionHandler(.success(.finished))
+
+ case .failure(let error):
+ completionHandler(.failure(error))
+ }
+ }
+
+ private class func handleResponse(accountToken: String, newPrivateKey: PrivateKeyWithMetadata, result: Result<REST.WireguardAddressesResponse, REST.Error>) -> Result<TunnelSettings, TunnelManager.Error> {
+ return result.flatMapError { restError in
+ return .failure(.replaceWireguardKey(restError))
+ }
+ .flatMap { associatedAddresses in
+ return TunnelSettingsManager.update(searchTerm: .accountToken(accountToken)) { newTunnelSettings in
+ newTunnelSettings.interface.privateKey = newPrivateKey
+ newTunnelSettings.interface.addresses = [
+ associatedAddresses.ipv4Address,
+ associatedAddresses.ipv6Address
+ ]
+ }.mapError { error -> TunnelManager.Error in
+ return .updateTunnelSettings(error)
+ }
+ }
+ }
+}
diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
new file mode 100644
index 0000000000..27843d3442
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift
@@ -0,0 +1,233 @@
+//
+// SetAccountOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 16/12/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import class WireGuardKit.PublicKey
+import Logging
+
+class SetAccountOperation: AsyncOperation {
+ typealias WillDeleteVPNConfigurationHandler = () -> Void
+ typealias CompletionHandler = (OperationCompletion<(), TunnelManager.Error>) -> Void
+
+ private let queue: DispatchQueue
+ private let state: TunnelManager.State
+ private let restClient: REST.Client
+ private let accountToken: String?
+
+ private var willDeleteVPNConfigurationHandler: WillDeleteVPNConfigurationHandler?
+ private var completionHandler: CompletionHandler?
+
+ private let logger = Logger(label: "TunnelManager.SetAccountOperation")
+
+ init(queue: DispatchQueue, state: TunnelManager.State, restClient: REST.Client, accountToken: String?, willDeleteVPNConfigurationHandler: @escaping WillDeleteVPNConfigurationHandler, completionHandler: @escaping CompletionHandler) {
+ self.queue = queue
+ self.state = state
+ self.restClient = restClient
+ self.accountToken = accountToken
+ self.willDeleteVPNConfigurationHandler = willDeleteVPNConfigurationHandler
+ self.completionHandler = completionHandler
+ }
+
+ override func main() {
+ queue.async {
+ self.execute { completion in
+ self.completionHandler?(completion)
+ self.completionHandler = nil
+
+ self.finish()
+ }
+ }
+ }
+
+ private func execute(completionHandler: @escaping CompletionHandler) {
+ guard !isCancelled else {
+ completionHandler(.cancelled)
+ return
+ }
+
+ // Delete current account key and configuration if set.
+ if let tunnelInfo = state.tunnelInfo, tunnelInfo.token != accountToken {
+ let currentAccountToken = tunnelInfo.token
+ let currentPublicKey = tunnelInfo.tunnelSettings.interface.publicKey
+
+ logger.debug("Unset current account token.")
+
+ deleteOldAccount(accountToken: currentAccountToken, publicKey: currentPublicKey) {
+ self.setNewAccount(completionHandler: completionHandler)
+ }
+ } else {
+ setNewAccount(completionHandler: completionHandler)
+ }
+ }
+
+ private func setNewAccount(completionHandler: @escaping CompletionHandler) {
+ guard let accountToken = accountToken else {
+ logger.debug("Account token is unset.")
+ completionHandler(.success(()))
+ return
+ }
+
+ logger.debug("Set new account token.")
+
+ switch makeTunnelSettings(accountToken: accountToken) {
+ case .success(let tunnelSettings):
+ let interfaceSettings = tunnelSettings.interface
+
+ // Push key if interface addresses were not received yet
+ if interfaceSettings.addresses.isEmpty {
+ pushNewAccountKey(
+ accountToken: accountToken,
+ publicKey: interfaceSettings.publicKey,
+ completionHandler: completionHandler
+ )
+ } else {
+ state.tunnelInfo = TunnelInfo(
+ token: accountToken,
+ tunnelSettings: tunnelSettings
+ )
+ completionHandler(.success(()))
+ }
+
+ case .failure(let error):
+ logger.error(chainedError: error, message: "Failed to make new account settings.")
+ completionHandler(.failure(error))
+ }
+ }
+
+ private func makeTunnelSettings(accountToken: String) -> Result<TunnelSettings, TunnelManager.Error> {
+ return TunnelSettingsManager.load(searchTerm: .accountToken(accountToken))
+ .mapError { TunnelManager.Error.readTunnelSettings($0) }
+ .map { $0.tunnelSettings }
+ .flatMapError { error in
+ if case .readTunnelSettings(.lookupEntry(.itemNotFound)) = error {
+ let defaultConfiguration = TunnelSettings()
+
+ return TunnelSettingsManager
+ .add(configuration: defaultConfiguration, account: accountToken)
+ .mapError { .addTunnelSettings($0) }
+ .map { defaultConfiguration }
+ } else {
+ return .failure(error)
+ }
+ }
+ }
+
+ private func deleteOldAccount(accountToken: String, publicKey: PublicKey, completionHandler: @escaping () -> Void) {
+ _ = REST.Client.shared.deleteWireguardKey(token: accountToken, publicKey: publicKey)
+ .execute(retryStrategy: .default) { result in
+ self.queue.async {
+ self.didDeleteOldAccountKey(result: result, accountToken: accountToken, completionHandler: completionHandler)
+ }
+ }
+ }
+
+ private func didDeleteOldAccountKey(result: Result<(), REST.Error>, accountToken: String, completionHandler: @escaping () -> Void) {
+ switch result {
+ case .success:
+ logger.info("Removed old key from server.")
+
+ case .failure(let error):
+ if case .server(.pubKeyNotFound) = error {
+ logger.debug("Old key was not found on server.")
+ } else {
+ logger.error(chainedError: error, message: "Failed to delete old key on server.")
+ }
+ }
+
+ // Tell the caller to unsubscribe from VPN status notifications.
+ willDeleteVPNConfigurationHandler?()
+ willDeleteVPNConfigurationHandler = nil
+
+ // Reset tunnel state to disconnected
+ state.tunnelState = .disconnected
+
+ // Remove tunnel info
+ state.tunnelInfo = nil
+
+ // Remove settings from Keychain
+ if case .failure(let error) = TunnelSettingsManager.remove(searchTerm: .accountToken(accountToken)) {
+ // Ignore Keychain errors because that normally means that the Keychain
+ // configuration was already removed and we shouldn't be blocking the
+ // user from logging out
+ logger.error(
+ chainedError: error,
+ message: "Failed to delete old account settings."
+ )
+ }
+
+ // Finish immediately if tunnel provider is not set.
+ guard let tunnelProvider = state.tunnelProvider else {
+ completionHandler()
+ return
+ }
+
+ // Remove VPN configuration
+ tunnelProvider.removeFromPreferences { error in
+ self.queue.async {
+ if let error = error {
+ // Ignore error but log it
+ self.logger.error(
+ chainedError: AnyChainedError(error),
+ message: "Failed to remove VPN configuration."
+ )
+ } else {
+ self.state.setTunnelProvider(nil, shouldRefreshTunnelState: false)
+ }
+
+ completionHandler()
+ }
+ }
+ }
+
+ private func pushNewAccountKey(accountToken: String, publicKey: PublicKey, completionHandler: @escaping CompletionHandler) {
+ _ = restClient.pushWireguardKey(token: accountToken, publicKey: publicKey)
+ .execute(retryStrategy: .default) { result in
+ self.queue.async {
+ self.didPushNewAccountKey(result: result, accountToken: accountToken, completionHandler: completionHandler)
+ }
+ }
+ }
+
+ private func didPushNewAccountKey(result: Result<REST.WireguardAddressesResponse, REST.Error>, accountToken: String, completionHandler: @escaping (OperationCompletion<(), TunnelManager.Error>) -> Void) {
+ switch result {
+ case .success(let associatedAddresses):
+ logger.debug("Pushed new key to server.")
+
+ let saveSettingsResult = TunnelSettingsManager.update(searchTerm: .accountToken(accountToken)) { tunnelSettings in
+ tunnelSettings.interface.addresses = [
+ associatedAddresses.ipv4Address,
+ associatedAddresses.ipv6Address
+ ]
+ }
+
+ switch saveSettingsResult {
+ case .success(let newTunnelSettings):
+ logger.debug("Saved associated addresses.")
+
+ let tunnelInfo = TunnelInfo(
+ token: accountToken,
+ tunnelSettings: newTunnelSettings
+ )
+
+ state.tunnelInfo = tunnelInfo
+
+ completionHandler(.success(()))
+
+ case .failure(let error):
+ logger.error(chainedError: error, message: "Failed to save associated addresses.")
+
+ completionHandler(.failure(.updateTunnelSettings(error)))
+ }
+
+ case .failure(let error):
+ logger.error(chainedError: error, message: "Failed to push new key to server.")
+
+ completionHandler(.failure(.pushWireguardKey(error)))
+ }
+ }
+}
diff --git a/ios/MullvadVPN/TunnelManager/SetTunnelSettingsOperation.swift b/ios/MullvadVPN/TunnelManager/SetTunnelSettingsOperation.swift
new file mode 100644
index 0000000000..52b71538c8
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/SetTunnelSettingsOperation.swift
@@ -0,0 +1,64 @@
+//
+// SetTunnelSettingsOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 16/12/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+class SetTunnelSettingsOperation: AsyncOperation {
+ typealias ModificationHandler = (inout TunnelSettings) -> Void
+ typealias CompletionHandler = (OperationCompletion<(), TunnelManager.Error>) -> Void
+
+ private let queue: DispatchQueue
+ private let state: TunnelManager.State
+ private let modificationBlock: ModificationHandler
+ private var completionHandler: CompletionHandler?
+
+ init(queue: DispatchQueue, state: TunnelManager.State, modificationBlock: @escaping ModificationHandler, completionHandler: @escaping CompletionHandler) {
+ self.queue = queue
+ self.state = state
+ self.modificationBlock = modificationBlock
+ self.completionHandler = completionHandler
+ }
+
+ override func main() {
+ queue.async {
+ self.execute { completion in
+ self.completionHandler?(completion)
+ self.completionHandler = nil
+
+ self.finish()
+ }
+ }
+ }
+
+ private func execute(completionHandler: CompletionHandler) {
+ guard !isCancelled else {
+ completionHandler(.cancelled)
+ return
+ }
+
+ guard let accountToken = state.tunnelInfo?.token else {
+ completionHandler(.failure(.missingAccount))
+ return
+ }
+
+ let result = TunnelSettingsManager.update(searchTerm: .accountToken(accountToken)) { tunnelSettings in
+ self.modificationBlock(&tunnelSettings)
+ }
+
+ switch result {
+ case .success(let newTunnelSettings):
+ state.tunnelInfo?.tunnelSettings = newTunnelSettings
+
+ completionHandler(.success(()))
+
+ case .failure(let error):
+ completionHandler(.failure(.updateTunnelSettings(error)))
+
+ }
+ }
+}
diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
new file mode 100644
index 0000000000..21b59bea5f
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
@@ -0,0 +1,190 @@
+//
+// StartTunnelOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 15/12/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import NetworkExtension
+
+class StartTunnelOperation: AsyncOperation {
+ typealias EncodeErrorHandler = (Error) -> Void
+ typealias CompletionHandler = (OperationCompletion<(), TunnelManager.Error>) -> Void
+
+ private let queue: DispatchQueue
+ private let state: TunnelManager.State
+
+ private var encodeErrorHandler: EncodeErrorHandler?
+ private var completionHandler: CompletionHandler?
+
+ init(queue: DispatchQueue, state: TunnelManager.State, encodeErrorHandler: @escaping EncodeErrorHandler, completionHandler: @escaping CompletionHandler) {
+ self.queue = queue
+ self.state = state
+ self.encodeErrorHandler = encodeErrorHandler
+ self.completionHandler = completionHandler
+ }
+
+ override func main() {
+ queue.async {
+ self.execute { completion in
+ self.completionHandler?(completion)
+ self.completionHandler = nil
+
+ self.finish()
+ }
+ }
+ }
+
+ private func execute(completionHandler: @escaping CompletionHandler) {
+ guard !self.isCancelled else {
+ completionHandler(.cancelled)
+ return
+ }
+
+ guard let tunnelInfo = self.state.tunnelInfo else {
+ completionHandler(.failure(.missingAccount))
+ return
+ }
+
+ switch self.state.tunnelState {
+ case .disconnecting(.nothing):
+ self.state.tunnelState = .disconnecting(.reconnect)
+
+ completionHandler(.success(()))
+
+ case .disconnected, .pendingReconnect:
+ RelayCache.Tracker.shared.read { readResult in
+ self.queue.async {
+ switch readResult {
+ case .success(let cachedRelays):
+ self.didReceiveRelays(
+ tunnelInfo: tunnelInfo,
+ cachedRelays: cachedRelays,
+ completionHandler: completionHandler
+ )
+
+ case .failure(let error):
+ completionHandler(.failure(.readRelays(error)))
+ }
+ }
+ }
+
+ default:
+ // Do not attempt to start the tunnel in all other cases.
+ completionHandler(.success(()))
+ }
+ }
+
+ private func didReceiveRelays(tunnelInfo: TunnelInfo, cachedRelays: RelayCache.CachedRelays, completionHandler: @escaping (OperationCompletion<(), TunnelManager.Error>) -> Void) {
+ let selectorResult = RelaySelector.evaluate(
+ relays: cachedRelays.relays,
+ constraints: tunnelInfo.tunnelSettings.relayConstraints
+ )
+
+ guard let selectorResult = selectorResult else {
+ completionHandler(.failure(.cannotSatisfyRelayConstraints))
+ return
+ }
+
+ Self.makeTunnelProvider(accountToken: tunnelInfo.token) { makeTunnelProviderResult in
+ self.queue.async {
+ switch makeTunnelProviderResult {
+ case .success(let tunnelProvider):
+ let startTunnelResult = Result { try self.startTunnel(tunnelProvider: tunnelProvider, selectorResult: selectorResult) }
+
+ completionHandler(OperationCompletion(result: startTunnelResult.mapError { .startVPNTunnel($0) }))
+
+ case .failure(let error):
+ completionHandler(.failure(error))
+ }
+ }
+ }
+ }
+
+ private func startTunnel(tunnelProvider: TunnelProviderManagerType, selectorResult: RelaySelectorResult) throws {
+ var tunnelOptions = PacketTunnelOptions()
+
+ do {
+ try tunnelOptions.setSelectorResult(selectorResult)
+ } catch {
+ encodeErrorHandler?(error)
+ }
+
+ encodeErrorHandler = nil
+
+ state.setTunnelProvider(tunnelProvider, shouldRefreshTunnelState: false)
+ state.tunnelState = .connecting(selectorResult.tunnelConnectionInfo)
+
+ try tunnelProvider.connection.startVPNTunnel(options: tunnelOptions.rawOptions())
+ }
+
+ private class func makeTunnelProvider(accountToken: String, completionHandler: @escaping (Result<TunnelProviderManagerType, TunnelManager.Error>) -> Void) {
+ TunnelProviderManagerType.loadAllFromPreferences { tunnelProviders, error in
+ if let error = error {
+ completionHandler(.failure(.loadAllVPNConfigurations(error)))
+ return
+ }
+
+ let result = Self.setupTunnelProvider(
+ accountToken: accountToken,
+ tunnels: tunnelProviders
+ )
+
+ guard case .success(let tunnelProvider) = result else {
+ completionHandler(result)
+ return
+ }
+
+ 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))
+ }
+ }
+ }
+ }
+ }
+
+ private class func setupTunnelProvider(accountToken: String, tunnels: [TunnelProviderManagerType]?) -> Result<TunnelProviderManagerType, TunnelManager.Error> {
+ // Request persistent keychain reference to tunnel settings
+ return TunnelSettingsManager.getPersistentKeychainReference(account: accountToken)
+ .mapError { error in
+ return .obtainPersistentKeychainReference(error)
+ }
+ .map { passwordReference in
+ // Get the first available tunnel or make a new one
+ let tunnelProvider = tunnels?.first ?? TunnelProviderManagerType()
+
+ let protocolConfig = NETunnelProviderProtocol()
+ protocolConfig.providerBundleIdentifier = ApplicationConfiguration.packetTunnelExtensionIdentifier
+ protocolConfig.serverAddress = ""
+ protocolConfig.username = accountToken
+ protocolConfig.passwordReference = passwordReference
+
+ tunnelProvider.isEnabled = true
+ tunnelProvider.localizedDescription = "WireGuard"
+ tunnelProvider.protocolConfiguration = protocolConfig
+
+ // Enable on-demand VPN, always connect the tunnel when on Wi-Fi or cellular
+ let alwaysOnRule = NEOnDemandRuleConnect()
+ alwaysOnRule.interfaceTypeMatch = .any
+ tunnelProvider.onDemandRules = [alwaysOnRule]
+ tunnelProvider.isOnDemandEnabled = true
+
+ return tunnelProvider
+ }
+ }
+}
diff --git a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
new file mode 100644
index 0000000000..1b06a6f68b
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
@@ -0,0 +1,71 @@
+//
+// StopTunnelOperation.swift
+// MullvadVPN
+//
+// Created by pronebird on 15/12/2021.
+// Copyright © 2021 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+
+class StopTunnelOperation: AsyncOperation {
+ typealias CompletionHandler = (OperationCompletion<(), TunnelManager.Error>) -> Void
+
+ private let queue: DispatchQueue
+ private let state: TunnelManager.State
+ private var completionHandler: CompletionHandler?
+
+ init(queue: DispatchQueue, state: TunnelManager.State, completionHandler: @escaping CompletionHandler) {
+ self.queue = queue
+ self.state = state
+ self.completionHandler = completionHandler
+ }
+
+ override func main() {
+ queue.async {
+ self.execute { completion in
+ self.completionHandler?(completion)
+ self.completionHandler = nil
+
+ self.finish()
+ }
+ }
+ }
+
+ private func execute(completionHandler: @escaping CompletionHandler) {
+ guard !isCancelled else {
+ completionHandler(.cancelled)
+ return
+ }
+
+ guard let tunnelProvider = state.tunnelProvider else {
+ completionHandler(.failure(.missingAccount))
+ return
+ }
+
+ switch self.state.tunnelState {
+ case .disconnecting(.reconnect):
+ state.tunnelState = .disconnecting(.nothing)
+
+ completionHandler(.success(()))
+
+ case .connected, .connecting:
+ // Disable on-demand when stopping the tunnel to prevent it from coming back up
+ tunnelProvider.isOnDemandEnabled = false
+
+ tunnelProvider.saveToPreferences { error in
+ self.queue.async {
+ if let error = error {
+ completionHandler(.failure(.saveVPNConfiguration(error)))
+ } else {
+ tunnelProvider.connection.stopVPNTunnel()
+ completionHandler(.success(()))
+ }
+ }
+ }
+
+ default:
+ completionHandler(.success(()))
+ }
+ }
+}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelInfo.swift b/ios/MullvadVPN/TunnelManager/TunnelInfo.swift
index 5a22468518..e16ab22632 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelInfo.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelInfo.swift
@@ -9,7 +9,7 @@
import Foundation
/// Struct that holds current account token and tunnel settings.
-struct TunnelInfo {
+struct TunnelInfo: Equatable {
/// Mullvad account token
var token: String
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
index 528321ee7e..d0eecaa4df 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
@@ -15,7 +15,8 @@ import class WireGuardKit.PublicKey
/// A class that provides a convenient interface for VPN tunnels configuration, manipulation and
/// monitoring.
-class TunnelManager {
+class TunnelManager: TunnelManagerStateDelegate
+{
/// Private key rotation interval (in seconds)
private static let privateKeyRotationInterval: TimeInterval = 60 * 60 * 24 * 4
@@ -26,79 +27,40 @@ class TunnelManager {
private enum OperationCategory {
static let manageTunnelProvider = "TunnelManager.manageTunnelProvider"
static let changeTunnelSettings = "TunnelManager.changeTunnelSettings"
- static let notifyTunnelSettingsChange = "TunnelManager.notifyTunnelSettingsChange"
+ static let tunnelStateUpdate = "TunnelManager.tunnelStateUpdate"
}
- // Switch to stabs on simulator
- #if targetEnvironment(simulator)
- typealias TunnelProviderManagerType = SimulatorTunnelProviderManager
- #else
- typealias TunnelProviderManagerType = NETunnelProviderManager
- #endif
-
- static let shared = TunnelManager()
+ static let shared: TunnelManager = {
+ return TunnelManager(restClient: REST.Client.shared)
+ }()
// MARK: - Internal variables
+ private let restClient: REST.Client
+
private let logger = Logger(label: "TunnelManager")
private let stateQueue = DispatchQueue(label: "TunnelManager.stateQueue")
- private let operationQueue: OperationQueue = {
- let operationQueue = OperationQueue()
- operationQueue.name = "TunnelManager.operationQueue"
- return operationQueue
- }()
-
- private var tunnelProvider: TunnelProviderManagerType?
- private var ipcSession: TunnelIPC.Session?
- private var tunnelConnectionInfoToken: PromiseCancellationToken?
+ private let operationQueue = OperationQueue()
+ private let exclusivityController = ExclusivityController()
- private let stateLock = NSLock()
+ private var lastMapConnectionStatusOperation: Operation?
private let observerList = ObserverList<AnyTunnelObserver>()
- /// A VPN connection status observer
- private var connectionStatusObserver: NSObjectProtocol?
+ private let state: TunnelManager.State
- private(set) var tunnelInfo: TunnelInfo? {
- set {
- stateLock.withCriticalBlock {
- _tunnelInfo = newValue
- tunnelInfoDidChange(newValue)
- }
- }
- get {
- return stateLock.withCriticalBlock {
- return _tunnelInfo
- }
- }
+ var tunnelInfo: TunnelInfo? {
+ return state.tunnelInfo
}
- private var _tunnelInfo: TunnelInfo?
- private var _tunnelState = TunnelState.disconnected
-
- private(set) var tunnelState: TunnelState {
- set {
- stateLock.withCriticalBlock {
- guard _tunnelState != newValue else { return }
-
- logger.info("Set tunnel state: \(newValue)")
-
- _tunnelState = newValue
-
- DispatchQueue.main.async {
- self.observerList.forEach { (observer) in
- observer.tunnelManager(self, didUpdateTunnelState: newValue)
- }
- }
- }
- }
- get {
- return stateLock.withCriticalBlock {
- return _tunnelState
- }
- }
+ var tunnelState: TunnelState {
+ return state.tunnelState
}
- private init() {
+ private init(restClient: REST.Client) {
+ self.restClient = restClient
+ self.state = TunnelManager.State(queue: stateQueue)
+ self.state.delegate = self
+
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationDidBecomeActive),
@@ -142,7 +104,7 @@ class TunnelManager {
guard self.isRunningPeriodicPrivateKeyRotation else { return }
- if let tunnelInfo = self.tunnelInfo {
+ if let tunnelInfo = self.state.tunnelInfo {
let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate
let scheduleDate = Date(timeInterval: Self.privateKeyRotationInterval, since: creationDate)
@@ -157,27 +119,20 @@ class TunnelManager {
private func schedulePrivateKeyRotationTimer(_ scheduleDate: Date) {
dispatchPrecondition(condition: .onQueue(stateQueue))
- var cancellationToken: PromiseCancellationToken?
-
- let timer = DispatchSource.makeTimerSource(flags: [], queue: self.stateQueue)
+ let timer = DispatchSource.makeTimerSource(queue: stateQueue)
timer.setEventHandler { [weak self] in
guard let self = self else { return }
- self.rotatePrivateKey()
- .receive(on: self.stateQueue)
- .storeCancellationToken(in: &cancellationToken)
- .observe { completion in
- guard !completion.isCancelled else { return }
+ self.rotatePrivateKey { rotationResult, error in
+ self.stateQueue.async {
+ if let scheduleDate = self.handlePrivateKeyRotationCompletion(result: rotationResult, error: error) {
+ guard self.isRunningPeriodicPrivateKeyRotation else { return }
- if let scheduleDate = self.handlePrivateKeyRotationCompletion(completion: completion) {
self.schedulePrivateKeyRotationTimer(scheduleDate)
}
}
- }
-
- timer.setCancelHandler {
- cancellationToken?.cancel()
+ }
}
// Cancel active timer
@@ -190,7 +145,7 @@ class TunnelManager {
timer.schedule(wallDeadline: .now() + scheduleDate.timeIntervalSinceNow)
timer.activate()
- self.logger.debug("Schedule next private key rotation on \(scheduleDate.logFormatDate())")
+ logger.debug("Schedule next private key rotation on \(scheduleDate.logFormatDate())")
}
// MARK: - Public methods
@@ -199,736 +154,369 @@ class TunnelManager {
///
/// The given account token is used to ensure that the system tunnel was configured for the same
/// account. The system tunnel is removed in case of inconsistency.
- func loadTunnel(accountToken: String?) -> Result<(), TunnelManager.Error>.Promise {
- return TunnelProviderManagerType.loadAllFromPreferences()
- .receive(on: self.stateQueue)
- .mapError { error in
- return .loadAllVPNConfigurations(error)
- }.mapThen { tunnels in
- return self.initializeManager(accountToken: accountToken, tunnels: tunnels)
- .then { result -> Result<(), TunnelManager.Error> in
- self.updatePrivateKeyRotationTimer()
+ func loadTunnel(accountToken: String?, completionHandler: @escaping (TunnelManager.Error?) -> Void) {
+ let operation = LoadTunnelOperation(queue: stateQueue, state: state, accountToken: accountToken) { [weak self] completion in
+ guard let self = self else { return }
- return result
- }
+ dispatchPrecondition(condition: .onQueue(self.stateQueue))
+
+ if case .failure(let error) = completion {
+ self.logger.error(chainedError: error, message: "Failed to load tunnel")
}
- .schedule(on: stateQueue)
- .run(on: operationQueue, categories: [OperationCategory.manageTunnelProvider, OperationCategory.changeTunnelSettings])
- .requestBackgroundTime(taskName: "TunnelManager.loadAccount")
- .doNotPropagateCancellation()
- }
- func startTunnel() {
- Result<(), TunnelManager.Error>.Promise { resolver in
- guard let tunnelInfo = self.tunnelInfo else {
- resolver.resolve(value: .failure(.missingAccount))
- return
+ self.updatePrivateKeyRotationTimer()
+
+ DispatchQueue.main.async {
+ completionHandler(completion.error)
}
+ }
- switch self.tunnelState {
- case .disconnecting(.nothing):
- self.tunnelState = .disconnecting(.reconnect)
- resolver.resolve(value: .success(()))
+ let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Load tunnel") {
+ operation.cancel()
+ }
- case .disconnected, .pendingReconnect:
- RelayCache.Tracker.shared.read()
- .mapError { error in
- return .readRelays(error)
- }
- .receive(on: self.stateQueue)
- .flatMap { cachedRelays in
- return RelaySelector.evaluate(
- relays: cachedRelays.relays,
- constraints: tunnelInfo.tunnelSettings.relayConstraints
- ).map { .success($0) } ?? .failure(.cannotSatisfyRelayConstraints)
- }
- .mapThen { selectorResult in
- return self.makeTunnelProvider(accountToken: tunnelInfo.token)
- .receive(on: self.stateQueue)
- .flatMap { tunnelProvider in
- self.setTunnelProvider(tunnelProvider: tunnelProvider)
+ operation.completionBlock = {
+ UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
+ }
- var tunnelOptions = PacketTunnelOptions()
+ exclusivityController.addOperation(operation, categories: [OperationCategory.manageTunnelProvider, OperationCategory.changeTunnelSettings])
- _ = Result { try tunnelOptions.setSelectorResult(selectorResult) }
- .mapError { error -> Swift.Error in
- self.logger.error(chainedError: AnyChainedError(error), message: "Failed to encode relay selector result.")
- return error
- }
+ operationQueue.addOperation(operation)
+ }
- self.tunnelState = .connecting(selectorResult.tunnelConnectionInfo)
+ func startTunnel() {
+ let operation = StartTunnelOperation(
+ queue: stateQueue,
+ state: state,
+ encodeErrorHandler: { [weak self] error in
+ guard let self = self else { return }
- return Result { try tunnelProvider.connection.startVPNTunnel(options: tunnelOptions.rawOptions()) }
- .mapError { error in
- return .startVPNTunnel(error)
- }
- }
- }.observe { completion in
- resolver.resolve(completion: completion)
- }
+ dispatchPrecondition(condition: .onQueue(self.stateQueue))
- default:
- // Do not attempt to start the tunnel in all other cases.
- resolver.resolve(value: .success(()))
- }
+ self.logger.error(chainedError: AnyChainedError(error), message: "Failed to encode tunnel options")
+ },
+ 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)
+ }
+ })
+
+
+ let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Start tunnel") {
+ operation.cancel()
}
- .schedule(on: stateQueue)
- .run(on: operationQueue, categories: [OperationCategory.manageTunnelProvider])
- .requestBackgroundTime(taskName: "TunnelManager.startTunnel")
- .onFailure { error in
- self.sendFailureToObservers(error)
+
+ operation.completionBlock = {
+ UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
}
- .observe { _ in }
+
+ exclusivityController.addOperation(operation, categories: [OperationCategory.manageTunnelProvider])
+
+ operationQueue.addOperation(operation)
}
func stopTunnel() {
- Result<(), Error>.Promise { resolver in
- guard let tunnelProvider = self.tunnelProvider else {
- resolver.resolve(value: .failure(.missingAccount))
- return
- }
+ let operation = StopTunnelOperation(queue: stateQueue, state: state) { [weak self] completion in
+ guard let self = self else { return }
- switch self.tunnelState {
- case .disconnecting(.reconnect):
- self.tunnelState = .disconnecting(.nothing)
- resolver.resolve(value: .success(()))
+ dispatchPrecondition(condition: .onQueue(self.stateQueue))
- case .connected, .connecting:
- // Disable on-demand when stopping the tunnel to prevent it from coming back up
- tunnelProvider.isOnDemandEnabled = false
+ guard let error = completion.error else { return }
- tunnelProvider.saveToPreferences()
- .mapError { error in
- return Error.saveVPNConfiguration(error)
- }
- .observe { completion in
- tunnelProvider.connection.stopVPNTunnel()
- resolver.resolve(completion: completion)
- }
-
- default:
- resolver.resolve(value: .success(()))
+ // Pass tunnel failure to observers
+ DispatchQueue.main.async {
+ self.observerList.forEach { observer in
+ observer.tunnelManager(self, didFailWithError: error)
+ }
}
}
- .schedule(on: stateQueue)
- .run(on: operationQueue, categories: [OperationCategory.manageTunnelProvider])
- .requestBackgroundTime(taskName: "TunnelManager.stopTunnel")
- .onFailure { error in
- self.sendFailureToObservers(error)
+
+ let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Stop tunnel") {
+ operation.cancel()
}
- .observe { _ in }
- }
- func reconnectTunnel() {
- notifyTunnelOnSettingsChange().observe { _ in }
- }
+ operation.completionBlock = {
+ UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
+ }
- func setAccount(accountToken: String) -> Result<(), TunnelManager.Error>.Promise {
- return Promise.deferred { Self.makeTunnelSettings(accountToken: accountToken) }
- .mapThen { tunnelSettings -> Result<TunnelSettings, Error>.Promise in
- let interfaceSettings = tunnelSettings.interface
- guard interfaceSettings.addresses.isEmpty else {
- return .success(tunnelSettings)
- }
+ exclusivityController.addOperation(operation, categories: [OperationCategory.manageTunnelProvider])
- // Push wireguard key if addresses were not received yet
- return self.pushWireguardKeyAndUpdateSettings(accountToken: accountToken, publicKey: interfaceSettings.publicKey)
- }
- .receive(on: self.stateQueue)
- .onSuccess { tunnelSettings in
- self.tunnelInfo = TunnelInfo(token: accountToken, tunnelSettings: tunnelSettings)
- self.updatePrivateKeyRotationTimer()
- }
- .setOutput(())
- .schedule(on: stateQueue)
- .run(on: operationQueue, categories: [OperationCategory.manageTunnelProvider, OperationCategory.changeTunnelSettings])
- .requestBackgroundTime(taskName: "TunnelManager.setAccount")
- .doNotPropagateCancellation()
+ operationQueue.addOperation(operation)
}
- /// Remove the account token and remove the active tunnel
- func unsetAccount() -> Result<(), TunnelManager.Error>.Promise {
- return Promise.deferred { self.tunnelInfo }
- .some(or: Error.missingAccount)
- .mapThen { tunnelInfo in
- let publicKey = tunnelInfo.tunnelSettings.interface.publicKey
-
- return self.removeWireguardKeyFromServer(accountToken: tunnelInfo.token, publicKey: publicKey)
- .receive(on: self.stateQueue)
- .then { result -> Result<(), Error>.Promise in
- switch result {
- case .success(let isRemoved):
- self.logger.warning("Removed the WireGuard key from server: \(isRemoved)")
+ func reconnectTunnel(completionHandler: (() -> Void)?) {
+ let operation = ReloadTunnelOperation(queue: stateQueue, state: state) { [weak self] completion in
+ guard let self = self else { return }
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Unset account error")
- }
+ dispatchPrecondition(condition: .onQueue(self.stateQueue))
- // Unregister from receiving the tunnel state changes
- self.unregisterConnectionObserver()
- self.tunnelConnectionInfoToken = nil
- self.tunnelState = .disconnected
- self.ipcSession = nil
+ if let error = completion.error {
+ self.logger.error(chainedError: error)
+ }
- // Remove settings from Keychain
- if case .failure(let error) = TunnelSettingsManager.remove(searchTerm: .accountToken(tunnelInfo.token)) {
- // Ignore Keychain errors because that normally means that the Keychain
- // configuration was already removed and we shouldn't be blocking the
- // user from logging out
- self.logger.error(
- chainedError: error,
- message: "Failure to remove tunnel setting from keychain when unsetting user account"
- )
- }
+ DispatchQueue.main.async {
+ completionHandler?()
+ }
+ }
- self.tunnelInfo = nil
- self.updatePrivateKeyRotationTimer()
+ let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Reconnect tunnel") {
+ operation.cancel()
+ }
- guard let tunnelProvider = self.tunnelProvider else {
- return .success(())
- }
+ operation.completionBlock = {
+ UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
+ }
- self.tunnelProvider = nil
+ exclusivityController.addOperation(operation, categories: [OperationCategory.manageTunnelProvider])
- // Remove VPN configuration
- return tunnelProvider.removeFromPreferences()
- .flatMapError { error -> Result<(), Error> in
- // Ignore error but log it
- self.logger.error(
- chainedError: Error.removeVPNConfiguration(error),
- message: "Failure to remove system VPN configuration when unsetting user account."
- )
+ operationQueue.addOperation(operation)
+ }
- return .success(())
- }
- }
+ func setAccount(accountToken: String, completionHandler: @escaping (TunnelManager.Error?) -> Void) {
+ let operation = makeSetAccountOperation(accountToken: accountToken) { completion in
+ DispatchQueue.main.async {
+ completionHandler(completion.error)
}
- .schedule(on: stateQueue)
- .run(on: operationQueue, categories: [OperationCategory.manageTunnelProvider, OperationCategory.changeTunnelSettings])
- .requestBackgroundTime(taskName: "TunnelManager.unsetAccount")
- .doNotPropagateCancellation()
- }
+ }
- func regeneratePrivateKey() -> Result<(), TunnelManager.Error>.Promise {
- return Promise.deferred { self.tunnelInfo }
- .some(or: .missingAccount)
- .mapThen { tunnelInfo in
- let newPrivateKey = PrivateKeyWithMetadata()
- let oldPublicKeyMetadata = tunnelInfo.tunnelSettings.interface
- .privateKey
- .publicKeyWithMetadata
+ let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Set tunnel account") {
+ operation.cancel()
+ }
- return self.replaceWireguardKeyAndUpdateSettings(
- accountToken: tunnelInfo.token,
- oldPublicKey: oldPublicKeyMetadata,
- newPrivateKey: newPrivateKey
- ).onSuccess { newTunnelSettings in
- self.tunnelInfo?.tunnelSettings = newTunnelSettings
- self.updatePrivateKeyRotationTimer()
+ operation.completionBlock = {
+ UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
+ }
- self.notifyTunnelOnSettingsChange().observe { _ in }
- }
- .setOutput(())
- }
- .schedule(on: stateQueue)
- .run(on: operationQueue, categories: [OperationCategory.changeTunnelSettings])
- .requestBackgroundTime(taskName: "TunnelManager.regeneratePrivateKey")
- .doNotPropagateCancellation()
+ exclusivityController.addOperation(operation, categories: [OperationCategory.manageTunnelProvider, OperationCategory.changeTunnelSettings])
+
+ operationQueue.addOperation(operation)
}
- func rotatePrivateKey() -> Result<KeyRotationResult, TunnelManager.Error>.Promise {
- return Promise.deferred { self.tunnelInfo }
- .some(or: .missingAccount)
- .mapThen { tunnelInfo in
- let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate
- let timeInterval = Date().timeIntervalSince(creationDate)
+ func unsetAccount(completionHandler: @escaping () -> Void) {
+ let operation = makeSetAccountOperation(accountToken: nil) { _ in
+ DispatchQueue.main.async {
+ completionHandler()
+ }
+ }
- guard timeInterval >= Self.privateKeyRotationInterval else {
- return .success(.throttled(creationDate))
- }
+ let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Unset tunnel account") {
+ operation.cancel()
+ }
- let newPrivateKey = PrivateKeyWithMetadata()
- let oldPublicKeyMetadata = tunnelInfo.tunnelSettings.interface
- .privateKey
- .publicKeyWithMetadata
+ operation.completionBlock = {
+ UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
+ }
- return self.replaceWireguardKeyAndUpdateSettings(accountToken: tunnelInfo.token, oldPublicKey: oldPublicKeyMetadata, newPrivateKey: newPrivateKey)
- .onSuccess { newTunnelSettings in
- self.tunnelInfo?.tunnelSettings = newTunnelSettings
- }
- .mapThen { _ in
- return self.notifyTunnelOnSettingsChange().then { _ in
- return .success(.finished)
- }
- }
- }
- .schedule(on: stateQueue)
- .run(on: operationQueue, categories: [OperationCategory.changeTunnelSettings])
- .requestBackgroundTime(taskName: "TunnelManager.rotatePrivateKey")
- .doNotPropagateCancellation()
- }
+ exclusivityController.addOperation(operation, categories: [OperationCategory.manageTunnelProvider, OperationCategory.changeTunnelSettings])
- func setRelayConstraints(_ newConstraints: RelayConstraints) -> Result<(), TunnelManager.Error>.Promise {
- return Promise.deferred { self.tunnelInfo }
- .some(or: .missingAccount)
- .flatMap { tunnelInfo in
- return Self.updateTunnelSettings(accountToken: tunnelInfo.token) { tunnelSettings in
- tunnelSettings.relayConstraints = newConstraints
- }
- }
- .onSuccess { newTunnelSettings in
- self.tunnelInfo?.tunnelSettings = newTunnelSettings
- self.notifyTunnelOnSettingsChange().observe { _ in }
- }
- .setOutput(())
- .schedule(on: stateQueue)
- .run(on: operationQueue, categories: [OperationCategory.changeTunnelSettings])
- .requestBackgroundTime(taskName: "TunnelManager.setRelayConstraints")
- .doNotPropagateCancellation()
+ operationQueue.addOperation(operation)
}
- func setDNSSettings(_ newDNSSettings: DNSSettings) -> Result<(), TunnelManager.Error>.Promise {
- return Promise.deferred { self.tunnelInfo }
- .some(or: .missingAccount)
- .flatMap { tunnelInfo in
- return Self.updateTunnelSettings(accountToken: tunnelInfo.token) { tunnelSettings in
- tunnelSettings.interface.dnsSettings = newDNSSettings
- }
- }
- .onSuccess { newTunnelSettings in
- self.tunnelInfo?.tunnelSettings = newTunnelSettings
- self.notifyTunnelOnSettingsChange().observe { _ in }
- }
- .setOutput(())
- .schedule(on: stateQueue)
- .run(on: operationQueue, categories: [OperationCategory.changeTunnelSettings])
- .requestBackgroundTime(taskName: "TunnelManager.setDNSSettings")
- .doNotPropagateCancellation()
- }
+ func regeneratePrivateKey(completionHandler: ((TunnelManager.Error?) -> Void)? = nil) {
+ let operation = RegeneratePrivateKeyOperation(queue: stateQueue, state: state, restClient: restClient) { [weak self] completion in
+ guard let self = self else { return }
- // MARK: - Tunnel observeration
+ dispatchPrecondition(condition: .onQueue(self.stateQueue))
- /// Add tunnel observer.
- /// In order to cancel the observation, either call `removeTunnelObserver(_:)` or simply release
- /// the observer.
- func addObserver<T: TunnelObserver>(_ observer: T) {
- observerList.append(AnyTunnelObserver(observer))
- }
+ switch completion {
+ case .success:
+ self.updatePrivateKeyRotationTimer()
+ self.reconnectTunnel(completionHandler: nil)
- /// Remove tunnel observer.
- func removeObserver<T: TunnelObserver>(_ observer: T) {
- observerList.remove(AnyTunnelObserver(observer))
- }
+ case .failure(let error):
+ self.logger.error(chainedError: error, message: "Failed to regenerate private key")
- // MARK: - Private methods
+ case .cancelled:
+ break
+ }
- private func tunnelInfoDidChange(_ newTunnelInfo: TunnelInfo?) {
- // Notify observers
- DispatchQueue.main.async {
- self.observerList.forEach { (observer) in
- observer.tunnelManager(self, didUpdateTunnelSettings: newTunnelInfo)
+ DispatchQueue.main.async {
+ completionHandler?(completion.error)
}
}
- }
- private func initializeManager(accountToken: String?, tunnels: [TunnelProviderManagerType]?) -> Result<(), TunnelManager.Error>.Promise {
- // Migrate the tunnel settings if needed
- let migrationResult = accountToken.map { self.migrateTunnelSettings(accountToken: $0) }
- switch migrationResult {
- case .success, .none:
- break
- case .failure(let migrationError):
- return .failure(migrationError)
+ let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Regenerate private key") {
+ operation.cancel()
}
- switch (tunnels?.first, accountToken) {
- // Case 1: tunnel exists and account token is set.
- // Verify that tunnel can access the configuration via the persistent keychain reference
- // stored in `passwordReference` field of VPN configuration.
- case (.some(let tunnelProvider), .some(let accountToken)):
- let verificationResult = self.verifyTunnel(tunnelProvider: tunnelProvider, expectedAccountToken: accountToken)
- let tunnelSettingsResult = Self.loadTunnelSettings(accountToken: accountToken)
+ operation.completionBlock = {
+ UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
+ }
- switch (verificationResult, tunnelSettingsResult) {
- case (.success(true), .success(let keychainEntry)):
- self.tunnelInfo = TunnelInfo(token: accountToken, tunnelSettings: keychainEntry.tunnelSettings)
- self.setTunnelProvider(tunnelProvider: tunnelProvider)
+ exclusivityController.addOperation(operation, categories: [OperationCategory.changeTunnelSettings])
- return .success(())
+ operationQueue.addOperation(operation)
+ }
- // Remove the tunnel when failed to verify it but successfuly loaded the tunnel
- // settings.
- case (.failure(let verificationError), .success(let keychainEntry)):
- self.logger.error(chainedError: verificationError, message: "Failed to verify the tunnel but successfully loaded the tunnel settings. Removing the tunnel.")
+ func rotatePrivateKey(completionHandler: @escaping (KeyRotationResult?, TunnelManager.Error?) -> Void) {
+ let operation = RotatePrivateKeyOperation(
+ queue: stateQueue,
+ state: state,
+ restClient: restClient,
+ rotationInterval: Self.privateKeyRotationInterval) { [weak self] completion in
+ guard let self = self else { return }
- // Identical code path as the case below.
- fallthrough
+ dispatchPrecondition(condition: .onQueue(self.stateQueue))
- // Remove the tunnel with corrupt configuration.
- // It will be re-created upon the first attempt to connect the tunnel.
- case (.success(false), .success(let keychainEntry)):
- return tunnelProvider.removeFromPreferences()
- .receive(on: self.stateQueue)
- .mapError { error in
- return .removeInconsistentVPNConfiguration(error)
- }
- .onSuccess { _ in
- self.tunnelInfo = TunnelInfo(token: accountToken, tunnelSettings: keychainEntry.tunnelSettings)
- }
+ var rotationResult: KeyRotationResult?
+ var rotationError: TunnelManager.Error?
- // Remove the tunnel when failed to verify the tunnel and load tunnel settings.
- case (.failure(let verificationError), .failure(_)):
- self.logger.error(chainedError: verificationError, message: "Failed to verify the tunnel and load tunnel settings. Removing the tunnel.")
+ switch completion {
+ case .success(let result):
+ rotationResult = result
- return tunnelProvider.removeFromPreferences()
- .receive(on: self.stateQueue)
- .mapError { error in
- return .removeInconsistentVPNConfiguration(error)
- }
- .flatMap { _ in
- return .failure(verificationError)
+ self.reconnectTunnel {
+ completionHandler(rotationResult, rotationError)
}
- // Remove the tunnel when the app is not able to read tunnel settings
- case (.success(_), .failure(let settingsReadError)):
- self.logger.error(chainedError: settingsReadError, message: "Failed to load tunnel settings. Removing the tunnel.")
+ case .failure(let error):
+ rotationError = error
+ self.logger.error(chainedError: error, message: "Failed to rotate private key")
- return tunnelProvider.removeFromPreferences()
- .receive(on: self.stateQueue)
- .mapError { error in
- return .removeInconsistentVPNConfiguration(error)
- }
- .flatMap { _ in
- return .failure(settingsReadError)
+ DispatchQueue.main.async {
+ completionHandler(rotationResult, rotationError)
}
- }
- // Case 2: tunnel exists but account token is unset.
- // Remove the orphaned tunnel.
- case (.some(let tunnelProvider), .none):
- return tunnelProvider.removeFromPreferences()
- .receive(on: self.stateQueue)
- .mapError { error in
- return .removeInconsistentVPNConfiguration(error)
+ case .cancelled:
+ DispatchQueue.main.async {
+ completionHandler(rotationResult, rotationError)
+ }
}
-
- // Case 3: tunnel does not exist but the account token is set.
- // Verify that tunnel settings exists in keychain.
- case (.none, .some(let accountToken)):
- switch Self.loadTunnelSettings(accountToken: accountToken) {
- case .success(let keychainEntry):
- self.tunnelInfo = TunnelInfo(token: accountToken, tunnelSettings: keychainEntry.tunnelSettings)
-
- return .success(())
-
- case .failure(let error):
- return .failure(error)
}
- // Case 4: no tunnels exist and account token is unset.
- case (.none, .none):
- return .success(())
- }
- }
-
- private func verifyTunnel(tunnelProvider: TunnelProviderManagerType, expectedAccountToken accountToken: String) -> Result<Bool, Error> {
- // Check that the VPN configuration points to the same account token
- guard let username = tunnelProvider.protocolConfiguration?.username, username == accountToken else {
- logger.warning("The token assigned to the VPN configuration does not match the logged in account.")
- return .success(false)
+ let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Rotate private key") {
+ operation.cancel()
}
- // Check that the passwordReference, containing the keychain reference for tunnel
- // configuration, is set.
- guard let keychainReference = tunnelProvider.protocolConfiguration?.passwordReference else {
- logger.warning("VPN configuration is missing the passwordReference.")
- return .success(false)
+ operation.completionBlock = {
+ UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
}
- // Verify that the keychain reference points to the existing entry in Keychain.
- // Bad reference is possible when migrating the user data from one device to the other.
- return TunnelSettingsManager.exists(searchTerm: .persistentReference(keychainReference))
- .mapError { (error) -> Error in
- logger.error(chainedError: error, message: "Failed to verify the persistent keychain reference for tunnel settings.")
+ exclusivityController.addOperation(operation, categories: [OperationCategory.changeTunnelSettings])
- return Error.readTunnelSettings(error)
- }
+ operationQueue.addOperation(operation)
}
- /// Set the instance of the active tunnel and add the tunnel status observer
- private func setTunnelProvider(tunnelProvider: TunnelProviderManagerType) {
- guard self.tunnelProvider != tunnelProvider else {
- return
- }
-
- // Save the new active tunnel provider
- self.tunnelProvider = tunnelProvider
-
- // Set up tunnel IPC
- self.ipcSession = TunnelIPC.Session(from: tunnelProvider)
+ func setRelayConstraints(_ newConstraints: RelayConstraints, completionHandler: @escaping (TunnelManager.Error?) -> Void) {
+ scheduleTunnelSettingsUpdate(
+ taskName: "Set relay constraints",
+ modificationBlock: { tunnelSettings in
+ tunnelSettings.relayConstraints = newConstraints
+ },
+ completionHandler: completionHandler
+ )
+ }
- // Register for tunnel connection status changes
- unregisterConnectionObserver()
- connectionStatusObserver = NotificationCenter.default
- .addObserver(forName: .NEVPNStatusDidChange, object: tunnelProvider.connection, queue: nil) {
- [weak self] (notification) in
- guard let self = self else { return }
+ func setDNSSettings(_ newDNSSettings: DNSSettings, completionHandler: @escaping (TunnelManager.Error?) -> Void) {
+ scheduleTunnelSettingsUpdate(
+ taskName: "Set DNS settings",
+ modificationBlock: { tunnelSettings in
+ tunnelSettings.interface.dnsSettings = newDNSSettings
+ },
+ completionHandler: completionHandler
+ )
+ }
- self.stateQueue.async {
- self.updateTunnelState()
- }
- }
+ // MARK: - Tunnel observeration
- // Update the existing state
- updateTunnelState()
+ /// Add tunnel observer.
+ /// In order to cancel the observation, either call `removeObserver(_:)` or simply release
+ /// the observer.
+ func addObserver<T: TunnelObserver>(_ observer: T) {
+ observerList.append(AnyTunnelObserver(observer))
}
- private func unregisterConnectionObserver() {
- if let connectionStatusObserver = connectionStatusObserver {
- NotificationCenter.default.removeObserver(connectionStatusObserver)
- self.connectionStatusObserver = nil
- }
+ /// Remove tunnel observer.
+ func removeObserver<T: TunnelObserver>(_ observer: T) {
+ observerList.remove(AnyTunnelObserver(observer))
}
- private func pushWireguardKeyAndUpdateSettings(accountToken: String, publicKey: PublicKey) -> Result<TunnelSettings, Error>.Promise {
- return REST.Client.shared.pushWireguardKey(token: accountToken, publicKey: publicKey)
- .execute()
- .mapError { error in
- return .pushWireguardKey(error)
- }
- .receive(on: stateQueue)
- .flatMap { associatedAddresses in
- return Self.updateTunnelSettings(accountToken: accountToken) { (tunnelSettings) in
- tunnelSettings.interface.addresses = [
- associatedAddresses.ipv4Address,
- associatedAddresses.ipv6Address
- ]
- }
- }
- }
+ // MARK: - TunnelManagerStateDelegate
- private func removeWireguardKeyFromServer(accountToken: String, publicKey: PublicKey) -> Result<Bool, Error>.Promise {
- return REST.Client.shared.deleteWireguardKey(token: accountToken, publicKey: publicKey)
- .execute(retryStrategy: .default)
- .map { _ in
- return true
- }
- .flatMapError { restError -> Result<Bool, Error> in
- if case .server(.pubKeyNotFound) = restError {
- return .success(false)
- } else {
- return .failure(.removeWireguardKey(restError))
- }
+ func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelInfo newTunnelInfo: TunnelInfo?) {
+ DispatchQueue.main.async {
+ self.observerList.forEach { (observer) in
+ observer.tunnelManager(self, didUpdateTunnelSettings: newTunnelInfo)
}
+ }
}
- private func replaceWireguardKeyAndUpdateSettings(
- accountToken: String,
- oldPublicKey: PublicKeyWithMetadata,
- newPrivateKey: PrivateKeyWithMetadata
- ) -> Result<TunnelSettings, Error>.Promise
- {
- return REST.Client.shared.replaceWireguardKey(token: accountToken, oldPublicKey: oldPublicKey.publicKey, newPublicKey: newPrivateKey.publicKey)
- .execute()
- .mapError { error in
- return .replaceWireguardKey(error)
- }
- .receive(on: self.stateQueue)
- .flatMap { associatedAddresses in
- return Self.updateTunnelSettings(accountToken: accountToken) { (tunnelSettings) in
- tunnelSettings.interface.privateKey = newPrivateKey
- tunnelSettings.interface.addresses = [
- associatedAddresses.ipv4Address,
- associatedAddresses.ipv6Address
- ]
- }
+ func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelState newTunnelState: TunnelState) {
+ logger.info("Set tunnel state: \(newTunnelState)")
+
+ DispatchQueue.main.async {
+ self.observerList.forEach { (observer) in
+ observer.tunnelManager(self, didUpdateTunnelState: newTunnelState)
}
+ }
}
- /// Update `TunnelState` from `NEVPNStatus`.
- /// Collects the `TunnelConnectionInfo` from the tunnel via IPC if needed before assigning the `tunnelState`
- private func updateTunnelState() {
+ func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelProvider newTunnelProvider: TunnelProviderManagerType?, shouldRefreshTunnelState: Bool) {
dispatchPrecondition(condition: .onQueue(stateQueue))
- guard let connectionStatus = self.tunnelProvider?.connection.status else { return }
-
- logger.debug("VPN status changed to \(connectionStatus)")
- tunnelConnectionInfoToken = nil
-
- switch connectionStatus {
- case .connecting:
- switch tunnelState {
- case .connecting(.some(_)):
- logger.debug("Ignore repeating connecting state.")
- default:
- tunnelState = .connecting(nil)
- }
-
- case .reasserting:
- ipcSession?.getTunnelConnectionInfo()
- .receive(on: stateQueue)
- .storeCancellationToken(in: &tunnelConnectionInfoToken)
- .onSuccess { connectionInfo in
- if let connectionInfo = connectionInfo {
- self.tunnelState = .reconnecting(connectionInfo)
- }
- }
- .observe { _ in }
-
- case .connected:
- ipcSession?.getTunnelConnectionInfo()
- .receive(on: stateQueue)
- .storeCancellationToken(in: &tunnelConnectionInfoToken)
- .onSuccess { connectionInfo in
- if let connectionInfo = connectionInfo {
- self.tunnelState = .connected(connectionInfo)
- }
- }
- .observe { _ in }
-
- case .disconnected:
- switch tunnelState {
- case .pendingReconnect:
- logger.debug("Ignore disconnected state when pending reconnect.")
-
- case .disconnecting(.reconnect):
- logger.debug("Restart the tunnel on disconnect.")
- tunnelState = .pendingReconnect
- startTunnel()
+ // Register for tunnel connection status changes
+ if let newTunnelProvider = newTunnelProvider {
+ subscribeVPNStatusObserver(for: newTunnelProvider)
+ } else {
+ unsubscribeVPNStatusObserver()
+ }
- default:
- tunnelState = .disconnected
- }
+ // Update the existing state
+ if shouldRefreshTunnelState {
+ updateTunnelState()
+ }
+ }
- case .disconnecting:
- switch tunnelState {
- case .disconnecting:
- break
- default:
- tunnelState = .disconnecting(.nothing)
- }
+ // MARK: - Private methods
- case .invalid:
- tunnelState = .disconnected
+ private func subscribeVPNStatusObserver(for tunnelProvider: TunnelProviderManagerType) {
+ unsubscribeVPNStatusObserver()
- @unknown default:
- logger.debug("Unknown NEVPNStatus: \(connectionStatus.rawValue)")
- }
+ NotificationCenter.default.addObserver(
+ self, selector: #selector(didReceiveVPNStatusChange(_:)),
+ name: .NEVPNStatusDidChange,
+ object: tunnelProvider.connection
+ )
}
- private func makeTunnelProvider(accountToken: String) -> Result<TunnelProviderManagerType, Error>.Promise {
- return TunnelProviderManagerType.loadAllFromPreferences()
- .mapError { error -> Error in
- return .loadAllVPNConfigurations(error)
- }
- .flatMap { tunnels in
- return Self.setupTunnelProvider(accountToken: accountToken, tunnels: tunnels)
- }
- .mapThen { tunnelProvider in
- return tunnelProvider.saveToPreferences()
- .mapError { error in
- return .saveVPNConfiguration(error)
- }
- .mapThen { _ in
- // 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.
- return tunnelProvider.loadFromPreferences()
- .mapError { error in
- return .reloadVPNConfiguration(error)
- }
- }
- .setOutput(tunnelProvider)
- }
+ private func unsubscribeVPNStatusObserver() {
+ NotificationCenter.default.removeObserver(self, name: .NEVPNStatusDidChange, object: nil)
}
- private func sendFailureToObservers(_ failure: Error) {
- DispatchQueue.main.async {
- self.observerList.forEach { observer in
- observer.tunnelManager(self, didFailWithError: failure)
- }
+ @objc private func didReceiveVPNStatusChange(_ notification: Notification) {
+ stateQueue.async {
+ self.updateTunnelState()
}
}
- private func notifyTunnelOnSettingsChange() -> Promise<Void> {
- return Promise.deferred { () -> (TunnelIPC.Session, TunnelProviderManagerType)? in
- if let ipcSession = self.ipcSession, let tunnelProvider = self.tunnelProvider {
- return (ipcSession, tunnelProvider)
- } else {
- return nil
- }
- }
- .mapThen(defaultValue: ()) { (ipc, tunnelProvider) in
- return Promise { resolver in
- let connection = tunnelProvider.connection
- var statusObserver: NSObjectProtocol?
- var ipcToken: PromiseCancellationToken?
-
- let releaseObserver = {
- if let statusObserver = statusObserver {
- NotificationCenter.default.removeObserver(statusObserver)
- }
- }
+ /// Update `TunnelState` from `NEVPNStatus`.
+ /// Collects the `TunnelConnectionInfo` from the tunnel via IPC if needed before assigning the `tunnelState`
+ private func updateTunnelState() {
+ dispatchPrecondition(condition: .onQueue(stateQueue))
- let handleStatus = {
- switch connection.status {
- case .connected:
- releaseObserver()
+ guard let connectionStatus = self.state.tunnelProvider?.connection.status else { return }
- ipc.reloadTunnelSettings()
- .storeCancellationToken(in: &ipcToken)
- .observe { completion in
- switch completion {
- case .finished(let result):
- if case .failure(let error) = result {
- self.logger.error(chainedError: error, message: "Failed to send IPC request to reload tunnel settings")
- }
- resolver.resolve(value: ())
- case .cancelled:
- resolver.resolve(completion: .cancelled)
- }
- }
+ logger.debug("VPN status changed to \(connectionStatus)")
- case .connecting, .reasserting:
- // wait for transition to complete
- break
+ let operation = MapConnectionStatusOperation(queue: stateQueue, state: state, connectionStatus: connectionStatus) { [weak self] in
+ guard let self = self else { return }
- case .invalid, .disconnecting, .disconnected:
- releaseObserver()
- resolver.resolve(value: ())
+ dispatchPrecondition(condition: .onQueue(self.stateQueue))
- @unknown default:
- break
- }
- }
+ self.startTunnel()
+ }
- // Add connection status observer
- statusObserver = NotificationCenter.default.addObserver(
- forName: .NEVPNStatusDidChange,
- object: connection,
- queue: .main) { note in
- handleStatus()
- }
+ exclusivityController.addOperation(operation, categories: [OperationCategory.tunnelStateUpdate])
- // Set cancellation handler
- resolver.setCancelHandler {
- DispatchQueue.main.async {
- releaseObserver()
- ipcToken = nil
- resolver.resolve(completion: .cancelled)
- }
- }
+ // Cancel last VPN status mapping operation
+ lastMapConnectionStatusOperation?.cancel()
+ lastMapConnectionStatusOperation = operation
- // Run initial check
- DispatchQueue.main.async {
- handleStatus()
- }
- }
- }
- .schedule(on: stateQueue)
- .run(on: operationQueue, categories: [OperationCategory.notifyTunnelSettingsChange])
- .requestBackgroundTime(taskName: "TunnelManager.notifyTunnelOnSettingsChange")
+ operationQueue.addOperation(operation)
}
@objc private func applicationDidBecomeActive() {
@@ -938,85 +526,72 @@ class TunnelManager {
}
}
- // MARK: - Private class methods
+ private func makeSetAccountOperation(accountToken: String?, completionHandler: @escaping (OperationCompletion<(), TunnelManager.Error>) -> Void) -> Operation {
+ return SetAccountOperation(
+ queue: stateQueue,
+ state: state,
+ restClient: restClient,
+ accountToken: accountToken,
+ willDeleteVPNConfigurationHandler: { [weak self] in
+ guard let self = self else { return }
- private class func loadTunnelSettings(accountToken: String) -> Result<TunnelSettingsManager.KeychainEntry, Error> {
- return TunnelSettingsManager.load(searchTerm: .accountToken(accountToken))
- .mapError { Error.readTunnelSettings($0) }
- }
+ dispatchPrecondition(condition: .onQueue(self.stateQueue))
- private class func updateTunnelSettings(accountToken: String, block: (inout TunnelSettings) -> Void) -> Result<TunnelSettings, Error> {
- return TunnelSettingsManager.update(searchTerm: .accountToken(accountToken), using: block)
- .mapError { Error.updateTunnelSettings($0) }
- }
+ // Unregister from receiving VPN connection status changes
+ self.unsubscribeVPNStatusObserver()
- /// Retrieve the existing `TunnelSettings` or create the new ones
- private class func makeTunnelSettings(accountToken: String) -> Result<TunnelSettings, Error> {
- return Self.loadTunnelSettings(accountToken: accountToken)
- .map { $0.tunnelSettings }
- .flatMapError { error in
- if case .readTunnelSettings(.lookupEntry(.itemNotFound)) = error {
- let defaultConfiguration = TunnelSettings()
+ // Cancel last VPN status mapping operation
+ self.lastMapConnectionStatusOperation?.cancel()
+ self.lastMapConnectionStatusOperation = nil
+ },
+ completionHandler: { [weak self] completion in
+ guard let self = self else { return }
- return TunnelSettingsManager
- .add(configuration: defaultConfiguration, account: accountToken)
- .mapError { .addTunnelSettings($0) }
- .map { defaultConfiguration }
- } else {
- return .failure(error)
- }
- }
+ dispatchPrecondition(condition: .onQueue(self.stateQueue))
+
+ self.updatePrivateKeyRotationTimer()
+
+ completionHandler(completion)
+ })
}
- private class func setupTunnelProvider(accountToken: String ,tunnels: [TunnelProviderManagerType]?) -> Result<TunnelProviderManagerType, Error> {
- // Request persistent keychain reference to tunnel settings
- return TunnelSettingsManager.getPersistentKeychainReference(account: accountToken)
- .map { (passwordReference) -> TunnelProviderManagerType in
- // Get the first available tunnel or make a new one
- let tunnelProvider = tunnels?.first ?? TunnelProviderManagerType()
+ private func scheduleTunnelSettingsUpdate(taskName: String, modificationBlock: @escaping (inout TunnelSettings) -> Void, completionHandler: @escaping (TunnelManager.Error?) -> Void) {
+ let operation = SetTunnelSettingsOperation(
+ queue: stateQueue,
+ state: state,
+ modificationBlock: modificationBlock,
+ completionHandler: { [weak self] completion in
+ guard let self = self else { return }
- let protocolConfig = NETunnelProviderProtocol()
- protocolConfig.providerBundleIdentifier = ApplicationConfiguration.packetTunnelExtensionIdentifier
- protocolConfig.serverAddress = ""
- protocolConfig.username = accountToken
- protocolConfig.passwordReference = passwordReference
+ dispatchPrecondition(condition: .onQueue(self.stateQueue))
- tunnelProvider.isEnabled = true
- tunnelProvider.localizedDescription = "WireGuard"
- tunnelProvider.protocolConfiguration = protocolConfig
+ switch completion {
+ case .success:
+ self.reconnectTunnel(completionHandler: nil)
- // 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
+ case .failure(let error):
+ self.logger.error(chainedError: error, message: "Failed to set tunnel settings")
- return tunnelProvider
- }.mapError { (error) -> Error in
- return .obtainPersistentKeychainReference(error)
- }
- }
+ case .cancelled:
+ break
+ }
- private func migrateTunnelSettings(accountToken: String) -> Result<Bool, Error> {
- let result = TunnelSettingsManager
- .migrateKeychainEntry(searchTerm: .accountToken(accountToken))
- .mapError { (error) -> Error in
- return .migrateTunnelSettings(error)
- }
+ DispatchQueue.main.async {
+ completionHandler(completion.error)
+ }
+ })
- switch result {
- case .success(let migrated):
- if migrated {
- self.logger.info("Migrated Keychain tunnel configuration.")
- } else {
- self.logger.info("Tunnel settings are up to date. No migration needed.")
- }
+ let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: taskName) {
+ operation.cancel()
+ }
- case .failure(let error):
- self.logger.error(chainedError: error)
+ operation.completionBlock = {
+ UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
}
- return result
+ exclusivityController.addOperation(operation, categories: [OperationCategory.changeTunnelSettings])
+
+ operationQueue.addOperation(operation)
}
}
@@ -1062,16 +637,15 @@ extension TunnelManager {
}
/// Schedule background task relative to the private key creation date.
- func scheduleBackgroundTask() -> Result<(), TunnelManager.Error>.Promise {
- return Promise.deferred { self.tunnelInfo }
- .some(or: .missingAccount)
- .flatMap { tunnelInfo -> Result<(), TunnelManager.Error> in
- let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate
- let beginDate = Date(timeInterval: Self.privateKeyRotationInterval, since: creationDate)
+ func scheduleBackgroundTask() -> Result<(), TunnelManager.Error> {
+ if let tunnelInfo = self.state.tunnelInfo {
+ let creationDate = tunnelInfo.tunnelSettings.interface.privateKey.creationDate
+ let beginDate = Date(timeInterval: Self.privateKeyRotationInterval, since: creationDate)
- return self.submitBackgroundTask(at: beginDate)
- }
- .schedule(on: stateQueue)
+ return submitBackgroundTask(at: beginDate)
+ } else {
+ return .failure(.missingAccount)
+ }
}
/// Create and submit task request to scheduler.
@@ -1090,64 +664,47 @@ extension TunnelManager {
/// Background task handler.
private func handleBackgroundTask(_ task: BGProcessingTask) {
- var cancellationToken: PromiseCancellationToken?
+ logger.debug("Start private key rotation task")
- self.logger.debug("Start private key rotation task")
+ rotatePrivateKey { rotationResult, error in
+ if let scheduleDate = self.handlePrivateKeyRotationCompletion(result: rotationResult, error: error) {
+ // Schedule next background task
+ switch self.submitBackgroundTask(at: scheduleDate) {
+ case .success:
+ self.logger.debug("Scheduled next private key rotation task at \(scheduleDate.logFormatDate())")
- self.rotatePrivateKey()
- .storeCancellationToken(in: &cancellationToken)
- .observe { completion in
- if let scheduleDate = self.handlePrivateKeyRotationCompletion(completion: completion) {
- // Schedule next background task
- switch self.submitBackgroundTask(at: scheduleDate) {
- case .success:
- self.logger.debug("Scheduled next private key rotation task at \(scheduleDate.logFormatDate())")
-
- case .failure(let error):
- self.logger.error(chainedError: error, message: "Failed to schedule next private key rotation task")
- }
+ case .failure(let error):
+ self.logger.error(chainedError: error, message: "Failed to schedule next private key rotation task")
}
-
- // Complete current task
- task.setTaskCompleted(success: Self.isTaskCompleted(completion: completion))
}
+ // Complete current task
+ task.setTaskCompleted(success: error == nil)
+ }
+
task.expirationHandler = {
- cancellationToken?.cancel()
+ // TODO: handle cancellation?
}
}
}
extension TunnelManager {
- fileprivate static func isTaskCompleted(completion: PromiseCompletion<Result<KeyRotationResult, TunnelManager.Error>>) -> Bool {
- switch completion {
- case .cancelled:
- return false
- case .finished(.success):
- return true
- case .finished(.failure):
- return false
- }
- }
- fileprivate func handlePrivateKeyRotationCompletion(completion: PromiseCompletion<Result<KeyRotationResult, TunnelManager.Error>>) -> Date? {
- switch completion {
- case .finished(.success(let result)):
+ fileprivate func handlePrivateKeyRotationCompletion(result: KeyRotationResult?, error: TunnelManager.Error?) -> Date? {
+ if let error = error {
+ logger.error(chainedError: error, message: "Failed to rotate private key")
+
+ return nextRetryScheduleDate(error)
+ } else if let result = result {
switch result {
case .finished:
- self.logger.debug("Finished private key rotation")
+ logger.debug("Finished private key rotation")
case .throttled:
- self.logger.debug("Private key was already rotated earlier")
+ logger.debug("Private key was already rotated earlier")
}
- return self.nextScheduleDate(result)
-
- case .finished(.failure(let error)):
- self.logger.error(chainedError: error, message: "Failed to rotate private key in background task")
-
- return self.nextRetryScheduleDate(error)
-
- case .cancelled:
- self.logger.debug("Private key rotation was cancelled")
+ return nextScheduleDate(result)
+ } else {
+ logger.debug("Private key rotation was cancelled")
return Date(timeIntervalSinceNow: Self.privateKeyRotationFailureRetryInterval)
}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift b/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift
index f282b83f8b..01fcbb136d 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManagerError.swift
@@ -69,6 +69,9 @@ extension TunnelManager {
/// A failure to schedule background task
case backgroundTaskScheduler(Swift.Error)
+ /// A failure to reload tunnel
+ case reloadTunnel(TunnelIPC.Error)
+
var errorDescription: String? {
switch self {
case .missingAccount:
@@ -109,6 +112,8 @@ extension TunnelManager {
return "Failed to remove the WireGuard key from server"
case .backgroundTaskScheduler:
return "Failed to schedule background task"
+ case .reloadTunnel:
+ return "Failed to reload tunnel"
}
}
}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift b/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift
new file mode 100644
index 0000000000..f8ebaf86b6
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/TunnelManagerState.swift
@@ -0,0 +1,107 @@
+//
+// TunnelManager.State.swift
+// MullvadVPN
+//
+// Created by pronebird on 26/01/2022.
+// Copyright © 2022 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import NetworkExtension
+
+// Switch to stabs on simulator
+#if targetEnvironment(simulator)
+typealias TunnelProviderManagerType = SimulatorTunnelProviderManager
+#else
+typealias TunnelProviderManagerType = NETunnelProviderManager
+#endif
+
+protocol TunnelManagerStateDelegate: AnyObject {
+ func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelInfo newTunnelInfo: TunnelInfo?)
+ func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelState newTunnelState: TunnelState)
+ func tunnelManagerState(_ state: TunnelManager.State, didChangeTunnelProvider newTunnelProvider: TunnelProviderManagerType?, shouldRefreshTunnelState: Bool)
+}
+
+extension TunnelManager {
+
+ class State {
+ let queue: DispatchQueue
+ weak var delegate: TunnelManagerStateDelegate?
+
+ private let queueMarkerKey = DispatchSpecificKey<Bool>()
+
+ private var _tunnelInfo: TunnelInfo?
+ private var _tunnelProvider: TunnelProviderManagerType?
+ private var _tunnelState: TunnelState = .disconnected
+
+ var tunnelInfo: TunnelInfo? {
+ get {
+ return performBlock {
+ return _tunnelInfo
+ }
+ }
+ set {
+ performBlock {
+ if _tunnelInfo != newValue {
+ _tunnelInfo = newValue
+
+ delegate?.tunnelManagerState(self, didChangeTunnelInfo: newValue)
+ }
+ }
+ }
+ }
+
+ var tunnelProvider: TunnelProviderManagerType? {
+ return performBlock {
+ return _tunnelProvider
+ }
+ }
+
+ var tunnelState: TunnelState {
+ get {
+ return performBlock {
+ return _tunnelState
+ }
+ }
+ set {
+ performBlock {
+ if _tunnelState != newValue {
+ _tunnelState = newValue
+
+ delegate?.tunnelManagerState(self, didChangeTunnelState: newValue)
+ }
+ }
+ }
+ }
+
+ init(queue: DispatchQueue) {
+ self.queue = queue
+
+ queue.setSpecific(key: queueMarkerKey, value: true)
+ }
+
+ deinit {
+ queue.setSpecific(key: queueMarkerKey, value: nil)
+ }
+
+ func setTunnelProvider(_ newTunnelProvider: TunnelProviderManagerType?, shouldRefreshTunnelState: Bool) {
+ performBlock {
+ if _tunnelProvider != newTunnelProvider {
+ _tunnelProvider = newTunnelProvider
+
+ delegate?.tunnelManagerState(self, didChangeTunnelProvider: newTunnelProvider, shouldRefreshTunnelState: shouldRefreshTunnelState)
+ }
+ }
+ }
+
+ private func performBlock<T>(_ block: () -> T) -> T {
+ let isTargetQueue = DispatchQueue.getSpecific(key: queueMarkerKey) ?? false
+
+ if isTargetQueue {
+ return block()
+ } else {
+ return queue.sync(execute: block)
+ }
+ }
+ }
+}
diff --git a/ios/MullvadVPN/WireguardAssociatedAddresses.swift b/ios/MullvadVPN/WireguardAssociatedAddresses.swift
deleted file mode 100644
index edd8d55ca0..0000000000
--- a/ios/MullvadVPN/WireguardAssociatedAddresses.swift
+++ /dev/null
@@ -1,16 +0,0 @@
-//
-// WireguardAssociatedAddresses.swift
-// MullvadVPN
-//
-// Created by pronebird on 13/06/2019.
-// Copyright © 2019 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import Network
-import struct WireGuardKit.IPAddressRange
-
-struct WireguardAssociatedAddresses: Codable {
- let ipv4Address: IPAddressRange
- let ipv6Address: IPAddressRange
-}
diff --git a/ios/MullvadVPN/WireguardKeysViewController.swift b/ios/MullvadVPN/WireguardKeysViewController.swift
index 30e9957bc8..e630285121 100644
--- a/ios/MullvadVPN/WireguardKeysViewController.swift
+++ b/ios/MullvadVPN/WireguardKeysViewController.swift
@@ -37,8 +37,6 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
private var verifyKeyCancellationToken: PromiseCancellationToken?
private let alertPresenter = AlertPresenter()
- private let logger = Logger(label: "WireguardKeys")
-
private var state: WireguardKeysViewState = .default {
didSet {
updateViewState(state)
@@ -239,18 +237,14 @@ class WireguardKeysViewController: UIViewController, TunnelObserver {
private func regeneratePrivateKey() {
self.updateViewState(.regeneratingKey)
- TunnelManager.shared.regeneratePrivateKey()
- .receive(on: .main)
- .onSuccess { [weak self] _ in
- self?.updateViewState(.regeneratedKey(true))
- }
- .onFailure { [weak self] error in
- self?.logger.error(chainedError: error, message: "Failed to regenerate the private key")
-
+ TunnelManager.shared.regeneratePrivateKey { [weak self] error in
+ if let error = error {
self?.showKeyRegenerationFailureAlert(error)
self?.updateViewState(.regeneratedKey(false))
+ } else {
+ self?.updateViewState(.regeneratedKey(true))
}
- .observe { _ in }
+ }
}
private func showKeyVerificationFailureAlert(_ error: REST.Error) {
diff --git a/ios/MullvadVPN/en.lproj/Account.strings b/ios/MullvadVPN/en.lproj/Account.strings
index 5df4e93dbd..aafd2bc65e 100644
--- a/ios/MullvadVPN/en.lproj/Account.strings
+++ b/ios/MullvadVPN/en.lproj/Account.strings
@@ -40,12 +40,6 @@
/* Title for confirmation button in logout dialog */
"LOGOUT_CONFIRMATION_ALERT_YES_ACTION" = "Log out";
-/* Message for logout failure alert */
-"LOGOUT_FAILURE_ALERT_OK_ACTION" = "OK";
-
-/* Title for logout failure alert */
-"LOGOUT_FAILURE_ALERT_TITLE" = "Failed to log out";
-
/* Navigation title */
"NAVIGATION_TITLE" = "Account";