diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-02-01 10:05:40 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-02-01 10:05:40 +0100 |
| commit | e3548fe7a570d0531afa76f2ee7622c1bac5be27 (patch) | |
| tree | ecf9ee3f8ae147dfe70a199fab02a2c42d1a9fe7 | |
| parent | 92f9f62174e18aeee0151c0ec7ea927b835d4e08 (diff) | |
| parent | 529635141f0588254d89cf07286231e93ea0ecfc (diff) | |
| download | mullvadvpn-e3548fe7a570d0531afa76f2ee7622c1bac5be27.tar.xz mullvadvpn-e3548fe7a570d0531afa76f2ee7622c1bac5be27.zip | |
Merge branch 'tunnel-manager-actor'
31 files changed, 2000 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"; diff --git a/ios/MullvadVPN/en.lproj/TunnelManager.strings b/ios/MullvadVPN/en.lproj/TunnelManager.strings index a6a08c78b8..4dfb42a1ba 100644 --- a/ios/MullvadVPN/en.lproj/TunnelManager.strings +++ b/ios/MullvadVPN/en.lproj/TunnelManager.strings @@ -29,6 +29,9 @@ "READ_TUNNEL_SETTINGS_ERROR" = "Failed to read tunnel settings"; /* No comment provided by engineer. */ +"RELOAD_TUNNEL_ERROR" = "Failed to reload tunnel: %@"; + +/* No comment provided by engineer. */ "RELOAD_VPN_CONFIGURATIONS_ERROR" = "Failed to reload a VPN configuration: %@"; /* No comment provided by engineer. */ |
