diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2020-07-15 14:40:39 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2020-07-15 14:40:39 +0200 |
| commit | e906abb2c3847e0fa3cfa54d9335f56fc9c8f8c7 (patch) | |
| tree | 054c1d7feaf6309ef073362f60e870b33b639072 | |
| parent | 8b7b7175ea5c93374daf60abfda8b560be374048 (diff) | |
| parent | 8b32eb69d489adf7a4dc06ab4fa3494bef726029 (diff) | |
| download | mullvadvpn-e906abb2c3847e0fa3cfa54d9335f56fc9c8f8c7.tar.xz mullvadvpn-e906abb2c3847e0fa3cfa54d9335f56fc9c8f8c7.zip | |
Merge branch 'uncombine-tunnel-manager'
30 files changed, 2370 insertions, 1613 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index 7275ea925f..45518ed1ff 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -23,7 +23,19 @@ Line wrap the file at 100 chars. Th ## [Unreleased] +### Added +- Ship the initial relay list with the app, and do once an hour periodic refresh in background. +- Refresh account expiry when visiting settings. + +### Fixed +- Fix the issue when starting the tunnel could take longer than expected due to the app refreshing + the relay list. +- Fix the issue when regenerating the WireGuard key and dismissing the settings at the same + time could lead to the revoked key still being used by the tunnel, leaving the tunnel unusable. +### Changed +- Remove the public WireGuard inside the VPN tunnel during the log out, if VPN is active at that + time. ## [2020.3] - 2020-06-12 ### Added diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 62f24964fa..b0e038f11b 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -61,7 +61,6 @@ 5845F83C236C72E300B2D93C /* AutoDisposableSink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5845F839236C6A7200B2D93C /* AutoDisposableSink.swift */; }; 5845F842236CBACD00B2D93C /* PacketTunnelIpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */; }; 5845F843236CBDAB00B2D93C /* PacketTunnelIpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */; }; - 584B26FF237435A90073B10E /* RelaySelector+RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584B26FD237435990073B10E /* RelaySelector+RelayCache.swift */; }; 584E96BA240D791E00D3334F /* CancellableDelayPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584E96B9240D791E00D3334F /* CancellableDelayPublisher.swift */; }; 584E96BC240FD4DA00D3334F /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8623F43901009F7EA6 /* Location.swift */; }; 584E96BD240FD4DA00D3334F /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8623F43901009F7EA6 /* Location.swift */; }; @@ -86,11 +85,9 @@ 58781CCE22AE8918009B9D8E /* RelayConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */; }; 58781CD522AFBA39009B9D8E /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CD422AFBA39009B9D8E /* RelaySelector.swift */; }; 587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */; }; - 587AD7C623421D7000E93A53 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelConfiguration.swift */; }; - 587AD7C723421D8600E93A53 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelConfiguration.swift */; }; - 587AD7C82342237300E93A53 /* TunnelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5835B7CB233B76CB0096D79F /* TunnelManager.swift */; }; + 587AD7C623421D7000E93A53 /* TunnelSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelSettings.swift */; }; + 587AD7C723421D8600E93A53 /* TunnelSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelSettings.swift */; }; 587AD7CA2342283900E93A53 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C92342283900E93A53 /* Account.swift */; }; - 587B08E0229433EB000E6F17 /* LoginState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587B08DF229433EB000E6F17 /* LoginState.swift */; }; 587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */; }; 588534BF246193D90018B744 /* AutomaticKeyRotationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588534BD246193C00018B744 /* AutomaticKeyRotationManager.swift */; }; 5888AD7F2279B6BF0051EB06 /* RelayStatusIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5888AD7E2279B6BF0051EB06 /* RelayStatusIndicatorView.swift */; }; @@ -118,8 +115,8 @@ 58ADDB3E227B1CD900FAFEA7 /* MullvadRpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ADDB3D227B1CD900FAFEA7 /* MullvadRpc.swift */; }; 58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; }; 58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; }; - 58AEEF6B2344A46200C9BBD5 /* TunnelConfigurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF6A2344A46200C9BBD5 /* TunnelConfigurationManager.swift */; }; - 58AEEF6C2344A49D00C9BBD5 /* TunnelConfigurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF6A2344A46200C9BBD5 /* TunnelConfigurationManager.swift */; }; + 58AEEF6B2344A46200C9BBD5 /* TunnelSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */; }; + 58AEEF6C2344A49D00C9BBD5 /* TunnelSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */; }; 58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584B26F3237434D00073B10E /* RelaySelectorTests.swift */; }; 58B0A2A9238EE6A100BC001D /* RelayConstraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */; }; 58B0A2AA238EE6A900BC001D /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CD422AFBA39009B9D8E /* RelaySelector.swift */; }; @@ -128,6 +125,7 @@ 58B0A2AD238EE6EC00BC001D /* MullvadEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */; }; 58B8743222B25A7600015324 /* WireguardAssociatedAddresses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B8743122B25A7600015324 /* WireguardAssociatedAddresses.swift */; }; 58B8743B22B788D200015324 /* PacketTunnelSettingsGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B8743722B25EAB00015324 /* PacketTunnelSettingsGenerator.swift */; }; + 58B9EB132488ED2100095626 /* AlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B9EB122488ED2100095626 /* AlertPresenter.swift */; }; 58B9EB152489139B00095626 /* DisplayChainedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B9EB142489139B00095626 /* DisplayChainedError.swift */; }; 58BA692E23E99EFF009DC256 /* Locking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA692D23E99EFF009DC256 /* Locking.swift */; }; 58BA692F23E99F5B009DC256 /* Locking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BA692D23E99EFF009DC256 /* Locking.swift */; }; @@ -167,9 +165,14 @@ 58D0C79E23F1CEBA00FE9BA7 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D0C79D23F1CEBA00FE9BA7 /* SnapshotHelper.swift */; }; 58D0C7A223F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D0C7A023F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift */; }; 58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF28A42417CB4B00E836B0 /* AppStorePaymentManager.swift */; }; + 58E6771F24ADFE7800AA26E7 /* SettingsNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E6771E24ADFE7800AA26E7 /* SettingsNavigationController.swift */; }; 58EC4E6C23915325003F5C5B /* Bundle+MullvadVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58EC4E6B23915325003F5C5B /* Bundle+MullvadVersion.swift */; }; 58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */; }; 58F3C0962492617E003E76BE /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E973DD24850EB600096F90 /* AsyncOperation.swift */; }; + 58F3C099249B978C003E76BE /* x25519.c in Sources */ = {isa = PBXBuildFile; fileRef = 58F3C098249B978C003E76BE /* x25519.c */; }; + 58F3C09A249B9852003E76BE /* x25519.c in Sources */ = {isa = PBXBuildFile; fileRef = 58F3C098249B978C003E76BE /* x25519.c */; }; + 58F3C09C249B99DD003E76BE /* Curve25519.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F3C09B249B99DD003E76BE /* Curve25519.swift */; }; + 58F3C09D249B99DD003E76BE /* Curve25519.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F3C09B249B99DD003E76BE /* Curve25519.swift */; }; 58F3C0A2249CA1E0003E76BE /* HeaderBarView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 58F3C0A1249CA1E0003E76BE /* HeaderBarView.xib */; }; 58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F3C0A3249CB069003E76BE /* HeaderBarView.swift */; }; 58F3C0A624A50157003E76BE /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 58F3C0A524A50155003E76BE /* relays.json */; }; @@ -271,7 +274,6 @@ 5845F839236C6A7200B2D93C /* AutoDisposableSink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoDisposableSink.swift; sourceTree = "<group>"; }; 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelIpc.swift; sourceTree = "<group>"; }; 584B26F3237434D00073B10E /* RelaySelectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorTests.swift; sourceTree = "<group>"; }; - 584B26FD237435990073B10E /* RelaySelector+RelayCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RelaySelector+RelayCache.swift"; sourceTree = "<group>"; }; 584E96B9240D791E00D3334F /* CancellableDelayPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellableDelayPublisher.swift; sourceTree = "<group>"; }; 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPEndpoint.swift; sourceTree = "<group>"; }; 5860F1C123A785C600CEA666 /* WireguardDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireguardDevice.swift; sourceTree = "<group>"; }; @@ -290,9 +292,8 @@ 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConstraints.swift; sourceTree = "<group>"; }; 58781CD422AFBA39009B9D8E /* RelaySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelector.swift; sourceTree = "<group>"; }; 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelProviderHost.swift; sourceTree = "<group>"; }; - 587AD7C523421D7000E93A53 /* TunnelConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelConfiguration.swift; sourceTree = "<group>"; }; + 587AD7C523421D7000E93A53 /* TunnelSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelSettings.swift; sourceTree = "<group>"; }; 587AD7C92342283900E93A53 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; }; - 587B08DF229433EB000E6F17 /* LoginState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginState.swift; sourceTree = "<group>"; }; 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Helpers.swift"; sourceTree = "<group>"; }; 588534BD246193C00018B744 /* AutomaticKeyRotationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticKeyRotationManager.swift; sourceTree = "<group>"; }; 5888AD7E2279B6BF0051EB06 /* RelayStatusIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayStatusIndicatorView.swift; sourceTree = "<group>"; }; @@ -312,11 +313,12 @@ 58ADDB3B227B1BD200FAFEA7 /* JsonRpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonRpc.swift; sourceTree = "<group>"; }; 58ADDB3D227B1CD900FAFEA7 /* MullvadRpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadRpc.swift; sourceTree = "<group>"; }; 58AEEF642344A36000C9BBD5 /* KeychainError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainError.swift; sourceTree = "<group>"; }; - 58AEEF6A2344A46200C9BBD5 /* TunnelConfigurationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelConfigurationManager.swift; sourceTree = "<group>"; }; + 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsManager.swift; sourceTree = "<group>"; }; 58B0A2A0238EE67E00BC001D /* MullvadVPNTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MullvadVPNTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 58B0A2A4238EE67E00BC001D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 58B8743122B25A7600015324 /* WireguardAssociatedAddresses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireguardAssociatedAddresses.swift; sourceTree = "<group>"; }; 58B8743722B25EAB00015324 /* PacketTunnelSettingsGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelSettingsGenerator.swift; sourceTree = "<group>"; }; + 58B9EB122488ED2100095626 /* AlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPresenter.swift; sourceTree = "<group>"; }; 58B9EB142489139B00095626 /* DisplayChainedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayChainedError.swift; sourceTree = "<group>"; }; 58BA692D23E99EFF009DC256 /* Locking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locking.swift; sourceTree = "<group>"; }; 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelProvider.swift; sourceTree = "<group>"; }; @@ -351,10 +353,14 @@ 58D0C79F23F1CECF00FE9BA7 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 58D0C7A023F1CECF00FE9BA7 /* MullvadVPNScreenshots.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MullvadVPNScreenshots.swift; sourceTree = "<group>"; }; 58DF28A42417CB4B00E836B0 /* AppStorePaymentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorePaymentManager.swift; sourceTree = "<group>"; }; + 58E6771E24ADFE7800AA26E7 /* SettingsNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsNavigationController.swift; sourceTree = "<group>"; }; 58E973DD24850EB600096F90 /* AsyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncOperation.swift; sourceTree = "<group>"; }; 58EC4E6B23915325003F5C5B /* Bundle+MullvadVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+MullvadVersion.swift"; sourceTree = "<group>"; }; 58ECD29123F178FD004298B6 /* Screenshots.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Screenshots.xcconfig; sourceTree = "<group>"; }; 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerActivityIndicatorView.swift; sourceTree = "<group>"; }; + 58F3C097249B978C003E76BE /* x25519.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = x25519.h; sourceTree = "<group>"; }; + 58F3C098249B978C003E76BE /* x25519.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = x25519.c; sourceTree = "<group>"; }; + 58F3C09B249B99DD003E76BE /* Curve25519.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Curve25519.swift; sourceTree = "<group>"; }; 58F3C0A1249CA1E0003E76BE /* HeaderBarView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HeaderBarView.xib; 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>"; }; @@ -488,6 +494,7 @@ 58CCA01D2242787B004F3011 /* AccountTextField.swift */, 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */, 58CCA01722426713004F3011 /* AccountViewController.swift */, + 58B9EB122488ED2100095626 /* AlertPresenter.swift */, 5868585424054096000B8131 /* AppButton.swift */, 58CE5E63224146200008646E /* AppDelegate.swift */, 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */, @@ -504,6 +511,7 @@ 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */, 58CCA00F224249A1004F3011 /* ConnectViewController.swift */, 58A99ED2240014A0006599E9 /* ConsentViewController.swift */, + 58F3C09B249B99DD003E76BE /* Curve25519.swift */, 582BB1B0229569620055B6EF /* CustomNavigationBar.swift */, 58C6B35D22BBBFE3003C19AD /* Data+HexCoding.swift */, 58B9EB142489139B00095626 /* DisplayChainedError.swift */, @@ -525,7 +533,6 @@ 58CE5E6C224146210008646E /* LaunchScreen.storyboard */, 58A1AA8623F43901009F7EA6 /* Location.swift */, 58BA692D23E99EFF009DC256 /* Locking.swift */, - 587B08DF229433EB000E6F17 /* LoginState.swift */, 58CE5E65224146200008646E /* LoginViewController.swift */, 58CE5E67224146200008646E /* Main.storyboard */, 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */, @@ -543,7 +550,6 @@ 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */, 5888AD88227B18C40051EB06 /* RelayList.swift */, 58781CD422AFBA39009B9D8E /* RelaySelector.swift */, - 584B26FD237435990073B10E /* RelaySelector+RelayCache.swift */, 5888AD7E2279B6BF0051EB06 /* RelayStatusIndicatorView.swift */, 582650832384102800FA7A86 /* ReplaceNilWithError.swift */, 587425C02299833500CA2045 /* RootContainerViewController.swift */, @@ -554,6 +560,7 @@ 581CBCEB2298041B00727D7F /* SettingsAppVersionCell.swift */, 5877152D23981C5B001F8237 /* SettingsBasicCell.swift */, 582BB1AE229566420055B6EF /* SettingsCell.swift */, + 58E6771E24ADFE7800AA26E7 /* SettingsNavigationController.swift */, 58CCA01122424D11004F3011 /* SettingsViewController.swift */, 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */, 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */, @@ -564,11 +571,11 @@ 581CBCED229826FD00727D7F /* StaticTableViewDataSource.swift */, 5807E2BF2432038B00F5FF30 /* String+Split.swift */, 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */, - 587AD7C523421D7000E93A53 /* TunnelConfiguration.swift */, - 58AEEF6A2344A46200C9BBD5 /* TunnelConfigurationManager.swift */, 5845F837236C466400B2D93C /* TunnelControlViewController.swift */, 5835B7CB233B76CB0096D79F /* TunnelManager.swift */, 58A8BE8223A0F362006B74AC /* UIAlertController+Error.swift */, + 587AD7C523421D7000E93A53 /* TunnelSettings.swift */, + 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */, 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */, 58CCA0152242560B004F3011 /* UIColor+Palette.swift */, 58FD5BF12424F7D700112C88 /* UserInterfaceInteractionRestriction.swift */, @@ -577,6 +584,8 @@ 5877152F23981F7B001F8237 /* WireguardKeysViewController.swift */, 58C6B35322BB87C4003C19AD /* WireguardPrivateKey.swift */, 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */, + 58F3C098249B978C003E76BE /* x25519.c */, + 58F3C097249B978C003E76BE /* x25519.h */, ); path = MullvadVPN; sourceTree = "<group>"; @@ -900,6 +909,7 @@ 58EC4E6C23915325003F5C5B /* Bundle+MullvadVersion.swift in Sources */, 580EE21224B322FC00F9D8A1 /* ResultOperation.swift in Sources */, 58BA693123EADA6A009DC256 /* SimulatorTunnelProvider.swift in Sources */, + 58E6771F24ADFE7800AA26E7 /* SettingsNavigationController.swift in Sources */, 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */, 58FD5BE92419406000112C88 /* SKRequestPublisher.swift in Sources */, 582BB1B52295780F0055B6EF /* AccountExpiry.swift in Sources */, @@ -924,7 +934,7 @@ 58FAEE0124533A9C00CB0F5B /* KeychainClass.swift in Sources */, 5845F838236C466400B2D93C /* TunnelControlViewController.swift in Sources */, 58CCA0162242560B004F3011 /* UIColor+Palette.swift in Sources */, - 58AEEF6B2344A46200C9BBD5 /* TunnelConfigurationManager.swift in Sources */, + 58AEEF6B2344A46200C9BBD5 /* TunnelSettingsManager.swift in Sources */, 587CBFE322807F530028DED3 /* UIColor+Helpers.swift in Sources */, 581CBCEC2298041B00727D7F /* SettingsAppVersionCell.swift in Sources */, 58FAEDFD24533A5500CB0F5B /* KeychainMatchLimit.swift in Sources */, @@ -933,6 +943,7 @@ 58FD5BEC2420F58A00112C88 /* SKPaymentQueuePublisher.swift in Sources */, 58CC40EF24A601900019D96E /* ObserverList.swift in Sources */, 58CCA01822426713004F3011 /* AccountViewController.swift in Sources */, + 58F3C099249B978C003E76BE /* x25519.c in Sources */, 5868585524054096000B8131 /* AppButton.swift in Sources */, 5845F842236CBACD00B2D93C /* PacketTunnelIpc.swift in Sources */, 58781CC922AE7CA8009B9D8E /* RelayConstraints.swift in Sources */, @@ -940,14 +951,15 @@ 58ADDB3E227B1CD900FAFEA7 /* MullvadRpc.swift in Sources */, 580EE20F24B322E700F9D8A1 /* TransformOperation.swift in Sources */, 58B8743222B25A7600015324 /* WireguardAssociatedAddresses.swift in Sources */, - 587B08E0229433EB000E6F17 /* LoginState.swift in Sources */, 58C6B34F22BB7AC0003C19AD /* IPAddressRange.swift in Sources */, + 58F3C09C249B99DD003E76BE /* Curve25519.swift in Sources */, 58DF28A52417CB4B00E836B0 /* AppStorePaymentManager.swift in Sources */, 580EE22124B3240100F9D8A1 /* TransformOperationObserver.swift in Sources */, 582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */, 5873884D239E6D7E00E96C4E /* EmbeddedViewContainerView.swift in Sources */, 582650862384116F00FA7A86 /* ReplaceNilWithError.swift in Sources */, 58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */, + 58B9EB132488ED2100095626 /* AlertPresenter.swift in Sources */, 587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */, 5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */, 580EE20624B3222200F9D8A1 /* ExclusivityController.swift in Sources */, @@ -981,8 +993,8 @@ 5896AE84246D5889005B36CB /* CustomDateComponentsFormatting.swift in Sources */, 588AE72F2362001F009F9F2E /* MutuallyExclusive.swift in Sources */, 5888AD89227B18C40051EB06 /* RelayList.swift in Sources */, - 587AD7C623421D7000E93A53 /* TunnelConfiguration.swift in Sources */, 580EE21824B3235100F9D8A1 /* AnyOperationObserver.swift in Sources */, + 587AD7C623421D7000E93A53 /* TunnelSettings.swift in Sources */, 58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */, 58561C99239A5D1500BD6B5E /* IPEndpoint.swift in Sources */, 58FD5BF22424F7D700112C88 /* UserInterfaceInteractionRestriction.swift in Sources */, @@ -1000,6 +1012,7 @@ files = ( 5845F83C236C72E300B2D93C /* AutoDisposableSink.swift in Sources */, 5860F1C423A8D25F00CEA666 /* WireguardConfiguration.swift in Sources */, + 58F3C09D249B99DD003E76BE /* Curve25519.swift in Sources */, 580EE21F24B3237F00F9D8A1 /* OutputOperation.swift in Sources */, 580EE20224B321DB00F9D8A1 /* OperationProtocol.swift in Sources */, 58FAEE0224533ABB00CB0F5B /* KeychainMatchLimit.swift in Sources */, @@ -1010,18 +1023,18 @@ 580EE20724B3222400F9D8A1 /* ExclusivityController.swift in Sources */, 58F840B02464382C0044E708 /* KeychainItemRevision.swift in Sources */, 58C6B35122BB7CFD003C19AD /* IPAddressRange.swift in Sources */, - 587AD7C723421D8600E93A53 /* TunnelConfiguration.swift in Sources */, + 587AD7C723421D8600E93A53 /* TunnelSettings.swift in Sources */, 58F3C0962492617E003E76BE /* AsyncOperation.swift in Sources */, 580EE22924B3289300F9D8A1 /* AssociatedValue.swift in Sources */, 58BFA5C322A7C93400A6173D /* RelayList.swift in Sources */, 58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */, - 587AD7C82342237300E93A53 /* TunnelManager.swift in Sources */, 5840250222B1124600E4CFEC /* IpAddress+Codable.swift in Sources */, 58BA693223EAE1AE009DC256 /* SimulatorTunnelProvider.swift in Sources */, 580EE21924B3235100F9D8A1 /* AnyOperationObserver.swift in Sources */, 580EE21324B322FC00F9D8A1 /* ResultOperation.swift in Sources */, 58C6B36522C10596003C19AD /* AnyIPEndpoint+Wireguard.swift in Sources */, 58CE5E7C224146470008646E /* PacketTunnelProvider.swift in Sources */, + 58F3C09A249B9852003E76BE /* x25519.c in Sources */, 58FAEDF1245069CA00CB0F5B /* KeychainAttributes.swift in Sources */, 586AA296234B696B00502875 /* WireguardAssociatedAddresses.swift in Sources */, 58BA692F23E99F5B009DC256 /* Locking.swift in Sources */, @@ -1032,7 +1045,7 @@ 58CC40F024A602780019D96E /* ObserverList.swift in Sources */, 58C6B35522BB87C4003C19AD /* WireguardPrivateKey.swift in Sources */, 58FAEE0424533AC000CB0F5B /* KeychainClass.swift in Sources */, - 58AEEF6C2344A49D00C9BBD5 /* TunnelConfigurationManager.swift in Sources */, + 58AEEF6C2344A49D00C9BBD5 /* TunnelSettingsManager.swift in Sources */, 58C6B35F22BBBFE3003C19AD /* Data+HexCoding.swift in Sources */, 5840250522B11AB700E4CFEC /* MullvadEndpoint.swift in Sources */, 582650872384117900FA7A86 /* ReplaceNilWithError.swift in Sources */, @@ -1050,7 +1063,6 @@ 580EE22524B3243100F9D8A1 /* AsyncBlockOperation.swift in Sources */, 580EE20D24B3225F00F9D8A1 /* DelayOperation.swift in Sources */, 588534BF246193D90018B744 /* AutomaticKeyRotationManager.swift in Sources */, - 584B26FF237435A90073B10E /* RelaySelector+RelayCache.swift in Sources */, 58781CCE22AE8918009B9D8E /* RelayConstraints.swift in Sources */, 58781CD522AFBA39009B9D8E /* RelaySelector.swift in Sources */, 580EE21C24B3236900F9D8A1 /* InputOperation.swift in Sources */, diff --git a/ios/MullvadVPN/Account.swift b/ios/MullvadVPN/Account.swift index ade6405da6..300e5d277b 100644 --- a/ios/MullvadVPN/Account.swift +++ b/ios/MullvadVPN/Account.swift @@ -6,89 +6,11 @@ // Copyright © 2019 Mullvad VPN AB. All rights reserved. // -import Combine import Foundation import NetworkExtension import StoreKit import os -/// A enum describing the errors emitted by `Account` -enum AccountError: Error { - /// A failure to perform the login - case login(AccountLoginError) - - /// A failure to login with the new account - case createNew(CreateAccountError) - - /// A failure to log out - case logout(TunnelManagerError) -} - -/// A enum describing the error emitted during login -enum AccountLoginError: Error { - case rpc(MullvadRpc.Error) - case tunnelConfiguration(TunnelManagerError) -} - -enum CreateAccountError: Error { - case rpc(MullvadRpc.Error) - case tunnelConfiguration(TunnelManagerError) -} - -extension AccountError: LocalizedError { - var errorDescription: String? { - switch self { - case .login: - return NSLocalizedString("Log in error", comment: "") - - case .logout: - return NSLocalizedString("Log out error", comment: "") - - case .createNew: - return NSLocalizedString("Create account error", comment: "") - } - } - - var failureReason: String? { - switch self { - case .createNew(.rpc): - return NSLocalizedString("Failed to create new account", comment: "") - - case .login(.rpc(.server(let serverError))) where serverError.code == .accountDoesNotExist: - return NSLocalizedString("Invalid account", comment: "") - - case .login(.rpc(.network)): - return NSLocalizedString("Network error", comment: "") - - case .login(.rpc(.server)): - return NSLocalizedString("Server error", comment: "") - - case .login(.tunnelConfiguration(.setAccount(let setAccountError))), - .createNew(.tunnelConfiguration(.setAccount(let setAccountError))): - switch setAccountError { - case .pushWireguardKey(.network): - return NSLocalizedString("Network error", comment: "") - - case .pushWireguardKey(.server(let serverError)): - return serverError.errorDescription ?? serverError.message - - case .setup(.saveTunnel(let systemError as NEVPNError)) - where systemError.code == .configurationReadWriteFailed: - return NSLocalizedString("Permission denied to add a VPN profile", comment: "") - - default: - return NSLocalizedString("Internal error", comment: "") - } - - case .logout: - return NSLocalizedString("Internal error", comment: "") - - default: - return nil - } - } -} - /// A enum holding the `UserDefaults` string keys private enum UserDefaultsKeys: String { case isAgreedToTermsOfService = "isAgreedToTermsOfService" @@ -99,14 +21,25 @@ private enum UserDefaultsKeys: String { /// A class that groups the account related operations class Account { + enum Error: ChainedError { + /// A failure to create the new account token + case createAccount(MullvadRpc.Error) + + /// A failure to verify the account token + case verifyAccount(MullvadRpc.Error) + + /// A failure to configure a tunnel + case tunnelConfiguration(TunnelManager.Error) + } + /// A notification name used to broadcast the changes to account expiry static let didUpdateAccountExpiryNotification = Notification.Name("didUpdateAccountExpiry") /// A notification userInfo key that holds the `Date` with the new account expiry static let newAccountExpiryUserInfoKey = "newAccountExpiry" + /// A shared instance of `Account` static let shared = Account() - private let rpc = MullvadRpc.withEphemeralURLSession() /// Returns true if user agreed to terms of service, otherwise false var isAgreedToTermsOfService: Bool { @@ -114,8 +47,13 @@ class Account { } /// Returns the currently used account token - var token: String? { - return UserDefaults.standard.string(forKey: UserDefaultsKeys.accountToken.rawValue) + private(set) var token: String? { + set { + UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.accountToken.rawValue) + } + get { + return UserDefaults.standard.string(forKey: UserDefaultsKeys.accountToken.rawValue) + } } var formattedToken: String? { @@ -123,10 +61,23 @@ class Account { } /// Returns the account expiry for the currently used account token - var expiry: Date? { - return UserDefaults.standard.object(forKey: UserDefaultsKeys.accountExpiry.rawValue) as? Date + private(set) var expiry: Date? { + set { + UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.accountExpiry.rawValue) + } + get { + return UserDefaults.standard.object(forKey: UserDefaultsKeys.accountExpiry.rawValue) as? Date + } + } + + private enum ExclusivityCategory { + case exclusive } + private let rpc = MullvadRpc.withEphemeralURLSession() + private let operationQueue = OperationQueue() + private lazy var exclusivityController = ExclusivityController<ExclusivityCategory>(operationQueue: operationQueue) + var isLoggedIn: Bool { return token != nil } @@ -136,61 +87,134 @@ class Account { UserDefaults.standard.set(true, forKey: UserDefaultsKeys.isAgreedToTermsOfService.rawValue) } - func loginWithNewAccount() -> AnyPublisher<String, AccountError> { - return rpc.createAccount() - .mapError { CreateAccountError.rpc($0) } - .flatMap { (newAccountToken) in - TunnelManager.shared.setAccount(accountToken: newAccountToken) - .mapError { CreateAccountError.tunnelConfiguration($0) } - .map { (newAccountToken, Date()) } - }.mapError { AccountError.createNew($0) } - .receive(on: DispatchQueue.main) - .map { (accountToken, expiry) -> String in - self.saveAccountToPreferences(accountToken: accountToken, expiry: expiry) + func loginWithNewAccount(completionHandler: @escaping (Result<(String, Date), Error>) -> Void) { + let operation = rpc.createAccount().operation() + + operation.addDidFinishBlockObserver({ (operation, result) in + DispatchQueue.main.async { + switch result { + case .success(let newAccountToken): + let expiry = Date() + self.setupTunnel(accountToken: newAccountToken, expiry: expiry) { (result) in + completionHandler(result.map { (newAccountToken, expiry) }) + } + + case .failure(let error): + completionHandler(.failure(.createAccount(error))) + } + } + }) - return accountToken - }.eraseToAnyPublisher() + exclusivityController.addOperation(operation, categories: [.exclusive]) } /// Perform the login and save the account token along with expiry (if available) to the /// application preferences. - func login(with accountToken: String) -> AnyPublisher<(), AccountError> { - return rpc.getAccountExpiry(accountToken: accountToken) - .mapError { AccountLoginError.rpc($0) } - .flatMap { (expiry) in - TunnelManager.shared.setAccount(accountToken: accountToken) - .mapError { AccountLoginError.tunnelConfiguration($0) } - .map { expiry } - }.mapError { AccountError.login($0) } - .receive(on: DispatchQueue.main) - .map { (expiry) in - self.saveAccountToPreferences(accountToken: accountToken, expiry: expiry) - }.eraseToAnyPublisher() + func login(with accountToken: String, completionHandler: @escaping (Result<Date, Error>) -> Void) { + let operation = rpc.getAccountExpiry(accountToken: accountToken) + .operation() + + operation.addDidFinishBlockObserver { (operation, result) in + DispatchQueue.main.async { + switch result { + case .success(let expiry): + self.setupTunnel(accountToken: accountToken, expiry: expiry) { (result) in + completionHandler(result.map { expiry }) + } + + case .failure(let error): + completionHandler(.failure(.verifyAccount(error))) + } + } + } + + exclusivityController.addOperation(operation, categories: [.exclusive]) } /// Perform the logout by erasing the account token and expiry from the application preferences. - func logout() -> AnyPublisher<(), AccountError> { - return TunnelManager.shared.unsetAccount() - .receive(on: DispatchQueue.main) - .mapError { AccountError.logout($0) } - .map(self.removeAccountFromPreferences) - .eraseToAnyPublisher() + func logout(completionHandler: @escaping (Result<(), Error>) -> Void) { + let operation = ResultOperation<(), Error> { (finish) in + TunnelManager.shared.unsetAccount { (result) in + DispatchQueue.main.async { + switch result { + case .success: + self.removeFromPreferences() + + finish(.success(())) + + case .failure(let error): + finish(.failure(.tunnelConfiguration(error))) + } + } + } + } + + operation.addDidFinishBlockObserver { (operation, result) in + DispatchQueue.main.async { + completionHandler(result) + } + } + + exclusivityController.addOperation(operation, categories: [.exclusive]) } - private func saveAccountToPreferences(accountToken: String, expiry: Date) { - let preferences = UserDefaults.standard + func updateAccountExpiry() { + let makeRequest = ResultOperation { () -> MullvadRpc.Request<Date>? in + return self.token.flatMap { (accountToken) -> MullvadRpc.Request<Date>? in + self.rpc.getAccountExpiry(accountToken: accountToken) + } + } + + let sendRequest = rpc.getAccountExpiry() + .injectResult(from: makeRequest) + + sendRequest.addDidFinishBlockObserver { (operation, result) in + DispatchQueue.main.async { + switch result { + case .success(let expiry): + self.expiry = expiry + self.postExpiryUpdateNotification(newExpiry: expiry) + + case .failure(let error): + error.logChain(message: "Failed to update account expiry") + } + } + } + + exclusivityController.addOperations([makeRequest, sendRequest], categories: [.exclusive]) + } - preferences.set(accountToken, forKey: UserDefaultsKeys.accountToken.rawValue) - preferences.set(expiry, forKey: UserDefaultsKeys.accountExpiry.rawValue) + private func setupTunnel(accountToken: String, expiry: Date, completionHandler: @escaping (Result<(), Error>) -> Void) { + TunnelManager.shared.setAccount(accountToken: accountToken) { (managerResult) in + DispatchQueue.main.async { + switch managerResult { + case .success: + self.token = accountToken + self.expiry = expiry + + completionHandler(.success(())) + + case .failure(let error): + completionHandler(.failure(.tunnelConfiguration(error))) + } + } + } } - private func removeAccountFromPreferences() { + private func removeFromPreferences() { let preferences = UserDefaults.standard preferences.removeObject(forKey: UserDefaultsKeys.accountToken.rawValue) preferences.removeObject(forKey: UserDefaultsKeys.accountExpiry.rawValue) } + + fileprivate func postExpiryUpdateNotification(newExpiry: Date) { + NotificationCenter.default.post( + name: Self.didUpdateAccountExpiryNotification, + object: self, userInfo: [Self.newAccountExpiryUserInfoKey: newExpiry] + ) + } } extension Account: AppStorePaymentObserver { diff --git a/ios/MullvadVPN/AlertPresenter.swift b/ios/MullvadVPN/AlertPresenter.swift new file mode 100644 index 0000000000..9718842c3c --- /dev/null +++ b/ios/MullvadVPN/AlertPresenter.swift @@ -0,0 +1,97 @@ +// +// AlertPresenter.swift +// MullvadVPN +// +// Created by pronebird on 04/06/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import UIKit + +private let kUIAlertControllerDidDissmissNotification = Notification.Name("UIAlertControllerDidDismiss") + +class AlertPresenter { + private enum ExclusivityCategory { + case exclusive + } + + private let operationQueue = OperationQueue() + private lazy var exclusivityController = ExclusivityController<ExclusivityCategory>(operationQueue: operationQueue) + + init() { + _ = AlertPresenterUIKitHooks.once + } + + func enqueue(_ alertController: UIAlertController, presentingController: UIViewController, presentCompletion: (() -> Void)? = nil) { + let operation = PresentAlertOperation( + alertController: alertController, + presentingController: presentingController, + presentCompletion: presentCompletion + ) + + exclusivityController.addOperation(operation, categories: [.exclusive]) + } + +} + +private class PresentAlertOperation: AsyncOperation { + private let alertController: UIAlertController + private let presentingController: UIViewController + private var dismissalObserver: NSObjectProtocol? + private let presentCompletion: (() -> Void)? + + init(alertController: UIAlertController, presentingController: UIViewController, presentCompletion: (() -> Void)? = nil) { + self.alertController = alertController + self.presentingController = presentingController + self.presentCompletion = presentCompletion + + super.init() + } + + override func main() { + DispatchQueue.main.async { + self.dismissalObserver = NotificationCenter.default.addObserver( + forName: kUIAlertControllerDidDissmissNotification, + object: self.alertController, + queue: nil, + using: { [weak self] (note) in + self?.finish() + }) + + self.presentingController.present(self.alertController, animated: true, completion: self.presentCompletion) + } + } +} + +/// A helper struct that swizzles `viewDidDisappear` on `UIAlertController` in order to be able to +/// detect when the controller disappears. +/// The event is broadcasted via `kUIAlertControllerDidDissmissNotification` notification. +private struct AlertPresenterUIKitHooks { + typealias MethodType = @convention(c) (UIAlertController, Selector, Bool) -> Void + typealias BlockImpType = @convention(block) (UIAlertController, Bool) -> Void + + static let once = AlertPresenterUIKitHooks() + + private init() { + let originalSelector = #selector(UIAlertController.viewDidDisappear(_:)) + let originalMethod = class_getInstanceMethod(UIAlertController.self, originalSelector)! + + var originalIMP: IMP? = nil + let swizzledBlockIMP: BlockImpType = { (receiver, animated) in + let superIMP = originalIMP.map { unsafeBitCast($0, to: MethodType.self) } + superIMP?(receiver, originalSelector, animated) + + Self.handleViewDidDisappear(receiver, animated) + } + + let swizzledIMP = imp_implementationWithBlock(unsafeBitCast(swizzledBlockIMP, to: AnyObject.self)) + originalIMP = method_setImplementation(originalMethod, swizzledIMP) + } + + private static func handleViewDidDisappear(_ alertController: UIAlertController, _ animated: Bool) { + if alertController.presentingViewController == nil { + NotificationCenter.default.post(name: kUIAlertControllerDidDissmissNotification, object: alertController) + } + } +} diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 57ffac8c90..16062c16b9 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -6,7 +6,6 @@ // Copyright © 2019 Mullvad VPN AB. All rights reserved. // -import Combine import UIKit import StoreKit @@ -21,9 +20,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let simulatorTunnelProvider = SimulatorTunnelProviderHost() #endif - private var loadTunnelSubscriber: AnyCancellable? - private var refreshTunnelSubscriber: AnyCancellable? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { #if targetEnvironment(simulator) SimulatorTunnelProvider.shared.delegate = simulatorTunnelProvider @@ -31,11 +27,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let accountToken = Account.shared.token - loadTunnelSubscriber = TunnelManager.shared.loadTunnel(accountToken: accountToken) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { (completion) in - if case .failure(let error) = completion { - fatalError("Failed to restore the account: \(error.localizedDescription)") + RelayCache.shared.updateRelays() + + TunnelManager.shared.loadTunnel(accountToken: accountToken) { (result) in + DispatchQueue.main.async { + if case .failure(let error) = result { + fatalError(error.displayChain(message: "Failed to load the tunnel for account")) } let rootViewController = RootContainerViewController() @@ -57,16 +54,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } self.window?.rootViewController = rootViewController - }) + } + } return true } func applicationDidBecomeActive(_ application: UIApplication) { - refreshTunnelSubscriber = TunnelManager.shared.refreshTunnelState() - .sink(receiveCompletion: { (_) in - // no-op - }) + TunnelManager.shared.refreshTunnelState(completionHandler: nil) } private func didPresentTheMainController() { diff --git a/ios/MullvadVPN/AutomaticKeyRotationManager.swift b/ios/MullvadVPN/AutomaticKeyRotationManager.swift index abc28d9eb0..1755c6f4af 100644 --- a/ios/MullvadVPN/AutomaticKeyRotationManager.swift +++ b/ios/MullvadVPN/AutomaticKeyRotationManager.swift @@ -6,7 +6,6 @@ // Copyright © 2020 Mullvad VPN AB. All rights reserved. // -import Combine import Foundation import os @@ -16,64 +15,67 @@ private let kRetryIntervalOnFailure = 300 /// A private key rotation interval (in days) private let kRotationInterval = 4 +/// A struct describing the key rotation result +struct KeyRotationResult { + var isNew: Bool + var creationDate: Date + var publicKey: WireguardPublicKey +} + class AutomaticKeyRotationManager { - enum Error: Swift.Error { + enum Error: ChainedError { /// An RPC failure case rpc(MullvadRpc.Error) - /// A failure to read the tunnel configuration - case readTunnelConfiguration(TunnelConfigurationManager.Error) + /// A failure to read the tunnel settings + case readTunnelSettings(TunnelSettingsManager.Error) - /// A failure to update tunnel configuration - case updateTunnelConfiguration(TunnelConfigurationManager.Error) + /// A failure to update tunnel settings + case updateTunnelSettings(TunnelSettingsManager.Error) - var localizedDescription: String { + var errorDescription: String? { switch self { - case .rpc(let error): - return "Rpc error: \(error.localizedDescription)" - case .readTunnelConfiguration(let error): - return "Read configuration error: \(error.localizedDescription)" - case .updateTunnelConfiguration(let error): - return "Update configuration error: \(error.localizedDescription)" + case .rpc: + return "RPC error" + case .readTunnelSettings: + return "Read tunnel settings error" + case .updateTunnelSettings: + return "Update tunnel settings error" } } } - struct KeyRotationEvent { - var isNew: Bool - var creationDate: Date - var publicKey: WireguardPublicKey - } - private let rpc = MullvadRpc.withEphemeralURLSession() private let persistentKeychainReference: Data - private var rotateKeySubscriber: AnyCancellable? /// A dispatch queue used for synchronization - private let dispatchQueue = DispatchQueue(label: "net.mullvad.vpn.key-manager", qos: .background) + private let dispatchQueue = DispatchQueue(label: "net.mullvad.vpn.key-manager", qos: .utility) /// A timer source used to schedule a delayed key rotation private var timerSource: DispatchSourceTimer? /// Internal lock used for access synchronization to public members of this class - private let lock = NSLock() + private let stateLock = NSLock() /// Internal variable indicating that the key rotation has already started private var isAutomaticRotationEnabled = false + /// An RPC request for replacing the key on server + private var request: MullvadRpc.Request<WireguardAssociatedAddresses>? + /// A variable backing the `eventHandler` public property - private var _eventHandler: ((KeyRotationEvent) -> Void)? + private var _eventHandler: ((KeyRotationResult) -> Void)? /// An event handler that's invoked when key rotation occurred - var eventHandler: ((KeyRotationEvent) -> Void)? { + var eventHandler: ((KeyRotationResult) -> Void)? { get { - lock.withCriticalBlock { + stateLock.withCriticalBlock { self._eventHandler } } set { - lock.withCriticalBlock { + stateLock.withCriticalBlock { self._eventHandler = newValue } } @@ -83,7 +85,7 @@ class AutomaticKeyRotationManager { self.persistentKeychainReference = persistentKeychainReference } - func startAutomaticRotation() { + func startAutomaticRotation(completionHandler: @escaping () -> Void) { dispatchQueue.async { guard !self.isAutomaticRotationEnabled else { return } @@ -91,140 +93,172 @@ class AutomaticKeyRotationManager { self.isAutomaticRotationEnabled = true self.performKeyRotation() + + completionHandler() } } - func stopAutomaticRotation() { + func stopAutomaticRotation(completionHandler: @escaping () -> Void) { dispatchQueue.async { guard self.isAutomaticRotationEnabled else { return } os_log(.default, log: tunnelProviderLog, "Stop automatic key rotation") self.isAutomaticRotationEnabled = false - self.rotateKeySubscriber?.cancel() + + self.request?.cancel() + self.request = nil + self.timerSource?.cancel() + + completionHandler() } } private func performKeyRotation() { - rotateKeySubscriber = tryRotatingPrivateKey() - .receive(on: dispatchQueue) - .sink(receiveCompletion: { [weak self] (completion) in - guard let self = self else { return } + let result = TunnelSettingsManager.load(searchTerm: .persistentReference(persistentKeychainReference)) + + switch result { + case .success(let keychainEntry): + let currentPrivateKey = keychainEntry.tunnelSettings.interface.privateKey - switch completion { - case .finished: - break + if Self.shouldRotateKey(creationDate: currentPrivateKey.creationDate) { + let request = replaceKey(accountToken: keychainEntry.accountToken, oldPublicKey: currentPrivateKey.publicKey) { (result) in + let result = result.map { (tunnelSettings) -> KeyRotationResult in + let newPrivateKey = tunnelSettings.interface.privateKey - case .failure(let error): - os_log(.error, log: tunnelProviderLog, - "Failed to rotate the private key: %{public}s. Retry in %d seconds.", - error.localizedDescription, - kRetryIntervalOnFailure) + return KeyRotationResult( + isNew: true, + creationDate: newPrivateKey.creationDate, + publicKey: newPrivateKey.publicKey + ) + } - self.scheduleRetry(wallDeadline: .now() + .seconds(kRetryIntervalOnFailure)) + self.didCompleteKeyRotation(result: result) } - }) { [weak self] (keyRotationEvent) in - guard let self = self else { return } - if keyRotationEvent.isNew { - os_log(.default, log: tunnelProviderLog, "Finished private key rotation") + self.request = request + } else { + let event = KeyRotationResult( + isNew: false, + creationDate: currentPrivateKey.creationDate, + publicKey: currentPrivateKey.publicKey + ) - self.eventHandler?(keyRotationEvent) - } + self.didCompleteKeyRotation(result: .success(event)) + } - if let rotationDate = Self.nextRotation(creationDate: keyRotationEvent.creationDate) { - let interval = rotationDate.timeIntervalSinceNow + case .failure(let error): + self.didCompleteKeyRotation(result: .failure(.readTunnelSettings(error))) + } + } - os_log(.default, log: tunnelProviderLog, - "Next private key rotation on %{public}s", "\(rotationDate)") + private func replaceKey( + accountToken: String, + oldPublicKey: WireguardPublicKey, + completionHandler: @escaping (Result<TunnelSettings, Error>) -> Void) -> MullvadRpc.Request<WireguardAssociatedAddresses> + { + let newPrivateKey = WireguardPrivateKey() - self.scheduleRetry(wallDeadline: .now() + .seconds(Int(interval))) - } else { - os_log(.error, log: tunnelProviderLog, - "Failed to compute the next private rotation date. Retry in %d seconds.") + let request = rpc.replaceWireguardKey( + accountToken: accountToken, + oldPublicKey: oldPublicKey.rawRepresentation, + newPublicKey: newPrivateKey.publicKey.rawRepresentation + ) - self.scheduleRetry(wallDeadline: .now() + .seconds(kRetryIntervalOnFailure)) + request.start { (result) in + self.dispatchQueue.async { + let updateResult = result.mapError { (error) -> Error in + return .rpc(error) + }.flatMap { (addresses) -> Result<TunnelSettings, Error> in + self.updateTunnelSettings(privateKey: newPrivateKey, addresses: addresses) } + completionHandler(updateResult) + } } + + return request } - private func scheduleRetry(wallDeadline: DispatchWallTime) { - let timerSource = DispatchSource.makeTimerSource(queue: dispatchQueue) - timerSource.setEventHandler { [weak self] in - self?.performKeyRotation() + private func updateTunnelSettings(privateKey: WireguardPrivateKey, addresses: WireguardAssociatedAddresses) -> Result<TunnelSettings, Error> { + let updateResult = TunnelSettingsManager.update(searchTerm: .persistentReference(self.persistentKeychainReference)) + { (tunnelSettings) in + tunnelSettings.interface.privateKey = privateKey + tunnelSettings.interface.addresses = [ + addresses.ipv4Address, + addresses.ipv6Address + ] } - timerSource.schedule(wallDeadline: wallDeadline) - timerSource.activate() - - self.timerSource = timerSource + return updateResult.mapError { .updateTunnelSettings($0) } } - private func tryRotatingPrivateKey() -> AnyPublisher<KeyRotationEvent, Error> { - return TunnelConfigurationManager - .load(searchTerm: .persistentReference(persistentKeychainReference)) - .mapError { .readTunnelConfiguration($0) } - .publisher - .flatMap { (keychainEntry) -> AnyPublisher<KeyRotationEvent, Error> in - let currentPrivateKey = keychainEntry.tunnelConfiguration.interface.privateKey + private func didCompleteKeyRotation(result: Result<KeyRotationResult, Error>) { + var nextRotationTime: DispatchWallTime? - if Self.shouldRotateKey(creationDate: currentPrivateKey.creationDate) { - return self.replaceWireguardKey( - accountToken: keychainEntry.accountToken, - oldPublicKey: currentPrivateKey.publicKey - ).map({ (newTunnelConfiguration) -> KeyRotationEvent in - let newPrivateKey = newTunnelConfiguration.interface.privateKey + switch result { + case .success(let event): + if event.isNew { + os_log(.default, log: tunnelProviderLog, "Finished private key rotation") - return KeyRotationEvent( - isNew: true, - creationDate: newPrivateKey.creationDate, - publicKey: newPrivateKey.publicKey - ) - }).eraseToAnyPublisher() - } else { - let result = KeyRotationEvent( - isNew: false, - creationDate: currentPrivateKey.creationDate, - publicKey: currentPrivateKey.publicKey - ) + eventHandler?(event) + } - return Result.Publisher(result).eraseToAnyPublisher() - } - }.eraseToAnyPublisher() + if let rotationDate = Self.nextRotation(creationDate: event.creationDate) { + let interval = rotationDate.timeIntervalSinceNow + + os_log(.default, log: tunnelProviderLog, + "Next private key rotation on %{public}s", "\(rotationDate)") + + nextRotationTime = .now() + .seconds(Int(interval)) + } else { + os_log(.error, log: tunnelProviderLog, + "Failed to compute the next private rotation date. Retry in %d seconds.") + + nextRotationTime = .now() + .seconds(kRetryIntervalOnFailure) + } + + case .failure(.rpc(.network(let urlError))) where urlError.code == .cancelled: + os_log(.default, log: tunnelProviderLog, "Key rotation was cancelled") + break + + case .failure(let error): + os_log(.error, log: tunnelProviderLog, + "Failed to rotate the private key: %{public}s. Retry in %d seconds.", + error.localizedDescription, + kRetryIntervalOnFailure) + + nextRotationTime = .now() + .seconds(kRetryIntervalOnFailure) + } + + if let nextRotationTime = nextRotationTime, isAutomaticRotationEnabled { + scheduleRetry(wallDeadline: nextRotationTime) + } } - private func replaceWireguardKey(accountToken: String, oldPublicKey: WireguardPublicKey) - -> AnyPublisher<TunnelConfiguration, Error> - { - let newPrivateKey = WireguardPrivateKey() + private func scheduleRetry(wallDeadline: DispatchWallTime) { + let timerSource = DispatchSource.makeTimerSource(queue: dispatchQueue) + timerSource.setEventHandler { [weak self] in + guard let self = self else { return } - return rpc.replaceWireguardKey( - accountToken: accountToken, - oldPublicKey: oldPublicKey.rawRepresentation, - newPublicKey: newPrivateKey.publicKey.rawRepresentation) - .mapError { .rpc($0) } - .flatMap { (addresses) in - TunnelConfigurationManager - .update(searchTerm: .persistentReference(self.persistentKeychainReference)) - { (tunnelConfiguration) in - tunnelConfiguration.interface.privateKey = newPrivateKey - tunnelConfiguration.interface.addresses = [ - addresses.ipv4Address, - addresses.ipv6Address - ] - } - .mapError { .updateTunnelConfiguration($0) } - .publisher - }.eraseToAnyPublisher() + if self.isAutomaticRotationEnabled { + self.performKeyRotation() + } + } + + timerSource.schedule(wallDeadline: wallDeadline) + timerSource.activate() + + self.timerSource = timerSource } - class func nextRotation(creationDate: Date) -> Date? { + private class func nextRotation(creationDate: Date) -> Date? { return Calendar.current.date(byAdding: .day, value: kRotationInterval, to: creationDate) } - class func shouldRotateKey(creationDate: Date) -> Bool { + private class func shouldRotateKey(creationDate: Date) -> Bool { return nextRotation(creationDate: creationDate) .map { $0 <= Date() } ?? false } + } diff --git a/ios/MullvadVPN/Base.lproj/Main.storyboard b/ios/MullvadVPN/Base.lproj/Main.storyboard index 1477e2a02e..f1a8fcd7fb 100644 --- a/ios/MullvadVPN/Base.lproj/Main.storyboard +++ b/ios/MullvadVPN/Base.lproj/Main.storyboard @@ -869,13 +869,13 @@ </objects> <point key="canvasLocation" x="2576.8000000000002" y="-1258.0209895052474"/> </scene> - <!--Navigation Controller--> + <!--Settings Navigation Controller--> <scene sceneID="er3-W2-NkS"> <objects> - <navigationController storyboardIdentifier="Settings" id="Kqv-qu-mfF" sceneMemberID="viewController"> + <navigationController storyboardIdentifier="Settings" id="Kqv-qu-mfF" customClass="SettingsNavigationController" customModule="MullvadVPN" customModuleProvider="target" sceneMemberID="viewController"> <navigationItem key="navigationItem" id="7IR-NQ-qLb"/> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" largeTitles="YES" id="7PK-0x-byW" customClass="CustomNavigationBar" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="0.0" y="0.0" width="375" height="108"/> + <rect key="frame" x="0.0" y="0.0" width="375" height="96"/> <autoresizingMask key="autoresizingMask"/> <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> </navigationBar> diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift index a388c5e23f..c2bcaf0e95 100644 --- a/ios/MullvadVPN/ConnectViewController.swift +++ b/ios/MullvadVPN/ConnectViewController.swift @@ -6,21 +6,22 @@ // Copyright © 2019 Mullvad VPN AB. All rights reserved. // -import Combine import UIKit import NetworkExtension import os -class ConnectViewController: UIViewController, RootContainment, TunnelControlViewControllerDelegate { +class ConnectViewController: UIViewController, + RootContainment, + TunnelControlViewControllerDelegate, + TunnelObserver +{ @IBOutlet var secureLabel: UILabel! @IBOutlet var countryLabel: UILabel! @IBOutlet var cityLabel: UILabel! @IBOutlet var connectionPanel: ConnectionPanelView! - private var setRelaysSubscriber: AnyCancellable? - private var startStopTunnelSubscriber: AnyCancellable? - private var tunnelStateSubscriber: AnyCancellable? + private let alertPresenter = AlertPresenter() override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent @@ -55,9 +56,8 @@ class ConnectViewController: UIViewController, RootContainment, TunnelControlVie connectionPanel.collapseButton.addTarget(self, action: #selector(handleConnectionPanelButton(_:)), for: .touchUpInside) - tunnelStateSubscriber = TunnelManager.shared.$tunnelState - .receive(on: DispatchQueue.main) - .assign(to: \.tunnelState, on: self) + TunnelManager.shared.addObserver(self) + self.tunnelState = TunnelManager.shared.tunnelState } override func viewDidAppear(_ animated: Bool) { @@ -74,6 +74,18 @@ class ConnectViewController: UIViewController, RootContainment, TunnelControlVie } } + // MARK: - TunnelObserver + + func tunnelStateDidChange(tunnelState: TunnelState) { + DispatchQueue.main.async { + self.tunnelState = tunnelState + } + } + + func tunnelPublicKeyDidChange(publicKey: WireguardPublicKey?) { + // no-op + } + // MARK: - TunnelControlViewControllerDelegate func tunnelControlViewController(_ controller: TunnelControlViewController, handleAction action: TunnelControlAction) { @@ -129,29 +141,47 @@ class ConnectViewController: UIViewController, RootContainment, TunnelControlVie } private func connectTunnel() { - startStopTunnelSubscriber = TunnelManager.shared.startTunnel() - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { (completion) in - if case .failure(let error) = completion { - os_log(.error, "Failed to start the tunnel: %{public}s", - error.localizedDescription) + TunnelManager.shared.startTunnel { (result) in + DispatchQueue.main.async { + switch result { + case .success: + break + + case .failure(let error): + error.logChain(message: "Failed to start the VPN tunnel") + + let alertController = UIAlertController( + title: NSLocalizedString("Failed to start the VPN tunnel", comment: ""), + message: error.errorChainDescription, + preferredStyle: .alert + ) + alertController.addAction( + UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel) + ) - self.presentError(error, preferredStyle: .alert) + self.alertPresenter.enqueue(alertController, presentingController: self) } - }) + } + } } private func disconnectTunnel() { - startStopTunnelSubscriber = TunnelManager.shared.stopTunnel() - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { (completion) in - if case .failure(let error) = completion { - os_log(.error, "Failed to stop the tunnel: %{public}s", - error.localizedDescription) + TunnelManager.shared.stopTunnel { (result) in + if case .failure(let error) = result { + error.logChain(message: "Failed to stop the VPN tunnel") - self.presentError(error, preferredStyle: .alert) - } - }) + let alertController = UIAlertController( + title: NSLocalizedString("Failed to stop the VPN tunnel", comment: ""), + message: error.errorChainDescription, + preferredStyle: .alert + ) + alertController.addAction( + UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel) + ) + + self.alertPresenter.enqueue(alertController, presentingController: self) + } + } } private func showAccountViewForExpiredAccount() { @@ -176,18 +206,18 @@ class ConnectViewController: UIViewController, RootContainment, TunnelControlVie let relayConstraints = RelayConstraints(location: .only(selectedLocation)) - setRelaysSubscriber = TunnelManager.shared.setRelayConstraints(relayConstraints) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { (completion) in - switch completion { - case .finished: - os_log(.debug, "Updated relay constraints: %{public}s", String(reflecting: relayConstraints)) - self.connectTunnel() + TunnelManager.shared.setRelayConstraints(relayConstraints) { [weak self] (result) in + DispatchQueue.main.async { + switch result { + case .success: + os_log(.debug, "Updated relay constraints: %{public}s", "\(relayConstraints)") + self?.connectTunnel() case .failure(let error): os_log(.error, "Failed to update relay constraints: %{public}s", error.localizedDescription) } - }) + } + } } } diff --git a/ios/MullvadVPN/Curve25519.swift b/ios/MullvadVPN/Curve25519.swift new file mode 100644 index 0000000000..324d84167a --- /dev/null +++ b/ios/MullvadVPN/Curve25519.swift @@ -0,0 +1,40 @@ +// +// Curve25519.swift +// MullvadVPN +// +// Created by pronebird on 18/06/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +struct Curve25519 { + + static let keyLength: Int = 32 + + static func generatePrivateKey() -> Data { + var privateKey = [UInt8](repeating: 0, count: keyLength) + privateKey.withUnsafeMutableBufferPointer { (ptr) in + curve25519_generate_private_key(ptr.baseAddress!) + } + return Data(privateKey) + } + + static func generatePublicKey(fromPrivateKey privateKey: Data) -> Data { + assert(privateKey.count == Self.keyLength) + + var publicKey = [UInt8](repeating: 0, count: keyLength) + privateKey.withUnsafeBytes { (privateKeyBytes) in + let privateKeyBytesPointer = privateKeyBytes.bindMemory(to: UInt8.self) + + publicKey.withUnsafeMutableBufferPointer { (publicKeyPointer) in + curve25519_derive_public_key( + publicKeyPointer.baseAddress!, + privateKeyBytesPointer.baseAddress! + ) + } + } + + return Data(publicKey) + } +} diff --git a/ios/MullvadVPN/DisplayChainedError.swift b/ios/MullvadVPN/DisplayChainedError.swift index 7f67e8102f..6015dc2de7 100644 --- a/ios/MullvadVPN/DisplayChainedError.swift +++ b/ios/MullvadVPN/DisplayChainedError.swift @@ -11,3 +11,115 @@ import Foundation protocol DisplayChainedError { var errorChainDescription: String? { get } } + +extension MullvadRpc.Error: DisplayChainedError { + var errorChainDescription: String? { + switch self { + case .network(let urlError): + return urlError.localizedDescription + + case .server(let serverError): + if let knownErrorDescription = serverError.errorDescription { + return knownErrorDescription + } else { + return String( + format: NSLocalizedString("Server error: %@", comment: ""), + serverError.message + ) + } + + case .encoding: + return NSLocalizedString("Server request encoding error", comment: "") + + case .decoding: + return NSLocalizedString("Server response decoding error", comment: "") + } + } +} + +extension TunnelManager.Error: DisplayChainedError { + var errorChainDescription: String? { + switch self { + case .loadAllVPNConfigurations(let systemError): + return String(format: NSLocalizedString("Failed to load system VPN configurations: %@", comment: ""), systemError.localizedDescription) + + case .reloadVPNConfiguration(let systemError): + return String(format: NSLocalizedString("Failed to reload a VPN configuration: %@", comment: ""), systemError.localizedDescription) + + case .saveVPNConfiguration(let systemError): + return String(format: NSLocalizedString("Failed to save a VPN tunnel configuration: %@", comment: ""), systemError.localizedDescription) + + case .obtainPersistentKeychainReference(_): + return NSLocalizedString("Failed to obtain the persistent keychain reference for the VPN configuration", comment: "") + + case .startVPNTunnel(let systemError): + return String(format: NSLocalizedString("System error when starting the VPN tunnel: %@", comment: ""), systemError.localizedDescription) + + case .removeVPNConfiguration(let systemError): + return String(format: NSLocalizedString("Failed to remove the system VPN configuration: %@", comment: ""), systemError.localizedDescription) + + case .removeInconsistentVPNConfiguration(let systemError): + return String(format: NSLocalizedString("Failed to remove the outdated system VPN configuration: %@", comment: ""), systemError.localizedDescription) + + case .readTunnelSettings(_): + return NSLocalizedString("Failed to read tunnel settings", comment: "") + + case .addTunnelSettings(_): + return NSLocalizedString("Failed to add tunnel settings", comment: "") + + case .updateTunnelSettings(_): + return NSLocalizedString("Failed to update tunnel settings", comment: "") + + case .removeTunnelSettings(_): + return NSLocalizedString("Failed to remove tunnel settings", comment: "") + + case .pushWireguardKey(let rpcError): + let reason = rpcError.errorChainDescription ?? "" + var message = String(format: NSLocalizedString("Failed to send the WireGuard key to server: %@", comment: ""), reason) + + if case .server(let serverError) = rpcError, serverError.code == .tooManyWireguardKeys { + message.append("\n\n") + message.append(NSLocalizedString("Remove unused WireGuard keys and try again", comment: "")) + } + + return message + + case .replaceWireguardKey(let rpcError): + let reason = rpcError.errorChainDescription ?? "" + var message = String(format: NSLocalizedString("Failed to replace the WireGuard key on server: %@", comment: ""), reason) + + if case .server(let serverError) = rpcError, serverError.code == .tooManyWireguardKeys { + message.append("\n\n") + message.append(NSLocalizedString("Remove unused WireGuard keys and try again", comment: "")) + } + + return message + + case .removeWireguardKey: + // This error is never displayed anywhere + return nil + + case .verifyWireguardKey(let rpcError): + let reason = rpcError.errorChainDescription ?? "" + + return String(format: NSLocalizedString("Failed to verify the WireGuard key on server: %@", comment: ""), reason) + + case .missingAccount: + return NSLocalizedString("Internal error: missing account", comment: "") + } + } +} + +extension Account.Error: DisplayChainedError { + + var errorChainDescription: String? { + switch self { + case .createAccount(let rpcError), .verifyAccount(let rpcError): + return rpcError.errorChainDescription + + case .tunnelConfiguration(let tunnelError): + return tunnelError.errorChainDescription + } + } + +} diff --git a/ios/MullvadVPN/KeychainError.swift b/ios/MullvadVPN/KeychainError.swift index 58af6c7ecc..4758d08b66 100644 --- a/ios/MullvadVPN/KeychainError.swift +++ b/ios/MullvadVPN/KeychainError.swift @@ -17,7 +17,6 @@ extension Keychain { return SecCopyErrorMessageString(code, nil) as String? } } - } diff --git a/ios/MullvadVPN/LoginState.swift b/ios/MullvadVPN/LoginState.swift deleted file mode 100644 index a28fefd39b..0000000000 --- a/ios/MullvadVPN/LoginState.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// LoginState.swift -// MullvadVPN -// -// Created by pronebird on 21/05/2019. -// Copyright © 2019 Mullvad VPN AB. All rights reserved. -// - -import Foundation - -enum AuthenticationMethod { - case existingAccount, newAccount -} - -enum LoginState { - case `default` - case authenticating(AuthenticationMethod) - case failure(AccountError) - case success(AuthenticationMethod) -} diff --git a/ios/MullvadVPN/LoginViewController.swift b/ios/MullvadVPN/LoginViewController.swift index 99918b1646..bc21068988 100644 --- a/ios/MullvadVPN/LoginViewController.swift +++ b/ios/MullvadVPN/LoginViewController.swift @@ -6,12 +6,22 @@ // Copyright © 2019 Mullvad VPN AB. All rights reserved. // -import Combine import UIKit import os private let kMinimumAccountTokenLength = 10 +enum AuthenticationMethod { + case existingAccount, newAccount +} + +enum LoginState { + case `default` + case authenticating(AuthenticationMethod) + case failure(Account.Error) + case success(AuthenticationMethod) +} + class LoginViewController: UIViewController, RootContainment { @IBOutlet var keyboardToolbar: UIToolbar! @@ -26,8 +36,6 @@ class LoginViewController: UIViewController, RootContainment { @IBOutlet var statusImageView: UIImageView! @IBOutlet var createAccountButton: AppButton! - private var loginSubscriber: AnyCancellable? - private var loginState = LoginState.default { didSet { loginStateDidChange() @@ -145,16 +153,17 @@ class LoginViewController: UIViewController, RootContainment { beginLogin(method: .existingAccount) - loginSubscriber = Account.shared.login(with: accountToken) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { (completionResult) in - switch completionResult { - case .finished: - self.endLogin(.success(.existingAccount)) - case .failure(let error): - self.endLogin(.failure(error)) - } - }, receiveValue: { _ in }) + Account.shared.login(with: accountToken) { (result) in + switch result { + case .success: + self.endLogin(.success(.existingAccount)) + + case .failure(let error): + error.logChain(message: "Failed to log in with existing account") + + self.endLogin(.failure(error)) + } + } } @IBAction func createNewAccount() { @@ -163,18 +172,18 @@ class LoginViewController: UIViewController, RootContainment { accountTextField.autoformattingText = "" updateKeyboardToolbar() - loginSubscriber = Account.shared.loginWithNewAccount() - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { (completionResult) in - switch completionResult { - case .finished: - self.endLogin(.success(.newAccount)) - case .failure(let error): - self.endLogin(.failure(error)) - } - }, receiveValue: { (newAccountToken) in + Account.shared.loginWithNewAccount { (result) in + switch result { + case .success(let (newAccountToken, _)): self.accountTextField.autoformattingText = newAccountToken - }) + + self.endLogin(.success(.newAccount)) + case .failure(let error): + error.logChain(message: "Failed to log in with new account") + + self.endLogin(.failure(error)) + } + } } // MARK: - Private @@ -195,10 +204,10 @@ class LoginViewController: UIViewController, RootContainment { fallthrough case .success: - rootContainerController?.headerBarSettingsButton.isEnabled = false + rootContainerController?.setEnableSettingsButton(false) case .default, .failure: - rootContainerController?.headerBarSettingsButton.isEnabled = true + rootContainerController?.setEnableSettingsButton(true) createAccountButton.isEnabled = true activityIndicator.stopAnimating() } @@ -246,7 +255,7 @@ class LoginViewController: UIViewController, RootContainment { } else if case .success = loginState { // Navigate to the main view after 1s delay DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { - self.rootContainerController?.headerBarSettingsButton.isEnabled = true + self.rootContainerController?.setEnableSettingsButton(true) self.performSegue(withIdentifier: SegueIdentifier.Login.showConnect.rawValue, sender: self) @@ -307,7 +316,12 @@ private extension LoginState { } case .failure(let error): - return error.failureReason ?? "" + switch error { + case .createAccount(let rpcError), .verifyAccount(let rpcError): + return rpcError.errorChainDescription ?? "" + case .tunnelConfiguration: + return NSLocalizedString("Internal error", comment: "") + } case .success(let method): switch method { diff --git a/ios/MullvadVPN/MullvadVPN-Bridging-Header.h b/ios/MullvadVPN/MullvadVPN-Bridging-Header.h index fcd44b3f57..cd9ddd8174 100644 --- a/ios/MullvadVPN/MullvadVPN-Bridging-Header.h +++ b/ios/MullvadVPN/MullvadVPN-Bridging-Header.h @@ -2,4 +2,5 @@ // Use this file to import your target's public headers that you would like to expose to Swift. // +#include "x25519.h" #include "wireguard-go-version.h" diff --git a/ios/MullvadVPN/PacketTunnelIpc.swift b/ios/MullvadVPN/PacketTunnelIpc.swift index 3e2d6b9af9..c4b4c6623e 100644 --- a/ios/MullvadVPN/PacketTunnelIpc.swift +++ b/ios/MullvadVPN/PacketTunnelIpc.swift @@ -6,43 +6,38 @@ // Copyright © 2019 Mullvad VPN AB. All rights reserved. // -import Combine import Foundation import NetworkExtension /// A enum describing the kinds of requests that `PacketTunnelProvider` handles -enum PacketTunnelRequest: Int, Codable { - /// Request the tunnel to reload the configuration - case reloadConfiguration +enum PacketTunnelRequest: Int, Codable, RawRepresentable { + /// Request the tunnel to reload settings + case reloadTunnelSettings /// Request the tunnel to return the connection information case tunnelInformation -} - -enum PacketTunnelIpcError: Error { - /// A failure to encode the request - case encoding(Error) - /// A failure to decode the response - case decoding(Error) + private enum CodingKeys: String, CodingKey { + case type + } - /// A failure to send the IPC request - case send(Error) + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(rawValue, forKey: CodingKeys.type) + } - /// A failure that's raised when the IPC response does not contain any data however the decoder - /// expected to receive data for decoding - case nilResponse + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let rawValue = try container.decode(RawValue.self, forKey: CodingKeys.type) - var localizedDescription: String { - switch self { - case .encoding(let error): - return "Encoding failure: \(error.localizedDescription)" - case .decoding(let error): - return "Decoding failure: \(error.localizedDescription)" - case .send(let error): - return "Submission failure: \(error.localizedDescription)" - case .nilResponse: - return "Unexpected nil response from the tunnel" + if let decoded = PacketTunnelRequest(rawValue: rawValue) { + self = decoded + } else { + throw DecodingError.dataCorruptedError( + forKey: CodingKeys.type, + in: container, + debugDescription: "Unrecognized raw value." + ) } } } @@ -64,86 +59,164 @@ extension TunnelConnectionInfo: CustomDebugStringConvertible { } } -enum PacketTunnelIpcHandlerError: Error { - /// A failure to encode the request - case encoding(Error) +enum PacketTunnelIpcHandler {} + +extension PacketTunnelIpcHandler { - /// A failure to decode the response - case decoding(Error) + enum Error: ChainedError { + /// A failure to encode the request + case encoding(Swift.Error) - /// A failure to process the request - case processing(Error) -} + /// A failure to decode the response + case decoding(Swift.Error) -enum PacketTunnelIpcHandler {} + /// A failure to process the request + case processing(Swift.Error) -extension PacketTunnelIpcHandler { + var errorDescription: String? { + switch self { + case .encoding: + return "Encoding failure" + case .decoding: + return "Decoding failure" + case .processing: + return "Request handling failure" + } + } + } - static func decodeRequest(messageData: Data) -> AnyPublisher<PacketTunnelRequest, PacketTunnelIpcHandlerError> { - return Just(messageData) - .setFailureType(to: PacketTunnelIpcHandlerError.self) - .decode(type: PacketTunnelRequest.self, decoder: JSONDecoder()) - .mapError { PacketTunnelIpcHandlerError.decoding($0) } - .eraseToAnyPublisher() + static func decodeRequest(messageData: Data) -> Result<PacketTunnelRequest, Error> { + do { + let decoder = JSONDecoder() + let value = try decoder.decode(PacketTunnelRequest.self, from: messageData) + + return .success(value) + } catch { + return .failure(.decoding(error)) + } } - static func encodeResponse<T>(response: T) -> AnyPublisher<Data, PacketTunnelIpcHandlerError> where T: Encodable { - return Just(response) - .setFailureType(to: PacketTunnelIpcHandlerError.self) - .encode(encoder: JSONEncoder()) - .mapError { PacketTunnelIpcHandlerError.encoding($0) } - .eraseToAnyPublisher() + static func encodeResponse<T>(response: T) -> Result<Data, Error> where T: Encodable { + do { + let encoder = JSONEncoder() + let value = try encoder.encode(response) + + return .success(value) + } catch { + return .failure(.encoding(error)) + } } } class PacketTunnelIpc { + enum Error: ChainedError { + /// A failure to encode the request + case encoding(Swift.Error) + + /// A failure to decode the response + case decoding(Swift.Error) + + /// A failure to send the IPC request + case send(Swift.Error) + + /// A failure that's raised when the IPC response does not contain any data however the decoder + /// expected to receive data for decoding + case nilResponse + + var errorDescription: String? { + switch self { + case .encoding: + return "Encoding failure" + case .decoding: + return "Decoding failure" + case .send: + return "Submission failure" + case .nilResponse: + return "Unexpected nil response from the tunnel" + } + } + } + let session: VPNTunnelProviderSessionProtocol init(session: VPNTunnelProviderSessionProtocol) { self.session = session } - func reloadConfiguration() -> AnyPublisher<(), PacketTunnelIpcError> { - return send(message: .reloadConfiguration) + func reloadTunnelSettings(completionHandler: @escaping (Result<(), Error>) -> Void) { + send(message: .reloadTunnelSettings, completionHandler: completionHandler) } - func getTunnelInformation() -> AnyPublisher<TunnelConnectionInfo, PacketTunnelIpcError> { - return send(message: .tunnelInformation) + func getTunnelInformation(completionHandler: @escaping (Result<TunnelConnectionInfo, Error>) -> Void) { + send(message: .tunnelInformation, completionHandler: completionHandler) } - private func send(message: PacketTunnelRequest) -> AnyPublisher<(), PacketTunnelIpcError> { - return sendWithoutDecoding(message: message) - .map { _ in () }.eraseToAnyPublisher() + private class func encodeRequest(message: PacketTunnelRequest) -> Result<Data, Error> { + do { + let encoder = JSONEncoder() + let data = try encoder.encode(message) + + return .success(data) + } catch { + return .failure(.encoding(error)) + } + } + + private class func decodeResponse<T>(data: Data) -> Result<T, Error> where T: Decodable { + do { + let decoder = JSONDecoder() + let value = try decoder.decode(T.self, from: data) + + return .success(value) + } catch { + return .failure(.decoding(error)) + } } - private func send<T>(message: PacketTunnelRequest) -> AnyPublisher<T, PacketTunnelIpcError> where T: Decodable { - return sendWithoutDecoding(message: message) - .replaceNil(with: .nilResponse) - .decode(type: T.self, decoder: JSONDecoder()) - .mapError { PacketTunnelIpcError.decoding($0) } - .eraseToAnyPublisher() + private func send(message: PacketTunnelRequest, completionHandler: @escaping (Result<(), Error>) -> Void) { + sendWithoutDecoding(message: message) { (result) in + let result = result.map { _ in () } + + completionHandler(result) + } } - private func sendWithoutDecoding(message: PacketTunnelRequest) -> AnyPublisher<Data?, PacketTunnelIpcError> { - return Just(message) - .setFailureType(to: PacketTunnelIpcError.self) - .encode(encoder: JSONEncoder()) - .mapError { PacketTunnelIpcError.encoding($0) } - .flatMap(self.sendProviderMessage) - .mapError { PacketTunnelIpcError.send($0) } - .eraseToAnyPublisher() + private func send<T>(message: PacketTunnelRequest, completionHandler: @escaping (Result<T, Error>) -> Void) + where T: Decodable + { + sendWithoutDecoding(message: message) { (result) in + let result = result.flatMap { (data) -> Result<T, Error> in + if let data = data { + return Self.decodeResponse(data: data) + } else { + return .failure(.nilResponse) + } + } + + completionHandler(result) + } } - private func sendProviderMessage(_ messageData: Data) -> Future<Data?, Error> { - return Future { (fulfill) in - do { - try self.session.sendProviderMessage(messageData, responseHandler: { (response) in - fulfill(.success(response)) - }) - } catch { - fulfill(.failure(error)) + private func sendWithoutDecoding(message: PacketTunnelRequest, completionHandler: @escaping (Result<Data?, Error>) -> Void) { + switch Self.encodeRequest(message: message) { + case .success(let data): + self.sendProviderMessage(data) { (result) in + completionHandler(result) } + + case .failure(let error): + completionHandler(.failure(error)) + } + } + + private func sendProviderMessage(_ messageData: Data, completionHandler: @escaping (Result<Data?, Error>) -> Void) { + do { + try self.session.sendProviderMessage(messageData, responseHandler: { (response) in + completionHandler(.success(response)) + }) + } catch { + completionHandler(.failure(.send(error))) } } diff --git a/ios/MullvadVPN/RelaySelector+RelayCache.swift b/ios/MullvadVPN/RelaySelector+RelayCache.swift deleted file mode 100644 index 3fd30180a5..0000000000 --- a/ios/MullvadVPN/RelaySelector+RelayCache.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// RelaySelector+RelayCache.swift -// MullvadVPN -// -// Created by pronebird on 07/11/2019. -// Copyright © 2019 Mullvad VPN AB. All rights reserved. -// - -import Combine -import Foundation - -extension RelaySelector { - - static func loadedFromRelayCache() -> AnyPublisher<RelaySelector, RelayCacheError> { - return RelayCache.withDefaultLocationAndEphemeralSession().publisher - .flatMap { $0.read() } - .map { RelaySelector(relayList: $0.relayList) } - .eraseToAnyPublisher() - } - -} diff --git a/ios/MullvadVPN/SettingsNavigationController.swift b/ios/MullvadVPN/SettingsNavigationController.swift new file mode 100644 index 0000000000..326c2047bd --- /dev/null +++ b/ios/MullvadVPN/SettingsNavigationController.swift @@ -0,0 +1,19 @@ +// +// SettingsNavigationController.swift +// MullvadVPN +// +// Created by pronebird on 02/07/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import UIKit + +class SettingsNavigationController: UINavigationController { + override func viewDidLoad() { + super.viewDidLoad() + + // Update account expiry + Account.shared.updateAccountExpiry() + } +} diff --git a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift index 92d31f1375..3481117f20 100644 --- a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift @@ -8,14 +8,12 @@ #if targetEnvironment(simulator) -import Combine import Foundation import Network import NetworkExtension class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { - private let cancellableSet = CancellableSet() private var connectionInfo: TunnelConnectionInfo? func startTunnel(options: [String: Any]?, completionHandler: @escaping (Error?) -> Void) { @@ -47,27 +45,33 @@ class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { } func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { - PacketTunnelIpcHandler.decodeRequest(messageData: messageData) - .receive(on: DispatchQueue.main) - .flatMap { (request) -> AnyPublisher<AnyEncodable, PacketTunnelIpcHandlerError> in + DispatchQueue.main.async { + let completeRequest = { (response: AnyEncodable) in + switch PacketTunnelIpcHandler.encodeResponse(response: response) { + case .success(let data): + completionHandler?(data) + + case .failure: + completionHandler?(nil) + } + + } + + let result = PacketTunnelIpcHandler.decodeRequest(messageData: messageData) + switch result { + case .success(let request): switch request { - case .reloadConfiguration: - return Result.Publisher(AnyEncodable(true)) - .eraseToAnyPublisher() + case .reloadTunnelSettings: + return completeRequest(AnyEncodable(true)) case .tunnelInformation: - return Result.Publisher(AnyEncodable(self.connectionInfo)) - .eraseToAnyPublisher() + return completeRequest(AnyEncodable(self.connectionInfo)) } - }.flatMap({ (response) in - return PacketTunnelIpcHandler.encodeResponse(response: response) - }).autoDisposableSink(cancellableSet: cancellableSet, receiveCompletion: { (completion) in - if case .failure = completion { + + case .failure: completionHandler?(nil) } - }, receiveValue: { (responseData) in - completionHandler?(responseData) - }) + } } } diff --git a/ios/MullvadVPN/TunnelControlViewController.swift b/ios/MullvadVPN/TunnelControlViewController.swift index 8ec87eb8d0..c380bafdd6 100644 --- a/ios/MullvadVPN/TunnelControlViewController.swift +++ b/ios/MullvadVPN/TunnelControlViewController.swift @@ -6,7 +6,6 @@ // Copyright © 2019 Mullvad VPN AB. All rights reserved. // -import Combine import UIKit enum TunnelControlAction { @@ -26,7 +25,7 @@ protocol TunnelControlViewControllerDelegate: class { func tunnelControlViewController(_ controller: TunnelControlViewController, handleAction action: TunnelControlAction) -> Void } -class TunnelControlViewController: UIViewController { +class TunnelControlViewController: UIViewController, TunnelObserver { @IBOutlet var disconnectedView: UIView! @IBOutlet var connectingView: UIView! @@ -34,19 +33,29 @@ class TunnelControlViewController: UIViewController { weak var delegate: TunnelControlViewControllerDelegate? - private var tunnelStateSubscriber: AnyCancellable? private var controlsView: UIView? override func viewDidLoad() { super.viewDidLoad() - tunnelStateSubscriber = TunnelManager.shared.$tunnelState - .receive(on: DispatchQueue.main) - .sink { [weak self] (tunnelState) in - self?.didReceiveTunnelState(tunnelState) + TunnelManager.shared.addObserver(self) + self.didReceiveTunnelState(TunnelManager.shared.tunnelState) + } + + // MARK: - TunnelObserver + + func tunnelStateDidChange(tunnelState: TunnelState) { + DispatchQueue.main.async { + self.didReceiveTunnelState(tunnelState) } } + func tunnelPublicKeyDidChange(publicKey: WireguardPublicKey?) { + // no-op + } + + /// MARK: - Private + private func didReceiveTunnelState(_ tunnelState: TunnelState) { switch tunnelState { case .disconnected: @@ -60,7 +69,6 @@ class TunnelControlViewController: UIViewController { } } - private func addControlsView(_ nextControlsView: UIView) { guard controlsView != nextControlsView else { return } diff --git a/ios/MullvadVPN/TunnelManager.swift b/ios/MullvadVPN/TunnelManager.swift index 4c116d8df1..65c86aee45 100644 --- a/ios/MullvadVPN/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager.swift @@ -6,194 +6,17 @@ // Copyright © 2019 Mullvad VPN AB. All rights reserved. // -import Combine import Foundation import NetworkExtension import os -/// An error emitted by all public methods of TunnelManager -enum TunnelManagerError: Error { - /// Account token is not set - case missingAccount - - /// A failure to start the tunnel - case startTunnel(StartTunnelError) - - /// A failure to stop the tunnel - case stopTunnel(Error) - - /// A failure to load the account with the assumption that it was already set up earlier - case loadTunnel(LoadTunnelError) - - /// A failure to set the account - case setAccount(SetAccountError) - - /// A failure to unset the account - case unsetAccount(UnsetAccountError) - - /// A failure to set the relay constraints - case setRelayConstraints(TunnelConfigurationManager.Error) - - /// A failure to get the relay constraints - case getRelayConstraints(TunnelConfigurationManager.Error) - - /// A failure to get a public key used for Wireguard - case getWireguardPublicKey(TunnelConfigurationManager.Error) - - /// A failure to re-generate a private key used for Wireguard - case regenerateWireguardPrivateKey(RegenerateWireguardPrivateKeyError) -} - -extension TunnelManagerError: LocalizedError { - - var errorDescription: String? { - switch self { - case .regenerateWireguardPrivateKey: - return NSLocalizedString("Cannot regenerate the private key", comment: "") - - case .setAccount: - return NSLocalizedString("Cannot set up the tunnel", comment: "") - - case .getWireguardPublicKey: - return NSLocalizedString("Cannot retrieve the public key", comment: "") - - case .startTunnel: - return NSLocalizedString("Cannot start the tunnel", comment: "") - - case .stopTunnel: - return NSLocalizedString("Cannot stop the tunnel", comment: "") - - default: - return nil - } - } - - var failureReason: String? { - switch self { - - case .setAccount(.pushWireguardKey(let pushError)), - .regenerateWireguardPrivateKey(.replaceWireguardKey(let pushError)): - switch pushError { - case .network(let urlError): - return urlError.localizedDescription - - case .server(let serverError): - return serverError.errorDescription - - default: - return NSLocalizedString("Internal error", comment: "") - } - - default: - return nil - } - } - - var recoverySuggestion: String? { - switch self { - case .regenerateWireguardPrivateKey(.replaceWireguardKey(let pushError)): - switch pushError { - case .server(let serverError) where serverError.code == .tooManyWireguardKeys: - return NSLocalizedString("Remove unused WireGuard keys and try again.", comment: "") - - default: - return nil - } - - default: - return nil - } - } - -} - -enum TunnelIpcRequestError: Error { - /// IPC is not set yet +enum MapConnectionStatusError: ChainedError { + /// A failure to perform the IPC request because the tunnel IPC is already deallocated case missingIpc - /// A failure to submit or handle the IPC request - case send(PacketTunnelIpcError) - - var localizedDescription: String { - switch self { - case .missingIpc: - return "IPC is not initialized yet" - - case .send(let ipcError): - return "Failure to send an IPC request: \(ipcError.localizedDescription)" - } - } -} - -enum SetAccountError: Error { - /// A failure to make the tunnel configuration - case makeTunnelConfiguration(TunnelConfigurationManager.Error) - - /// A failure to update the tunnel configuration - case updateTunnelConfiguration(TunnelConfigurationManager.Error) - - /// A failure to push the wireguard key - case pushWireguardKey(MullvadRpc.Error) - - /// A failure to set up a tunnel - case setup(SetupTunnelError) -} - -enum UnsetAccountError: Error { - /// A failure to remove the system tunnel - case removeTunnel(Error) - - /// A failure to remove a tunnel configuration from Keychain - case removeTunnelConfiguration(TunnelConfigurationManager.Error) -} - -enum RegenerateWireguardPrivateKeyError: Error { - /// A failure to read the public Wireguard key from Keychain - case readPublicWireguardKey(TunnelConfigurationManager.Error) - - /// A failure to replace the public Wireguard key - case replaceWireguardKey(MullvadRpc.Error) - - /// A failure to update tunnel configuration - case updateTunnelConfiguration(TunnelConfigurationManager.Error) -} - -enum StartTunnelError: Error { - /// An error that happened during the tunnel setup stage - case setup(SetupTunnelError) - - /// System call error - case system(Error) -} - -enum SetupTunnelError: Error { - /// A failure to load a list of tunnels associated with the app - case loadTunnels(Error) - - /// A failure to save tunnel preferences - case saveTunnel(Error) - - /// A failure to reload the tunnel preferences - case reloadTunnel(Error) - - /// Unable to obtain the keychain reference for the configuration - case obtainKeychainRef(TunnelConfigurationManager.Error) -} - -enum LoadTunnelError: Error { - /// A failure to load a list of tunnels associated with the app - case loadTunnels(Error) - - /// A failure to perform a recovery (by removing the tunnel) when the inconsistency between - /// the given account token and the username saved in the tunnel provide configuration is - /// detected. - case removeInconsistentTunnel(Error) -} - -enum MapConnectionStatusError: Error { /// A failure to send a subsequent IPC request to collect more information, such as tunnel /// connection info. - case ipcRequest(TunnelIpcRequestError) + case ipcRequest(PacketTunnelIpc.Error) /// A failure to map the status because the unknown variant of `NEVPNStatus` was given. case unknownStatus(NEVPNStatus) @@ -201,7 +24,23 @@ enum MapConnectionStatusError: Error { /// A failure to map the status because the `NEVPNStatus.invalid` variant was given /// This happens when attempting to start a tunnel with configuration that does not exist /// anymore in system preferences. - case invalidConfiguration(NEVPNStatus) + case invalidConfiguration + + var errorDescription: String? { + switch self { + case .missingIpc: + return "Missing IPC" + + case .ipcRequest: + return "IPC request error" + + case .unknownStatus(let status): + return "Unknown NEVPNStatus: \(status)" + + case .invalidConfiguration: + return "Invalid VPN configuration" + } + } } /// A enum that describes the tunnel state @@ -223,7 +62,7 @@ enum TunnelState: Equatable { case reconnecting(TunnelConnectionInfo) } -extension TunnelState: CustomStringConvertible { +extension TunnelState: CustomStringConvertible, CustomDebugStringConvertible { var description: String { switch self { case .connecting: @@ -238,9 +77,7 @@ extension TunnelState: CustomStringConvertible { return "reconnecting" } } -} -extension TunnelState: CustomDebugStringConvertible { var debugDescription: String { var output = "TunnelState." @@ -269,10 +106,128 @@ extension TunnelState: CustomDebugStringConvertible { } } +protocol TunnelObserver: class { + func tunnelStateDidChange(tunnelState: TunnelState) + func tunnelPublicKeyDidChange(publicKey: WireguardPublicKey?) +} + +private class AnyTunnelObserver: WeakObserverBox, TunnelObserver { + + typealias Wrapped = TunnelObserver + + private(set) weak var inner: TunnelObserver? + + init<T: TunnelObserver>(_ inner: T) { + self.inner = inner + } + + func tunnelStateDidChange(tunnelState: TunnelState) { + self.inner?.tunnelStateDidChange(tunnelState: tunnelState) + } + + func tunnelPublicKeyDidChange(publicKey: WireguardPublicKey?) { + self.inner?.tunnelPublicKeyDidChange(publicKey: publicKey) + } + + static func == (lhs: AnyTunnelObserver, rhs: AnyTunnelObserver) -> Bool { + return lhs.inner === rhs.inner + } +} + /// A class that provides a convenient interface for VPN tunnels configuration, manipulation and /// monitoring. class TunnelManager { + /// An error emitted by all public methods of TunnelManager + enum Error: ChainedError { + /// Account token is not set + case missingAccount + + /// A failure to start the VPN tunnel via system call + case startVPNTunnel(Swift.Error) + + /// A failure to load the system VPN configurations created by the app + case loadAllVPNConfigurations(Swift.Error) + + /// A failure to save the system VPN configuration + case saveVPNConfiguration(Swift.Error) + + /// A failure to reload the system VPN configuration + case reloadVPNConfiguration(Swift.Error) + + /// A failure to remove the system VPN configuration + case removeVPNConfiguration(Swift.Error) + + /// A failure to perform a recovery (by removing the VPN configuration) when the + /// inconsistency between the given account token and the username saved in the tunnel + /// provider configuration is detected. + case removeInconsistentVPNConfiguration(Swift.Error) + + /// A failure to read tunnel settings + case readTunnelSettings(TunnelSettingsManager.Error) + + /// A failure to add the tunnel settings + case addTunnelSettings(TunnelSettingsManager.Error) + + /// A failure to update the tunnel settings + case updateTunnelSettings(TunnelSettingsManager.Error) + + /// A failure to remove the tunnel settings from Keychain + case removeTunnelSettings(TunnelSettingsManager.Error) + + /// Unable to obtain the persistent keychain reference for the tunnel settings + case obtainPersistentKeychainReference(TunnelSettingsManager.Error) + + /// A failure to push the public WireGuard key + case pushWireguardKey(MullvadRpc.Error) + + /// A failure to replace the public WireGuard key + case replaceWireguardKey(MullvadRpc.Error) + + /// A failure to remove the public WireGuard key + case removeWireguardKey(MullvadRpc.Error) + + /// A failure to verify the public WireGuard key + case verifyWireguardKey(MullvadRpc.Error) + + var errorDescription: String? { + switch self { + case .missingAccount: + return "Missing account token" + case .startVPNTunnel: + return "Failed to start the VPN tunnel" + case .loadAllVPNConfigurations: + return "Failed to load the system VPN configurations" + case .saveVPNConfiguration: + return "Failed to save the system VPN configuration" + case .reloadVPNConfiguration: + return "Failed to reload the system VPN configuration" + case .removeVPNConfiguration: + return "Failed to remove the system VPN configuration" + case .removeInconsistentVPNConfiguration: + return "Failed to remove the inconsistent VPN tunnel" + case .readTunnelSettings: + return "Failed to read the tunnel settings" + case .addTunnelSettings: + return "Failed to add the tunnel settings" + case .updateTunnelSettings: + return "Failed to update the tunnel settings" + case .removeTunnelSettings: + return "Failed to remove the tunnel settings" + case .obtainPersistentKeychainReference: + return "Failed to obtain the persistent keychain refrence" + case .pushWireguardKey: + return "Failed to push the WireGuard key to server" + case .replaceWireguardKey: + return "Failed to replace the WireGuard key on server" + case .removeWireguardKey: + return "Failed to remove the WireGuard key from server" + case .verifyWireguardKey: + return "Failed to verify the WireGuard key on server" + } + } + } + // Switch to stabs on simulator #if targetEnvironment(simulator) typealias TunnelProviderManagerType = SimulatorTunnelProviderManager @@ -284,607 +239,810 @@ class TunnelManager { // MARK: - Internal variables - /// A queue used for dispatching tunnel related jobs that require mutual exclusion - private let exclusivityQueue = DispatchQueue(label: "net.mullvad.vpn.tunnel-manager.exclusivity-queue") - - /// A queue used for access synchronization to the TunnelManager members - private let executionQueue = DispatchQueue(label: "net.mullvad.vpn.tunnel-manager.execution-queue") + private let dispatchQueue = DispatchQueue(label: "net.mullvad.MullvadVPN.TunnelManager") private let rpc = MullvadRpc.withEphemeralURLSession() private var tunnelProvider: TunnelProviderManagerType? private var tunnelIpc: PacketTunnelIpc? - /// A subscriber used for tunnel connection status changes - private var tunnelStatusSubscriber: AnyCancellable? + private let stateLock = NSLock() + private let observerList = ObserverList<AnyTunnelObserver>() - /// A subscriber used for mapping a connection status (`NEVPNStatus`) to `TunnelState` - private var mapTunnelStateSubscriber: AnyCancellable? + /// A VPN connection status observer + private var connectionStatusObserver: NSObjectProtocol? /// An account token associated with the active tunnel private var accountToken: String? + private var _tunnelState = TunnelState.disconnected + private var _publicKey: WireguardPublicKey? + private init() {} // MARK: - Public - @Published private(set) var tunnelState = TunnelState.disconnected + private(set) var tunnelState: TunnelState { + set { + stateLock.withCriticalBlock { + guard _tunnelState != newValue else { return } + + os_log(.default, "Set tunnel state: %{public}s", String(reflecting: newValue)) - /// A last known public key - @Published private(set) var publicKey: WireguardPublicKey? + _tunnelState = newValue + + observerList.forEach { (observer) in + observer.tunnelStateDidChange(tunnelState: newValue) + } + } + } + get { + stateLock.withCriticalBlock { + return _tunnelState + } + } + } + + /// The last known public key + private(set) var publicKey: WireguardPublicKey? { + set { + stateLock.withCriticalBlock { + guard _publicKey != newValue else { return } + + _publicKey = newValue + + observerList.forEach { (observer) in + observer.tunnelPublicKeyDidChange(publicKey: newValue) + } + } + } + get { + stateLock.withCriticalBlock { + return _publicKey + } + } + } /// Initialize the TunnelManager with the tunnel from the system /// /// The given account token is used to ensure that the system tunnel was configured for the same /// account. The system tunnel is removed in case of inconsistency. - func loadTunnel(accountToken: String?) -> AnyPublisher<(), TunnelManagerError> { - MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { - TunnelProviderManagerType.loadAllFromPreferences() - .mapError { LoadTunnelError.loadTunnels($0) } - .receive(on: self.executionQueue) - .flatMap { (tunnels) -> AnyPublisher<(), LoadTunnelError> in - - if let accountToken = accountToken { - // Migrate tunnel configuration if needed - self.migrateTunnelConfiguration(accountToken: accountToken) + func loadTunnel(accountToken: String?, completionHandler: @escaping (Result<(), TunnelManager.Error>) -> Void) { + let operation = ResultOperation<(), TunnelManager.Error> { (finish) in + TunnelProviderManagerType.loadAllFromPreferences { (tunnels, error) in + self.dispatchQueue.async { + if let error = error { + finish(.failure(.loadAllVPNConfigurations(error))) + } else { + if let accountToken = self.accountToken { + // Migrate the tunnel settings if needed + Self.migrateTunnelSettings(accountToken: accountToken) - // Load last known public key - self.loadPublicKey(accountToken: accountToken) - } + // Load last known public key + self.loadPublicKey(accountToken: accountToken) + } - // No tunnels found. Save the account token. - guard let tunnelProvider = tunnels?.first else { - self.accountToken = accountToken + if let tunnelProvider = tunnels?.first { + // Ensure the consistency between the given account token and the one + // saved in the system tunnel configuration. + if let username = tunnelProvider.protocolConfiguration?.username, + let accountToken = accountToken, accountToken == username { + self.accountToken = accountToken - return Result.Publisher(()).eraseToAnyPublisher() - } + self.setTunnelProvider(tunnelProvider: tunnelProvider) - // Ensure the consistency between the given account token and the one - // saved in the system tunnel configuration. - if let username = tunnelProvider.protocolConfiguration?.username, - let accountToken = accountToken, accountToken == username { - self.accountToken = accountToken - self.setTunnelProvider(tunnelProvider: tunnelProvider) + finish(.success(())) + } else { + // In case of inconsistency, remove the tunnel + tunnelProvider.removeFromPreferences { (error) in + self.dispatchQueue.async { + if let error = error { + finish(.failure(.removeInconsistentVPNConfiguration(error))) + } else { + self.accountToken = accountToken - return Result.Publisher(()).eraseToAnyPublisher() - } else { - // In case of inconsistency, remove the tunnel - return tunnelProvider.removeFromPreferences() - .receive(on: self.executionQueue) - .mapError { LoadTunnelError.removeInconsistentTunnel($0) } - .handleEvents(receiveCompletion: { completion in - if case .finished = completion { - self.accountToken = accountToken + finish(.success(())) + } + } } - }).eraseToAnyPublisher() + } + } else { + // No tunnels found. Save the account token. + self.accountToken = accountToken + + finish(.success(())) + } } - }.mapError { TunnelManagerError.loadTunnel($0) } - }.eraseToAnyPublisher() + } + } + } + + operation.addDidFinishBlockObserver { (operation, result) in + completionHandler(result) + } + + exclusityController.addOperation(operation, categories: [.tunnelControl]) } /// Refresh tunnel state. /// Use this method to update the tunnel state when app transitions from suspended to active /// state. - func refreshTunnelState() -> AnyPublisher<(), TunnelManagerError> { - MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { - () -> AnyPublisher<(), TunnelManagerError> in - if let status = self.tunnelProvider?.connection.status { - self.updateTunnelState(connectionStatus: status) - } - + func refreshTunnelState(completionHandler: (() -> Void)?) { + let operation = BlockOperation { // Reload the last known public key if let accountToken = self.accountToken { self.loadPublicKey(accountToken: accountToken) } - return Result.Publisher(()).eraseToAnyPublisher() - }.eraseToAnyPublisher() + + if let status = self.tunnelProvider?.connection.status { + self.updateTunnelState(connectionStatus: status) + } + + completionHandler?() + } + + exclusityController.addOperation(operation, categories: [.tunnelControl]) } - func startTunnel() -> AnyPublisher<(), TunnelManagerError> { - MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { - Just(self.accountToken) - .setFailureType(to: TunnelManagerError.self) - .replaceNil(with: .missingAccount) - .flatMap { (accountToken) in - self.setupTunnel(accountToken: accountToken) - .mapError { StartTunnelError.setup($0) } - .flatMap({ (tunnelProvider) -> Result<(), StartTunnelError>.Publisher in - Just(tunnelProvider) - .tryMap { try $0.connection.startVPNTunnel() } - .mapError { StartTunnelError.system($0) } - }).mapError { TunnelManagerError.startTunnel($0) } + func startTunnel(completionHandler: @escaping (Result<(), Error>) -> Void) { + let operation = ResultOperation<(), Error> { (finish) in + guard let accountToken = self.accountToken else { + finish(.failure(.missingAccount)) + return + } + + self.makeTunnelProvider(accountToken: accountToken) { (result) in + let result = result.flatMap { (tunnelProvider) -> Result<(), Error> in + self.setTunnelProvider(tunnelProvider: tunnelProvider) + + return Result { try tunnelProvider.connection.startVPNTunnel() } + .mapError { Error.startVPNTunnel($0) } + } + finish(result) } - }.eraseToAnyPublisher() + } + + operation.addDidFinishBlockObserver { (operation, result) in + completionHandler(result) + } + + exclusityController.addOperation(operation, categories: [.tunnelControl]) } - func stopTunnel() -> AnyPublisher<(), TunnelManagerError> { - MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { () -> AnyPublisher<(), TunnelManagerError> in - if let tunnelProvider = self.tunnelProvider { - // Disable on-demand when turning off the tunnel to prevent the tunnel from coming - // back up - tunnelProvider.isOnDemandEnabled = false + func stopTunnel(completionHandler: @escaping (Result<(), Error>) -> Void) { + let operation = ResultOperation<(), Error> { (finish) in + guard let tunnelProvider = self.tunnelProvider else { + finish(.success(())) + return + } + + // Disable on-demand when stopping the tunnel to prevent it from coming back up + tunnelProvider.isOnDemandEnabled = false - return tunnelProvider.saveToPreferences() - .mapError { TunnelManagerError.stopTunnel($0) } - .map { _ -> () in - tunnelProvider.connection.stopVPNTunnel() - return () - }.eraseToAnyPublisher() - } else { - return Result.Publisher(()).eraseToAnyPublisher() + tunnelProvider.saveToPreferences { (error) in + if let error = error { + completionHandler(.failure(.saveVPNConfiguration(error))) + } else { + tunnelProvider.connection.stopVPNTunnel() + finish(.success(())) + } } - }.eraseToAnyPublisher() + } + + operation.addDidFinishBlockObserver { (operation, result) in + completionHandler(result) + } + + exclusityController.addOperation(operation, categories: [.tunnelControl]) } - func setAccount(accountToken: String) -> AnyPublisher<(), TunnelManagerError> { - MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { - self.makeTunnelConfiguration(accountToken: accountToken).publisher - .mapError { .makeTunnelConfiguration($0) } - .flatMap { (tunnelConfig: TunnelConfiguration) -> AnyPublisher<(), SetAccountError> in + func setAccount(accountToken: String, completionHandler: @escaping (Result<(), TunnelManager.Error>) -> Void) { + let operation = ResultOperation<(), TunnelManager.Error> { (finish) in + let result = Self.makeTunnelSettings(accountToken: accountToken) - let setupTunnelPublisher = Deferred { - self.setupTunnel(accountToken: accountToken) - .handleEvents(receiveCompletion: { (completion) in - if case .finished = completion { - self.accountToken = accountToken - } - }) - .map { _ in () } - .mapError { SetAccountError.setup($0) } - } + guard case .success(let tunnelSettings) = result else { + finish(result.map { _ in () }) + return + } - let publicKey = tunnelConfig.interface.privateKey.publicKey + let interfaceSettings = tunnelSettings.interface + let publicKey = interfaceSettings.privateKey.publicKey - // Save the last known public key - self.publicKey = publicKey + let saveAccountData = { + // Save the last known public key + self.publicKey = publicKey + self.accountToken = accountToken + } - // Make sure to avoid pushing the wireguard keys when addresses are assigned - guard tunnelConfig.interface.addresses.isEmpty else { - return setupTunnelPublisher.eraseToAnyPublisher() - } + guard interfaceSettings.addresses.isEmpty else { + saveAccountData() + finish(.success(())) + return + } - // Send wireguard key to the server - return self.rpc.pushWireguardKey( - accountToken: accountToken, - publicKey: publicKey.rawRepresentation - ) - .mapError { SetAccountError.pushWireguardKey($0) } - .flatMap { (addresses) in - self.updateAssociatedAddresses( - accountToken: accountToken, - addresses: addresses - ).mapError { SetAccountError.updateTunnelConfiguration($0) } - .publisher - .flatMap { _ in setupTunnelPublisher } - }.eraseToAnyPublisher() - }.mapError { TunnelManagerError.setAccount($0) } - }.eraseToAnyPublisher() + // Push wireguard key if addresses were not received yet + self.pushWireguardKeyAndUpdateSettings(accountToken: accountToken, publicKey: publicKey) { (result) in + if case .success = result { + saveAccountData() + } + finish(result) + } + } + operation.addDidFinishBlockObserver { (operation, result) in + completionHandler(result) + } + + exclusityController.addOperation(operation, categories: [.tunnelControl]) } /// Remove the account token and remove the active tunnel - func unsetAccount() -> AnyPublisher<(), TunnelManagerError> { - MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { - Just(self.accountToken) - .setFailureType(to: TunnelManagerError.self) - .replaceNil(with: .missingAccount) - .map { ($0, self.tunnelProvider) } - .flatMap { (accountToken, tunnelProvider) -> AnyPublisher<(), TunnelManagerError> in + func unsetAccount(completionHandler: @escaping (Result<(), TunnelManager.Error>) -> Void) { + let operation = ResultOperation<(), TunnelManager.Error> { (finish) in + guard let accountToken = self.accountToken else { + finish(.failure(.missingAccount)) + return + } - let removeKeychainConfigPublisher = Deferred { - () -> AnyPublisher<(), UnsetAccountError> in - // Load existing configuration - switch TunnelConfigurationManager.load(searchTerm: .accountToken(accountToken)) { - case .success(let keychainEntry): - let publicKey = keychainEntry.tunnelConfiguration - .interface - .privateKey - .publicKey - .rawRepresentation + let completeOperation = { + self.accountToken = nil + self.publicKey = nil - // Remove configuration from Keychain - return TunnelConfigurationManager.remove(searchTerm: .accountToken(accountToken)) - .mapError { UnsetAccountError.removeTunnelConfiguration($0) } - .publisher - .flatMap { - // Remove WireGuard key from master - self.rpc.removeWireguardKey( - accountToken: accountToken, - publicKey: publicKey - ) - .retry(1) - .map({ (isRemoved) -> () in - os_log(.debug, "Removed the WireGuard key from server: %{public}s", "\(isRemoved)") - return () - }).catch({ (error) -> Result<(), UnsetAccountError>.Publisher in - os_log(.error, "Failed to remove the Wireguard key from server: %{public}s", error.localizedDescription) + finish(.success(())) + } - // Suppress network errors - return Result.Publisher(()) - }) - }.eraseToAnyPublisher() + let removeTunnel = { + // Unregister from receiving the tunnel state changes + self.unregisterConnectionObserver() + self.tunnelState = .disconnected + self.tunnelIpc = nil - case .failure(let error): - // 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 - os_log(.error, "Failed to read the tunnel configuration from Keychain: %{public}s", error.localizedDescription) + // Remove settings from Keychain + switch TunnelSettingsManager.remove(searchTerm: .accountToken(accountToken)) { + case .success: + break + case .failure(let error): + // 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 + error.logChain(message: "Unset account error") + } - return Just(()) - .setFailureType(to: UnsetAccountError.self) - .eraseToAnyPublisher() - } - } + guard let tunnelProvider = self.tunnelProvider else { + completeOperation() + return + } - let removeTunnelPublisher = Deferred { - () -> AnyPublisher<(), UnsetAccountError> in - if let tunnelProvider = tunnelProvider { - return tunnelProvider.removeFromPreferences() - .catch { (error) -> Result<(), UnsetAccountError>.Publisher in - // Ignore error if the tunnel was already removed by user - if case NEVPNError.configurationInvalid = error { - return .init(()) - } else { - return .init(.failure(.removeTunnel(error))) - } - }.eraseToAnyPublisher() + self.tunnelProvider = nil + + // Remove VPN configuration + tunnelProvider.removeFromPreferences(completionHandler: { (error) in + self.dispatchQueue.async { + if let error = error { + // Ignore error if the tunnel was already removed by user + if let systemError = error as? NEVPNError, systemError.code == .configurationInvalid { + completeOperation() + } else { + finish(.failure(.removeVPNConfiguration(error))) + } } else { - return Result.Publisher(()) - .eraseToAnyPublisher() + completeOperation() } } - - return removeTunnelPublisher - .receive(on: self.executionQueue) - .flatMap { removeKeychainConfigPublisher } - .mapError { TunnelManagerError.unsetAccount($0) } - .eraseToAnyPublisher() + }) } - .receive(on: self.executionQueue) - .handleEvents(receiveCompletion: { (completion) in - if case .finished = completion { - self.accountToken = nil - self.publicKey = nil - self.tunnelProvider = nil - self.tunnelIpc = nil - self.tunnelStatusSubscriber?.cancel() - self.tunnelStatusSubscriber = nil + switch Self.loadTunnelSettings(accountToken: accountToken) { + case .success(let keychainEntry): + let publicKey = keychainEntry.tunnelSettings + .interface + .privateKey + .publicKey + .rawRepresentation + + self.removeWireguardKeyFromServer(accountToken: accountToken, publicKey: publicKey) { (result) in + switch result { + case .success(let isRemoved): + os_log(.debug, "Removed the WireGuard key from server: %{public}s", "\(isRemoved)") - self.mapTunnelStateSubscriber?.cancel() - self.mapTunnelStateSubscriber = nil + case .failure(let error): + error.logChain(message: "Unset account error") + } - self.tunnelState = .disconnected + removeTunnel() } - }) - }.eraseToAnyPublisher() + + case .failure(let error): + // 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 + error.logChain(message: "Unset account error") + + removeTunnel() + } + + } + + operation.addDidFinishBlockObserver { (operation, result) in + completionHandler(result) + } + + exclusityController.addOperation(operation, categories: [.tunnelControl]) } - func regeneratePrivateKey() -> AnyPublisher<(), TunnelManagerError> { - MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { - Just(self.accountToken) - .setFailureType(to: TunnelManagerError.self) - .replaceNil(with: .missingAccount) - .flatMap { (accountToken) -> AnyPublisher<(), TunnelManagerError> in - let newPrivateKey = WireguardPrivateKey() + func verifyPublicKey(completionHandler: @escaping (Result<Bool, Error>) -> Void) { + let makeRequest = ResultOperation<MullvadRpc.Request<Bool>, Error> { + () -> Result<MullvadRpc.Request<Bool>, Error> in + guard let accountToken = self.accountToken else { + return .failure(.missingAccount) + } - return TunnelConfigurationManager.load(searchTerm: .accountToken(accountToken)) - .map { $0.tunnelConfiguration.interface.privateKey.publicKey } - .mapError { RegenerateWireguardPrivateKeyError.readPublicWireguardKey($0) } - .publisher - .flatMap { (oldPublicKey) in - self.rpc.replaceWireguardKey( - accountToken: accountToken, - oldPublicKey: oldPublicKey.rawRepresentation, - newPublicKey: newPrivateKey.publicKey.rawRepresentation) - .mapError { RegenerateWireguardPrivateKeyError.replaceWireguardKey($0) } - .receive(on: self.executionQueue) - .flatMap { (addresses) in - TunnelConfigurationManager.update(searchTerm: .accountToken(accountToken)) { - (tunnelConfiguration) in - tunnelConfiguration.interface.privateKey = newPrivateKey - tunnelConfiguration.interface.addresses = [ - addresses.ipv4Address, - addresses.ipv6Address - ] - } - .mapError { .updateTunnelConfiguration($0) } - .map { _ in () } - .publisher - }.receive(on: self.executionQueue) - .flatMap { _ -> AnyPublisher<(), RegenerateWireguardPrivateKeyError> in - // Save new public key - self.publicKey = newPrivateKey.publicKey + return Self.loadTunnelSettings(accountToken: accountToken) + .map { (keychainEntry) -> MullvadRpc.Request<Bool> in + let publicKey = keychainEntry.tunnelSettings.interface + .privateKey + .publicKey.rawRepresentation - // Ignore Packet Tunnel IPC errors but log them - return self.reloadPacketTunnelConfiguration() - .handleEvents(receiveCompletion: { (completion) in - if case .failure(let error) = completion { - os_log(.error, "Failed to tell the tunnel to reload configuration: %{public}s", error.localizedDescription) - } - }) - .replaceError(with: ()) - .setFailureType(to: RegenerateWireguardPrivateKeyError.self) - .eraseToAnyPublisher() - } - } - .mapError { TunnelManagerError.regenerateWireguardPrivateKey($0) } - .eraseToAnyPublisher() + return self.rpc.checkWireguardKey( + accountToken: keychainEntry.accountToken, + publicKey: publicKey + ) } - }.eraseToAnyPublisher() + } + + let sendRequest = rpc.checkWireguardKey() + .injectResult(from: makeRequest) + + sendRequest.addDidFinishBlockObserver { (operation, result) in + completionHandler(result.mapError { Error.verifyWireguardKey($0) }) + } + + operationQueue.addOperations([makeRequest, sendRequest], waitUntilFinished: false) } - func setRelayConstraints(_ constraints: RelayConstraints) -> AnyPublisher<(), TunnelManagerError> { - MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { - Just(self.accountToken) - .setFailureType(to: TunnelManagerError.self) - .replaceNil(with: .missingAccount) - .flatMap { (accountToken) in - TunnelConfigurationManager - .update(searchTerm: .accountToken(accountToken)) { (tunnelConfig) in - tunnelConfig.relayConstraints = constraints - }.mapError { TunnelManagerError.setRelayConstraints($0) } - .publisher - .flatMap { _ in - // Ignore Packet Tunnel IPC errors but log them - self.reloadPacketTunnelConfiguration() - .replaceError(with: ()) - .setFailureType(to: TunnelManagerError.self) - .handleEvents(receiveCompletion: { (completion) in - if case .failure(let error) = completion { - os_log(.error, "Failed to tell the tunnel to reload configuration: %{public}s", error.localizedDescription) - } - }) + func regeneratePrivateKey(completionHandler: @escaping (Result<(), Error>) -> Void) { + let operation = ResultOperation<(), Error> { (finish) in + guard let accountToken = self.accountToken else { + finish(.failure(.missingAccount)) + return + } + + let result = Self.loadTunnelSettings(accountToken: accountToken) + guard case .success(let keychainEntry) = result else { + finish(result.map { _ in () }) + return + } + + let newPrivateKey = WireguardPrivateKey() + let oldPublicKey = keychainEntry.tunnelSettings.interface + .privateKey + .publicKey + + self.replaceWireguardKeyAndUpdateSettings(accountToken: accountToken, oldPublicKey: oldPublicKey, newPrivateKey: newPrivateKey) { (result) in + guard case .success = result else { + finish(result) + return + } + + // Save new public key + self.publicKey = newPrivateKey.publicKey + + guard let tunnelIpc = self.tunnelIpc else { + finish(.success(())) + return + } + + tunnelIpc.reloadTunnelSettings { (ipcResult) in + if case .failure(let error) = ipcResult { + // Ignore Packet Tunnel IPC errors but log them + error.logChain(message: "Failed to IPC the tunnel to reload configuration") } + + finish(.success(())) + } } - }.eraseToAnyPublisher() + } + + operation.addDidFinishBlockObserver { (operation, result) in + completionHandler(result) + } + + exclusityController.addOperation(operation, categories: [.tunnelControl]) } - func getRelayConstraints() -> AnyPublisher<RelayConstraints, TunnelManagerError> { - MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { - Just(self.accountToken) - .setFailureType(to: TunnelManagerError.self) - .replaceNil(with: .missingAccount) - .flatMap { (accountToken) in - TunnelConfigurationManager.load(searchTerm: .accountToken(accountToken)) - .map { $0.tunnelConfiguration.relayConstraints } - .flatMapError { (error) -> Result<RelayConstraints, TunnelConfigurationManager.Error> in - // Return default constraints if the config is not found in Keychain - if case .lookupEntry(.itemNotFound) = error { - return .success(TunnelConfiguration().relayConstraints) - } else { - return .failure(error) - } - }.mapError { .getRelayConstraints($0) } - .publisher + func setRelayConstraints(_ constraints: RelayConstraints, completionHandler: @escaping (Result<(), TunnelManager.Error>) -> Void) { + let operation = ResultOperation<(), TunnelManager.Error> { (finish) in + guard let accountToken = self.accountToken else { + finish(.failure(.missingAccount)) + return + } + + let result = Self.updateTunnelSettings(accountToken: accountToken) { (tunnelSettings) in + tunnelSettings.relayConstraints = constraints + } + + guard case .success = result else { + finish(result.map { _ in () }) + return + } + + guard let tunnelIpc = self.tunnelIpc else { + finish(.success(())) + return + } + + tunnelIpc.reloadTunnelSettings { (ipcResult) in + // Ignore Packet Tunnel IPC errors but log them + if case .failure(let error) = ipcResult { + error.logChain(message: "Failed to reload tunnel settings") + } + + finish(.success(())) } - }.eraseToAnyPublisher() + } + + operation.addDidFinishBlockObserver { (operation, result) in + completionHandler(result) + } + + exclusityController.addOperation(operation, categories: [.tunnelControl]) } - // MARK: - Private + func getRelayConstraints(completionHandler: @escaping (Result<RelayConstraints, TunnelManager.Error>) -> Void) { + let operation = BlockOperation { + guard let accountToken = self.accountToken else { + completionHandler(.failure(.missingAccount)) + return + } - /// Tell Packet Tunnel process to reload the tunnel configuration - private func reloadPacketTunnelConfiguration() -> AnyPublisher<(), TunnelIpcRequestError> { - return executeIpcRequest { $0.reloadConfiguration() } + let result = Self.loadTunnelSettings(accountToken: accountToken) + .map { (keychainEntry) -> RelayConstraints in + return keychainEntry.tunnelSettings.relayConstraints + } + + completionHandler(result) + } + + exclusityController.addOperation(operation, categories: [.tunnelControl]) } - /// Ask Packet Tunnel process to return the current tunnel connection info - private func getTunnelConnectionInfo() -> AnyPublisher<TunnelConnectionInfo, TunnelIpcRequestError> { - return executeIpcRequest { $0.getTunnelInformation() } + // MARK: - Tunnel observeration + + /// 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)) } - /// IPC interactor helper that automatically maps the `PacketTunnelIpcError` to - /// `TunnelIpcRequestError` - private func executeIpcRequest<T>( - _ body: @escaping (PacketTunnelIpc) -> AnyPublisher<T, PacketTunnelIpcError> - ) -> AnyPublisher<T, TunnelIpcRequestError> - { - Just(tunnelIpc) - .setFailureType(to: TunnelIpcRequestError.self) - .replaceNil(with: .missingIpc) - .flatMap { (tunnelIpc) in - body(tunnelIpc) - .mapError { .send($0) } - }.eraseToAnyPublisher() + /// Remove tunnel observer. + func removeObserver<T: TunnelObserver>(_ observer: T) { + observerList.remove(AnyTunnelObserver(observer)) } + // MARK: - Operation management + + enum OperationCategory { + case tunnelControl + case stateUpdate + } + + private lazy var operationQueue: OperationQueue = { + let queue = OperationQueue() + queue.underlyingQueue = self.dispatchQueue + return queue + }() + private lazy var exclusityController: ExclusivityController<OperationCategory> = { + return ExclusivityController(operationQueue: self.operationQueue) + }() + + // MARK: - Private methods + /// Set the instance of the active tunnel and add the tunnel status observer private func setTunnelProvider(tunnelProvider: TunnelProviderManagerType) { - guard self.tunnelProvider != tunnelProvider else { return } - - let connection = tunnelProvider.connection + guard self.tunnelProvider != tunnelProvider else { + return + } // Save the new active tunnel provider self.tunnelProvider = tunnelProvider // Set up tunnel IPC - if let session = connection as? VPNTunnelProviderSessionProtocol { - self.tunnelIpc = PacketTunnelIpc(session: session) - } + let connection = tunnelProvider.connection + let session = connection as! VPNTunnelProviderSessionProtocol + let tunnelIpc = PacketTunnelIpc(session: session) + self.tunnelIpc = tunnelIpc // Register for tunnel connection status changes - tunnelStatusSubscriber = NotificationCenter.default.publisher(for: .NEVPNStatusDidChange, object: connection) - .map { notification in - (notification.object as? VPNConnectionProtocol)?.status - } - .receive(on: executionQueue) - .sink { [weak self] (connectionStatus) in - guard let connectionStatus = connectionStatus else { return } + unregisterConnectionObserver() + connectionStatusObserver = NotificationCenter.default + .addObserver(forName: .NEVPNStatusDidChange, object: connection, queue: nil) { + [weak self] (notification) in + guard let self = self else { return } + + let connection = notification.object as? VPNConnectionProtocol - self?.updateTunnelState(connectionStatus: connectionStatus) + if let status = connection?.status { + self.updateTunnelState(connectionStatus: status) + } } - // Update the existing connection status + // Update the existing state updateTunnelState(connectionStatus: connection.status) } + private func unregisterConnectionObserver() { + if let connectionStatusObserver = connectionStatusObserver { + NotificationCenter.default.removeObserver(connectionStatusObserver) + self.connectionStatusObserver = nil + } + } + private func loadPublicKey(accountToken: String) { - switch TunnelConfigurationManager.load(searchTerm: .accountToken(accountToken)) { + switch TunnelSettingsManager.load(searchTerm: .accountToken(accountToken)) { case .success(let entry): - self.publicKey = entry.tunnelConfiguration.interface.privateKey.publicKey + self.publicKey = entry.tunnelSettings.interface.privateKey.publicKey case .failure(let error): - os_log(.error, "Failed to load the public key: %{public}s", error.localizedDescription) + error.logChain(message: "Failed to load the public key") self.publicKey = nil } } + private func pushWireguardKeyAndUpdateSettings( + accountToken: String, + publicKey: WireguardPublicKey, + completionHandler: @escaping (Result<(), Error>) -> Void) + { + let request = self.rpc.pushWireguardKey( + accountToken: accountToken, + publicKey: publicKey.rawRepresentation + ) + + request.start { (rpcResult) in + self.dispatchQueue.async { + let updateResult = rpcResult + .mapError({ (rpcError) -> Error in + return Error.pushWireguardKey(rpcError) + }) + .flatMap { (associatedAddresses) -> Result<(), Error> in + return Self.updateTunnelSettings(accountToken: accountToken) { (tunnelSettings) in + tunnelSettings.interface.addresses = [ + associatedAddresses.ipv4Address, + associatedAddresses.ipv6Address + ] + }.map { _ in () } + } + + completionHandler(updateResult) + } + } + } + + private func removeWireguardKeyFromServer(accountToken: String, publicKey: Data, completionHandler: @escaping (Result<Bool, Error>) -> Void) { + let request = self.rpc.removeWireguardKey( + accountToken: accountToken, + publicKey: publicKey + ) + + request.start(completionHandler: { (result) in + self.dispatchQueue.async { + completionHandler(result.mapError { Error.removeWireguardKey($0) }) + } + }) + } + + private func replaceWireguardKeyAndUpdateSettings( + accountToken: String, + oldPublicKey: WireguardPublicKey, + newPrivateKey: WireguardPrivateKey, + completionHandler: @escaping (Result<(), Error>) -> Void) + { + let request = self.rpc.replaceWireguardKey( + accountToken: accountToken, + oldPublicKey: oldPublicKey.rawRepresentation, + newPublicKey: newPrivateKey.publicKey.rawRepresentation + ) + + request.start { (rpcResult) in + self.dispatchQueue.async { + let updateResult = rpcResult + .mapError({ (rpcError) -> Error in + return Error.replaceWireguardKey(rpcError) + }) + .flatMap { (associatedAddresses) -> Result<(), Error> in + return Self.updateTunnelSettings(accountToken: accountToken) { (tunnelSettings) in + tunnelSettings.interface.privateKey = newPrivateKey + tunnelSettings.interface.addresses = [ + associatedAddresses.ipv4Address, + associatedAddresses.ipv6Address + ] + }.map { _ in () } + } + + completionHandler(updateResult) + } + } + } + /// Initiates the `tunnelState` update private func updateTunnelState(connectionStatus: NEVPNStatus) { - os_log(.default, "VPN Status: %{public}s", "\(connectionStatus)") + let operation = AsyncBlockOperation { (finish) in + self.mapTunnelState(connectionStatus: connectionStatus) { (result) in + switch result { + case .success(let tunnelState): + self.tunnelState = tunnelState - mapTunnelStateSubscriber = mapTunnelState(connectionStatus: connectionStatus) - .receive(on: executionQueue) - .sink(receiveCompletion: { (completion) in - if case .failure(let error) = completion { - os_log(.error, "Failed to map the tunnel state: %{public}s", error.localizedDescription) + case .failure(let error): + error.logChain(message: "Failed to map the tunnel state") } - }, receiveValue: { (tunnelState) in - os_log(.default, "Set tunnel state: %{public}s", String(reflecting: tunnelState)) - self.tunnelState = tunnelState - }) + + finish() + } + } + + exclusityController.addOperation(operation, categories: [.stateUpdate]) } /// Maps `NEVPNStatus` to `TunnelState`. /// Collects the `TunnelConnectionInfo` from the tunnel via IPC if needed before assigning the /// `tunnelState` - private func mapTunnelState(connectionStatus: NEVPNStatus) -> AnyPublisher<TunnelState, MapConnectionStatusError> { - Just(connectionStatus) - .setFailureType(to: MapConnectionStatusError.self) - .flatMap { (connectionStatus) -> AnyPublisher<TunnelState, MapConnectionStatusError> in - switch connectionStatus { - case .connected: - return self.getTunnelConnectionInfo() - .mapError { .ipcRequest($0) } - .map { .connected($0) } - .eraseToAnyPublisher() + private func mapTunnelState(connectionStatus: NEVPNStatus, completionHandler: @escaping (Result<TunnelState, MapConnectionStatusError>) -> Void) { + switch connectionStatus { + case .connected: + guard let tunnelIpc = tunnelIpc else { + completionHandler(.failure(.missingIpc)) + return + } - case .connecting: - return Result.Publisher(TunnelState.connecting) - .eraseToAnyPublisher() + tunnelIpc.getTunnelInformation { (result) in + self.dispatchQueue.async { + let result = result.map { TunnelState.connected($0) } + .mapError { MapConnectionStatusError.ipcRequest($0) } - case .disconnected: - return Result.Publisher(TunnelState.disconnected) - .eraseToAnyPublisher() + completionHandler(result) + } + } - case .disconnecting: - return Result.Publisher(TunnelState.disconnecting) - .eraseToAnyPublisher() + case .connecting: + completionHandler(.success(.connecting)) - case .reasserting: - // Refresh the last known public key on reconnect to cover the possibility of - // the key being changed due to key rotation. - if let accountToken = self.accountToken { - self.loadPublicKey(accountToken: accountToken) - } + case .disconnected: + completionHandler(.success(.disconnected)) - return self.getTunnelConnectionInfo() - .mapError { .ipcRequest($0) } - .map { .reconnecting($0) } - .eraseToAnyPublisher() + case .disconnecting: + completionHandler(.success(.disconnecting)) - case .invalid: - return Fail(error: MapConnectionStatusError.invalidConfiguration(connectionStatus)) - .eraseToAnyPublisher() + case .reasserting: + // Refresh the last known public key on reconnect to cover the possibility of + // the key being changed due to key rotation. + if let accountToken = self.accountToken { + self.loadPublicKey(accountToken: accountToken) + } - @unknown default: - return Fail(error: MapConnectionStatusError.unknownStatus(connectionStatus)) - .eraseToAnyPublisher() + guard let tunnelIpc = tunnelIpc else { + completionHandler(.failure(.missingIpc)) + return + } + + tunnelIpc.getTunnelInformation { (result) in + self.dispatchQueue.async { + let result = result.map { TunnelState.reconnecting($0) } + .mapError { MapConnectionStatusError.ipcRequest($0) } + + completionHandler(result) } - }.eraseToAnyPublisher() + } + + case .invalid: + completionHandler(.failure(.invalidConfiguration)) + + @unknown default: + completionHandler(.failure(.unknownStatus(connectionStatus))) + } } - /// Retrieve the existing TunnelConfiguration or create a new one - private func makeTunnelConfiguration(accountToken: String) -> Result<TunnelConfiguration, TunnelConfigurationManager.Error> { - TunnelConfigurationManager.load(searchTerm: .accountToken(accountToken)) - .map { $0.tunnelConfiguration } - .flatMapError { (error) -> Result<TunnelConfiguration, TunnelConfigurationManager.Error> in + private func makeTunnelProvider(accountToken: String, completionHandler: @escaping (Result<TunnelProviderManagerType, TunnelManager.Error>) -> Void) { + TunnelProviderManagerType.loadAllFromPreferences { (tunnels, error) in + self.dispatchQueue.async { + if let error = error { + completionHandler(.failure(.loadAllVPNConfigurations(error))) + } else { + let result = Self.setupTunnelProvider(accountToken: accountToken, tunnels: tunnels) + + guard case .success(let tunnelProvider) = result else { + completionHandler(result) + return + } + + tunnelProvider.saveToPreferences { (error) in + self.dispatchQueue.async { + if let error = error { + completionHandler(.failure(.saveVPNConfiguration(error))) + } else { + // Refresh connection status after saving the tunnel preferences. + // Basically it's only necessary to do for new instances of + // `NETunnelProviderManager`, but we do that for the existing ones too + // for simplicity as it has no side effects. + tunnelProvider.loadFromPreferences { (error) in + self.dispatchQueue.async { + if let error = error { + completionHandler(.failure(.reloadVPNConfiguration(error))) + } else { + completionHandler(.success(tunnelProvider)) + } + } + } + } + } + } + + } + } + } + } + + // MARK: - Private class methods + + private class func loadTunnelSettings(accountToken: String) -> Result<TunnelSettingsManager.KeychainEntry, Error> { + return TunnelSettingsManager.load(searchTerm: .accountToken(accountToken)) + .mapError { Error.readTunnelSettings($0) } + } + + 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) } + } + + /// Retrieve the existing `TunnelSettings` or create the new ones + private class func makeTunnelSettings(accountToken: String) -> Result<TunnelSettings, TunnelManager.Error> { + return TunnelSettingsManager.load(searchTerm: .accountToken(accountToken)) + .map { $0.tunnelSettings } + .flatMapError { (error) -> Result<TunnelSettings, TunnelManager.Error> in // Return default tunnel configuration if the config is not found in Keychain if case .lookupEntry(.itemNotFound) = error { - let defaultConfiguration = TunnelConfiguration() + let defaultConfiguration = TunnelSettings() - return TunnelConfigurationManager + return TunnelSettingsManager .add(configuration: defaultConfiguration, account: accountToken) + .mapError { .addTunnelSettings($0) } .map { defaultConfiguration } } else { - return .failure(error) + return .failure(.readTunnelSettings(error)) } } } - private func setupTunnel(accountToken: String) -> AnyPublisher<TunnelProviderManagerType, SetupTunnelError> { - TunnelProviderManagerType.loadAllFromPreferences() - .receive(on: executionQueue) - .mapError { SetupTunnelError.loadTunnels($0) } - .map { (tunnels) in - // Return the first available tunnel or make a new one - return tunnels?.first ?? TunnelProviderManagerType() - } - .flatMap { (tunnelProvider) in - TunnelConfigurationManager.getPersistentKeychainReference(account: accountToken) - .mapError { SetupTunnelError.obtainKeychainRef($0) } - .map { (tunnelProvider, $0) } - .publisher - } - .flatMap { (tunnelProvider, passwordReference) -> AnyPublisher<TunnelProviderManagerType, SetupTunnelError> in - tunnelProvider.isEnabled = true - tunnelProvider.localizedDescription = "WireGuard" - tunnelProvider.protocolConfiguration = self.makeProtocolConfiguration( - accountToken: accountToken, - passwordReference: passwordReference - ) - - // 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 + 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() - return tunnelProvider.saveToPreferences() - .mapError { SetupTunnelError.saveTunnel($0) } - .flatMap { - // 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() - .mapError { SetupTunnelError.reloadTunnel($0) } - } - .map { tunnelProvider } - .receive(on: self.executionQueue) - .handleEvents(receiveCompletion: { (completion) in - if case .finished = completion { - self.setTunnelProvider(tunnelProvider: tunnelProvider) - } - }).eraseToAnyPublisher() - }.eraseToAnyPublisher() - } + let protocolConfig = NETunnelProviderProtocol() + protocolConfig.providerBundleIdentifier = ApplicationConfiguration.packetTunnelExtensionIdentifier + protocolConfig.serverAddress = "" + protocolConfig.username = accountToken + protocolConfig.passwordReference = passwordReference - /// Produce the new tunnel provider protocol configuration - private func makeProtocolConfiguration(accountToken: String, passwordReference: Data) -> NETunnelProviderProtocol { - let protocolConfig = NETunnelProviderProtocol() - protocolConfig.providerBundleIdentifier = ApplicationConfiguration.packetTunnelExtensionIdentifier - protocolConfig.serverAddress = "" - protocolConfig.username = accountToken - protocolConfig.passwordReference = passwordReference + tunnelProvider.isEnabled = true + tunnelProvider.localizedDescription = "WireGuard" + tunnelProvider.protocolConfiguration = protocolConfig - return 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 - private func updateAssociatedAddresses( - accountToken: String, - addresses: WireguardAssociatedAddresses - ) -> Result<TunnelConfiguration, TunnelConfigurationManager.Error> - { - TunnelConfigurationManager.update(searchTerm: .accountToken(accountToken)) { (tunnelConfig) in - tunnelConfig.interface.addresses = [ - addresses.ipv4Address, - addresses.ipv6Address - ] + return tunnelProvider + }.mapError { (error) -> Error in + return .obtainPersistentKeychainReference(error) } } - private func migrateTunnelConfiguration(accountToken: String) { - let result = TunnelConfigurationManager + private class func migrateTunnelSettings(accountToken: String) { + let result = TunnelSettingsManager .migrateKeychainEntry(searchTerm: .accountToken(accountToken)) switch result { @@ -892,50 +1050,11 @@ class TunnelManager { if migrated { os_log("Migrated Keychain tunnel configuration") } else { - os_log("Tunnel configuration is up to date. No migration needed.") + os_log("Tunnel settings are up to date. No migration needed.") } case .failure(let error): - os_log("Failed to migrate tunnel configuration: %{public}s", - error.localizedDescription) - } - } - -} - -/// Convenience methods to provide `Future` based alternatives for working with -/// `TunnelProviderManager` -private extension VPNTunnelProviderManagerProtocol { - - static func loadAllFromPreferences() -> Future<[SelfType]?, Error> { - Future { (fulfill) in - self.loadAllFromPreferences { (tunnels, error) in - fulfill(error.flatMap { .failure($0) } ?? .success(tunnels)) - } - } - } - - func loadFromPreferences() -> Future<(), Error> { - Future { (fulfill) in - self.loadFromPreferences { (error) in - fulfill(error.flatMap { .failure($0) } ?? .success(())) - } - } - } - - func saveToPreferences() -> Future<(), Error> { - Future { (fulfill) in - self.saveToPreferences { (error) in - fulfill(error.flatMap { .failure($0) } ?? .success(())) - } - } - } - - func removeFromPreferences() -> Future<(), Error> { - Future { (fulfill) in - self.removeFromPreferences { (error) in - fulfill(error.flatMap { .failure($0) } ?? .success(())) - } + error.logChain(message: "Failed to migrate tunnel settings") } } diff --git a/ios/MullvadVPN/TunnelConfiguration.swift b/ios/MullvadVPN/TunnelSettings.swift index cae302b6fd..b7da8df890 100644 --- a/ios/MullvadVPN/TunnelConfiguration.swift +++ b/ios/MullvadVPN/TunnelSettings.swift @@ -1,5 +1,5 @@ // -// TunnelConfiguration.swift +// TunnelSettings.swift // MullvadVPN // // Created by pronebird on 19/06/2019. @@ -11,13 +11,13 @@ import Network import NetworkExtension /// A struct that holds a tun interface configuration -struct InterfaceConfiguration: Codable { +struct InterfaceSettings: Codable { var privateKey = WireguardPrivateKey() var addresses = [IPAddressRange]() } /// A struct that holds the configuration passed via NETunnelProviderProtocol -struct TunnelConfiguration: Codable { +struct TunnelSettings: Codable { var relayConstraints = RelayConstraints() - var interface = InterfaceConfiguration() + var interface = InterfaceSettings() } diff --git a/ios/MullvadVPN/TunnelConfigurationManager.swift b/ios/MullvadVPN/TunnelSettingsManager.swift index e0d24672f9..c30440324a 100644 --- a/ios/MullvadVPN/TunnelConfigurationManager.swift +++ b/ios/MullvadVPN/TunnelSettingsManager.swift @@ -1,5 +1,5 @@ // -// TunnelConfigurationManager.swift +// TunnelSettingsManager.swift // MullvadVPN // // Created by pronebird on 02/10/2019. @@ -15,12 +15,12 @@ private let kServiceName = "Mullvad VPN" /// Maximum number of attempts to perform when updating the Keychain entry "atomically" private let kMaxAtomicUpdateRetryLimit = 20 -enum TunnelConfigurationManager {} +enum TunnelSettingsManager {} -extension TunnelConfigurationManager { +extension TunnelSettingsManager { - enum Error: Swift.Error { - /// A failure to encode the given tunnel configuration + enum Error: ChainedError { + /// A failure to encode the given tunnel settings case encode(Swift.Error) /// A failure to decode the data stored in Keychain @@ -44,7 +44,7 @@ extension TunnelConfigurationManager { typealias Result<T> = Swift.Result<T, Error> - /// Keychain access level that should be used for all items containing tunnel configuration + /// Keychain access level that should be used for all items containing tunnel settings private static let keychainAccessibleLevel = Keychain.Accessible.afterFirstUnlock enum KeychainSearchTerm { @@ -71,7 +71,7 @@ extension TunnelConfigurationManager { struct KeychainEntry { let accountToken: String - let tunnelConfiguration: TunnelConfiguration + let tunnelSettings: TunnelSettings } static func load(searchTerm: KeychainSearchTerm) -> Result<KeychainEntry> { @@ -86,11 +86,11 @@ extension TunnelConfigurationManager { let data = attributes.valueData! return Self.decode(data: data) - .map { KeychainEntry(accountToken: account, tunnelConfiguration: $0) } + .map { KeychainEntry(accountToken: account, tunnelSettings: $0) } } } - static func add(configuration: TunnelConfiguration, account: String) -> Result<()> { + static func add(configuration: TunnelSettings, account: String) -> Result<()> { Self.encode(tunnelConfig: configuration) .flatMap { (data) -> Result<()> in var attributes = KeychainSearchTerm.accountToken(account) @@ -157,14 +157,14 @@ extension TunnelConfigurationManager { } } - /// Reads the tunnel configuration from Keychain, then passes it to the given closure for + /// Reads the tunnel settings from Keychain, then passes it to the given closure for /// modifications, saves the result back to Keychain. /// /// The given block may run multiple times if Keychain entry was changed between read and write /// operations. static func update(searchTerm: KeychainSearchTerm, - using changeConfiguration: (inout TunnelConfiguration) -> Void) - -> Result<TunnelConfiguration> + using changeConfiguration: (inout TunnelSettings) -> Void) + -> Result<TunnelSettings> { for _ in (0 ..< kMaxAtomicUpdateRetryLimit) { var searchQuery = searchTerm.makeKeychainAttributes() @@ -172,7 +172,7 @@ extension TunnelConfigurationManager { let result = Keychain.findFirst(query: searchQuery) .mapError { .lookupEntry($0) } - .flatMap { (itemAttributes) -> Result<TunnelConfiguration> in + .flatMap { (itemAttributes) -> Result<TunnelSettings> in let itemAttributes = itemAttributes! let serializedData = itemAttributes.valueData! let account = itemAttributes.account! @@ -185,12 +185,12 @@ extension TunnelConfigurationManager { ?? KeychainItemRevision.firstRevision() return Self.decode(data: serializedData) - .flatMap { (tunnelConfig) -> Result<TunnelConfiguration> in + .flatMap { (tunnelConfig) -> Result<TunnelSettings> in var tunnelConfig = tunnelConfig changeConfiguration(&tunnelConfig) return Self.encode(tunnelConfig: tunnelConfig) - .flatMap { (newData) -> Result<TunnelConfiguration> in + .flatMap { (newData) -> Result<TunnelSettings> in // `SecItemUpdate` does not accept query parameters when using // persistent reference, so constraint the query to account // token instead now when we know it @@ -245,13 +245,13 @@ extension TunnelConfigurationManager { } } - private static func encode(tunnelConfig: TunnelConfiguration) -> Result<Data> { + private static func encode(tunnelConfig: TunnelSettings) -> Result<Data> { return Swift.Result { try JSONEncoder().encode(tunnelConfig) } .mapError { .encode($0) } } - private static func decode(data: Data) -> Result<TunnelConfiguration> { - return Swift.Result { try JSONDecoder().decode(TunnelConfiguration.self, from: data) } + private static func decode(data: Data) -> Result<TunnelSettings> { + return Swift.Result { try JSONDecoder().decode(TunnelSettings.self, from: data) } .mapError { .decode($0) } } } diff --git a/ios/MullvadVPN/WireguardKeysViewController.swift b/ios/MullvadVPN/WireguardKeysViewController.swift index e29035b5c5..d7ed4dfe03 100644 --- a/ios/MullvadVPN/WireguardKeysViewController.swift +++ b/ios/MullvadVPN/WireguardKeysViewController.swift @@ -6,13 +6,12 @@ // Copyright © 2019 Mullvad VPN AB. All rights reserved. // -import Combine import Foundation import UIKit import os /// A UI refresh interval for the public key creation date (in seconds) -private let kCreationDateRefreshInterval = TimeInterval(60) +private let kCreationDateRefreshInterval = Int(60) /// A maximum number of characters to display out of the entire public key representation private let kDisplayPublicKeyMaxLength = 20 @@ -24,34 +23,7 @@ private enum WireguardKeysViewState { case regeneratingKey } -private struct VerifyWireguardPublicKeyError: Error { - var underlyingError: MullvadRpc.Error - - init(_ error: MullvadRpc.Error) { - self.underlyingError = error - } -} - -extension VerifyWireguardPublicKeyError: LocalizedError { - var errorDescription: String? { - return NSLocalizedString("Cannot verify the public key", comment: "") - } - - var failureReason: String? { - switch underlyingError { - case .network(let urlError): - return urlError.localizedDescription - - case .server(let serverError): - return serverError.errorDescription - - case .decoding, .encoding: - return NSLocalizedString("Internal error", comment: "") - } - } -} - -class WireguardKeysViewController: UIViewController { +class WireguardKeysViewController: UIViewController, TunnelObserver { @IBOutlet var publicKeyButton: UIButton! @IBOutlet var creationDateLabel: UILabel! @@ -59,14 +31,10 @@ class WireguardKeysViewController: UIViewController { @IBOutlet var verifyKeyButton: UIButton! @IBOutlet var wireguardKeyStatusView: WireguardKeyStatusView! - private var publicKeySubscriber: AnyCancellable? - private var loadKeySubscriber: AnyCancellable? - private var verifyKeySubscriber: AnyCancellable? - private var regenerateKeySubscriber: AnyCancellable? - private var creationDateTimerSubscriber: AnyCancellable? - private var copyToPasteboardSubscriber: AnyCancellable? + private var publicKeyPeriodicUpdateTimer: DispatchSourceTimer? + private var copyToPasteboardWork: DispatchWorkItem? - private let rpc = MullvadRpc.withEphemeralURLSession() + private let alertPresenter = AlertPresenter() private var state: WireguardKeysViewState = .default { didSet { @@ -77,23 +45,36 @@ class WireguardKeysViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - creationDateTimerSubscriber = Timer.publish(every: kCreationDateRefreshInterval, on: .main, in: .common) - .autoconnect() - .sink { [weak self] _ in - let publicKey = TunnelManager.shared.publicKey + TunnelManager.shared.addObserver(self) + updatePublicKey(publicKey: TunnelManager.shared.publicKey, animated: false) - self?.updatePublicKey(publicKey: publicKey, animated: true) + startPublicKeyPeriodicUpdate() + } + + private func startPublicKeyPeriodicUpdate() { + let interval = DispatchTimeInterval.seconds(kCreationDateRefreshInterval) + let timerSource = DispatchSource.makeTimerSource(queue: .main) + timerSource.setEventHandler { [weak self] () -> Void in + let publicKey = TunnelManager.shared.publicKey + + self?.updatePublicKey(publicKey: publicKey, animated: true) } + timerSource.schedule(deadline: .now() + interval, repeating: interval) + timerSource.activate() - publicKeySubscriber = TunnelManager.shared.$publicKey - .dropFirst() - .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] (publicKey) in - self?.updatePublicKey(publicKey: publicKey, animated: true) - }) + self.publicKeyPeriodicUpdateTimer = timerSource + } - // Set public key title without animation - updatePublicKey(publicKey: TunnelManager.shared.publicKey, animated: false) + // MARK: - TunnelObserver + + func tunnelStateDidChange(tunnelState: TunnelState) { + // no-op + } + + func tunnelPublicKeyDidChange(publicKey: WireguardPublicKey?) { + DispatchQueue.main.async { + self.updatePublicKey(publicKey: publicKey, animated: true) + } } // MARK: - IBActions @@ -107,15 +88,16 @@ class WireguardKeysViewController: UIViewController { string: NSLocalizedString("COPIED TO PASTEBOARD!", comment: ""), animated: true) - copyToPasteboardSubscriber = - Just(()).cancellableDelay(for: .seconds(3), scheduler: DispatchQueue.main) - .sink(receiveValue: { [weak self] () in - guard let self = self else { return } + let dispatchWork = DispatchWorkItem { [weak self] in + let publicKey = TunnelManager.shared.publicKey - let publicKey = TunnelManager.shared.publicKey + self?.updatePublicKey(publicKey: publicKey, animated: true) + } - self.updatePublicKey(publicKey: publicKey, animated: true) - }) + DispatchQueue.main.asyncAfter(wallDeadline: .now() + .seconds(3), execute: dispatchWork) + + self.copyToPasteboardWork?.cancel() + self.copyToPasteboardWork = dispatchWork } @IBAction func handleRegenerateKey(_ sender: Any) { @@ -123,10 +105,7 @@ class WireguardKeysViewController: UIViewController { } @IBAction func handleVerifyKey(_ sender: Any) { - guard let accountToken = Account.shared.token, - let publicKey = TunnelManager.shared.publicKey else { return } - - verifyKey(accountToken: accountToken, publicKey: publicKey) + verifyKey() } // MARK: - Private @@ -183,50 +162,58 @@ class WireguardKeysViewController: UIViewController { verifyKeyButton.isEnabled = enabled } - private func verifyKey(accountToken: String, publicKey: WireguardPublicKey) { - verifyKeySubscriber = rpc.checkWireguardKey( - accountToken: accountToken, - publicKey: publicKey.rawRepresentation - ) - .retry(1) - .receive(on: DispatchQueue.main) - .mapError { VerifyWireguardPublicKeyError($0) } - .handleEvents(receiveSubscription: { _ in - self.updateViewState(.verifyingKey) - }) - .sink(receiveCompletion: { (completion) in - switch completion { - case .finished: - break + private func verifyKey() { + self.updateViewState(.verifyingKey) + + TunnelManager.shared.verifyPublicKey { (result) in + DispatchQueue.main.async { + switch result { + case .success(let isValid): + self.updateViewState(.verifiedKey(isValid)) case .failure(let error): - self.presentError(error, preferredStyle: .alert) + let alertController = UIAlertController( + title: NSLocalizedString("Cannot verify the key", comment: ""), + message: error.errorChainDescription, + preferredStyle: .alert + ) + alertController.addAction( + UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel) + ) + + self.alertPresenter.enqueue(alertController, presentingController: self) self.updateViewState(.default) } - }) { (isValid) in - self.updateViewState(.verifiedKey(isValid)) + } } } private func regeneratePrivateKey() { - regenerateKeySubscriber = TunnelManager.shared.regeneratePrivateKey() - .receive(on: DispatchQueue.main) - .handleEvents(receiveSubscription: { (_) in - self.updateViewState(.regeneratingKey) - }, receiveCompletion: { (completion) in - self.updateViewState(.default) - }) - .sink { (completion) in - switch completion { - case .finished: + self.updateViewState(.regeneratingKey) + + TunnelManager.shared.regeneratePrivateKey { (result) in + DispatchQueue.main.async { + switch result { + case .success: break case .failure(let error): - os_log(.error, "Failed to re-generate the private key: %{public}s", - error.errorDescription ?? "") + let alertController = UIAlertController( + title: NSLocalizedString("Cannot regenerate the key", comment: ""), + message: error.errorChainDescription, + preferredStyle: .alert + ) + alertController.addAction( + UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel) + ) - self.presentError(error, preferredStyle: .alert) + error.logChain(message: "Failed to regenerate the private key") + + self.alertPresenter.enqueue(alertController, presentingController: self) } + + self.updateViewState(.default) + } } } diff --git a/ios/MullvadVPN/WireguardPrivateKey.swift b/ios/MullvadVPN/WireguardPrivateKey.swift index 07defa7cf8..41bc98f5a9 100644 --- a/ios/MullvadVPN/WireguardPrivateKey.swift +++ b/ios/MullvadVPN/WireguardPrivateKey.swift @@ -6,7 +6,6 @@ // Copyright © 2019 Mullvad VPN AB. All rights reserved. // -import CryptoKit import Foundation /// A convenience wrapper around the wireguard key @@ -16,30 +15,27 @@ struct WireguardPrivateKey { let creationDate: Date /// Private key's raw representation - var rawRepresentation: Data { - innerPrivateKey.rawRepresentation - } + private(set) var rawRepresentation: Data /// Public key var publicKey: WireguardPublicKey { WireguardPublicKey( creationDate: creationDate, - rawRepresentation: innerPrivateKey.publicKey.rawRepresentation + rawRepresentation: Curve25519.generatePublicKey(fromPrivateKey: rawRepresentation) ) } - /// An inner impelementation of a private key - private let innerPrivateKey: Curve25519.KeyAgreement.PrivateKey - /// Initialize the new private key init() { - innerPrivateKey = Curve25519.KeyAgreement.PrivateKey() + rawRepresentation = Curve25519.generatePrivateKey() creationDate = Date() } /// Load with the existing private key - init(rawRepresentation: Data, createdAt: Date) throws { - innerPrivateKey = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: rawRepresentation) + init?(rawRepresentation: Data, createdAt: Date) { + guard rawRepresentation.count == Curve25519.keyLength else { return nil } + + self.rawRepresentation = rawRepresentation creationDate = createdAt } @@ -81,7 +77,7 @@ extension WireguardPrivateKey: Codable { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(innerPrivateKey.rawRepresentation, forKey: .privateKeyData) + try container.encode(rawRepresentation, forKey: .privateKeyData) try container.encode(creationDate, forKey: .creationDate) } @@ -90,6 +86,14 @@ extension WireguardPrivateKey: Codable { let privateKeyBytes = try container.decode(Data.self, forKey: .privateKeyData) let creationDate = try container.decode(Date.self, forKey: .creationDate) - self = try .init(rawRepresentation: privateKeyBytes, createdAt: creationDate) + if let instance = WireguardPrivateKey(rawRepresentation: privateKeyBytes, createdAt: creationDate) { + self = instance + } else { + throw DecodingError.dataCorruptedError( + forKey: CodingKeys.privateKeyData, + in: container, + debugDescription: "Invalid key data" + ) + } } } diff --git a/ios/MullvadVPN/x25519.c b/ios/MullvadVPN/x25519.c new file mode 100644 index 0000000000..b77da0b0ea --- /dev/null +++ b/ios/MullvadVPN/x25519.c @@ -0,0 +1,178 @@ +/* SPDX-License-Identifier: GPL-2.0+ + * + * Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. + * + * Curve25519 ECDH functions, based on TweetNaCl but cleaned up. + */ + +#include <stdint.h> +#include <string.h> +#include <assert.h> +#include <CommonCrypto/CommonRandom.h> + +#include "x25519.h" + +typedef int64_t fe[16]; + +static inline void carry(fe o) +{ + int i; + + for (i = 0; i < 16; ++i) { + o[(i + 1) % 16] += (i == 15 ? 38 : 1) * (o[i] >> 16); + o[i] &= 0xffff; + } +} + +static inline void cswap(fe p, fe q, int b) +{ + int i; + int64_t t, c = ~(b - 1); + + for (i = 0; i < 16; ++i) { + t = c & (p[i] ^ q[i]); + p[i] ^= t; + q[i] ^= t; + } +} + +static inline void pack(uint8_t *o, const fe n) +{ + int i, j, b; + fe m, t; + + memcpy(t, n, sizeof(t)); + carry(t); + carry(t); + carry(t); + for (j = 0; j < 2; ++j) { + m[0] = t[0] - 0xffed; + for (i = 1; i < 15; ++i) { + m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1); + m[i - 1] &= 0xffff; + } + m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1); + b = (m[15] >> 16) & 1; + m[14] &= 0xffff; + cswap(t, m, 1 - b); + } + for (i = 0; i < 16; ++i) { + o[2 * i] = t[i] & 0xff; + o[2 * i + 1] = t[i] >> 8; + } +} + +static inline void unpack(fe o, const uint8_t *n) +{ + int i; + + for (i = 0; i < 16; ++i) + o[i] = n[2 * i] + ((int64_t)n[2 * i + 1] << 8); + o[15] &= 0x7fff; +} + +static inline void add(fe o, const fe a, const fe b) +{ + int i; + + for (i = 0; i < 16; ++i) + o[i] = a[i] + b[i]; +} + +static inline void subtract(fe o, const fe a, const fe b) +{ + int i; + + for (i = 0; i < 16; ++i) + o[i] = a[i] - b[i]; +} + +static inline void multmod(fe o, const fe a, const fe b) +{ + int i, j; + int64_t t[31] = { 0 }; + + for (i = 0; i < 16; ++i) { + for (j = 0; j < 16; ++j) + t[i + j] += a[i] * b[j]; + } + for (i = 0; i < 15; ++i) + t[i] += 38 * t[i + 16]; + memcpy(o, t, sizeof(fe)); + carry(o); + carry(o); +} + +static inline void invert(fe o, const fe i) +{ + fe c; + int a; + + memcpy(c, i, sizeof(c)); + for (a = 253; a >= 0; --a) { + multmod(c, c, c); + if (a != 2 && a != 4) + multmod(c, c, i); + } + memcpy(o, c, sizeof(fe)); +} + +static void curve25519_shared_secret(uint8_t shared_secret[32], const uint8_t private_key[32], const uint8_t public_key[32]) +{ + static const fe a24 = { 0xdb41, 1 }; + uint8_t z[32]; + int64_t r; + int i; + fe a = { 1 }, b, c = { 0 }, d = { 1 }, e, f, x; + + memcpy(z, private_key, sizeof(z)); + + z[31] = (z[31] & 127) | 64; + z[0] &= 248; + + unpack(x, public_key); + memcpy(b, x, sizeof(b)); + + for (i = 254; i >= 0; --i) { + r = (z[i >> 3] >> (i & 7)) & 1; + cswap(a, b, (int)r); + cswap(c, d, (int)r); + add(e, a, c); + subtract(a, a, c); + add(c, b, d); + subtract(b, b, d); + multmod(d, e, e); + multmod(f, a, a); + multmod(a, c, a); + multmod(c, b, e); + add(e, a, c); + subtract(a, a, c); + multmod(b, a, a); + subtract(c, d, f); + multmod(a, c, a24); + add(a, a, d); + multmod(c, c, a); + multmod(a, d, f); + multmod(d, b, x); + multmod(b, e, e); + cswap(a, b, (int)r); + cswap(c, d, (int)r); + } + invert(c, c); + multmod(a, a, c); + pack(shared_secret, a); +} + +void curve25519_derive_public_key(uint8_t public_key[32], const uint8_t private_key[32]) +{ + static const uint8_t basepoint[32] = { 9 }; + + curve25519_shared_secret(public_key, private_key, basepoint); +} + +void curve25519_generate_private_key(uint8_t private_key[32]) +{ + assert(CCRandomGenerateBytes(private_key, 32) == kCCSuccess); + private_key[31] = (private_key[31] & 127) | 64; + private_key[0] &= 248; +} diff --git a/ios/MullvadVPN/x25519.h b/ios/MullvadVPN/x25519.h new file mode 100644 index 0000000000..7d8440dd3d --- /dev/null +++ b/ios/MullvadVPN/x25519.h @@ -0,0 +1,7 @@ +#ifndef X25519_H +#define X25519_H + +void curve25519_derive_public_key(unsigned char public_key[32], const unsigned char private_key[32]); +void curve25519_generate_private_key(unsigned char private_key[32]); + +#endif diff --git a/ios/PacketTunnel/PacketTunnel-Bridging-Header.h b/ios/PacketTunnel/PacketTunnel-Bridging-Header.h index 207551c8bb..9d77777330 100644 --- a/ios/PacketTunnel/PacketTunnel-Bridging-Header.h +++ b/ios/PacketTunnel/PacketTunnel-Bridging-Header.h @@ -2,5 +2,6 @@ // Use this file to import your target's public headers that you would like to expose to Swift. // +#include "x25519.h" #include "../wireguard-go-bridge/wireguard.h" #include "wireguard-go-version.h" diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index 6ca459ef3b..d9e40ed496 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -6,24 +6,23 @@ // Copyright © 2019 Mullvad VPN AB. All rights reserved. // -import Combine import Foundation import Network import NetworkExtension import os -enum PacketTunnelProviderError: Error { +enum PacketTunnelProviderError: ChainedError { /// Failure to read the relay cache case readRelayCache(RelayCacheError) /// Failure to satisfy the relay constraint case noRelaySatisfyingConstraint - /// Missing the persistent keychain reference to the tunnel configuration + /// Missing the persistent keychain reference to the tunnel settings case missingKeychainConfigurationReference - /// Failure to read the tunnel configuration from Keychain - case cannotReadTunnelConfiguration(TunnelConfigurationManager.Error) + /// Failure to read the tunnel settings from Keychain + case cannotReadTunnelSettings(TunnelSettingsManager.Error) /// Failure to set network settings case setNetworkSettings(Error) @@ -31,16 +30,19 @@ enum PacketTunnelProviderError: Error { /// Failure to start the Wireguard backend case startWireguardDevice(WireguardDevice.Error) + /// Failure to stop the Wireguard backend + case stopWireguardDevice(WireguardDevice.Error) + /// Failure to update the Wireguard configuration case updateWireguardConfiguration(Error) /// IPC handler failure - case ipcHandler(PacketTunnelIpcHandlerError) + case ipcHandler(PacketTunnelIpcHandler.Error) - var localizedDescription: String { + var errorDescription: String? { switch self { - case .readRelayCache(let relayError): - return "Failure to read the relay cache: \(relayError.localizedDescription)" + case .readRelayCache: + return "Failure to read the relay cache" case .noRelaySatisfyingConstraint: return "No relay satisfying the given constraint" @@ -48,27 +50,30 @@ enum PacketTunnelProviderError: Error { case .missingKeychainConfigurationReference: return "Invalid protocol configuration" - case .cannotReadTunnelConfiguration(let readError): - return "Cannot read tunnel configuration: \(readError.localizedDescription)" + case .cannotReadTunnelSettings: + return "Failure to read tunnel settings" + + case .setNetworkSettings: + return "Failure to set system network settings" - case .setNetworkSettings(let systemError): - return "Failed to set network settings: \(systemError.localizedDescription)" + case .startWireguardDevice: + return "Failure to start the WireGuard device" - case .startWireguardDevice(let deviceError): - return "Failure to start Wireguard: \(deviceError.localizedDescription)" + case .stopWireguardDevice: + return "Failure to stop the WireGuard device" - case .updateWireguardConfiguration(let error): - return "Failure to update Wireguard configuration: \(error.localizedDescription)" + case .updateWireguardConfiguration: + return "Failure to update the Wireguard configuration" - case .ipcHandler(let ipcError): - return "Failure to handle the IPC request: \(ipcError.localizedDescription)" + case .ipcHandler: + return "Failure to handle the IPC request" } } } struct PacketTunnelConfiguration { var persistentKeychainReference: Data - var tunnelConfig: TunnelConfiguration + var tunnelSettings: TunnelSettings var selectorResult: RelaySelectorResult } @@ -88,7 +93,7 @@ extension PacketTunnelConfiguration { } return WireguardConfiguration( - privateKey: tunnelConfig.interface.privateKey, + privateKey: tunnelSettings.interface.privateKey, peers: wireguardPeers, allowedIPs: [ IPAddressRange(address: IPv4Address.any, networkPrefixLength: 0), @@ -100,18 +105,26 @@ extension PacketTunnelConfiguration { class PacketTunnelProvider: NEPacketTunnelProvider { + enum OperationCategory { + case exclusive + } + /// Active wireguard device private var wireguardDevice: WireguardDevice? /// Active tunnel connection information private var connectionInfo: TunnelConnectionInfo? - private let cancellableSet = CancellableSet() - private var startStopTunnelSubscriber: AnyCancellable? - private var startedTunnel = false + private let dispatchQueue = DispatchQueue(label: "net.mullvad.MullvadVPN.PacketTunnel", qos: .utility) - private let exclusivityQueue = DispatchQueue(label: "net.mullvad.vpn.packet-tunnel.exclusivity-queue") - private let executionQueue = DispatchQueue(label: "net.mullvad.vpn.packet-tunnel.execution-queue") + private lazy var operationQueue: OperationQueue = { + let operationQueue = OperationQueue() + operationQueue.underlyingQueue = self.dispatchQueue + return operationQueue + }() + private lazy var exclusivityController: ExclusivityController<OperationCategory> = { + return ExclusivityController(operationQueue: self.operationQueue) + }() private var keyRotationManager: AutomaticKeyRotationManager? @@ -121,60 +134,87 @@ class PacketTunnelProvider: NEPacketTunnelProvider { self.configureLogger() } + // MARK: - Subclass + override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { - startStopTunnelSubscriber = self.startTunnel() - .sink(receiveCompletion: { (completion) in - switch completion { - case .finished: + os_log(.default, log: tunnelProviderLog, "Start the tunnel") + + let operation = AsyncBlockOperation { (finish) in + self.doStartTunnel { (result) in + switch result { + case .success: + os_log(.default, log: tunnelProviderLog, "Started the tunnel") completionHandler(nil) case .failure(let error): - os_log(.error, log: tunnelProviderLog, - "Failed to start the tunnel: %{public}s", error.localizedDescription) - + error.logChain(message: "Failed to start the tunnel", log: tunnelProviderLog) completionHandler(error) } - }) + + finish() + } + } + + exclusivityController.addOperation(operation, categories: [.exclusive]) } override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { - startStopTunnelSubscriber = stopTunnel(reason: reason) - .sink(receiveCompletion: { (completion) in + os_log(.default, log: tunnelProviderLog, "Stop the tunnel. Reason: %{public}s", "\(reason)") + + let operation = AsyncBlockOperation { (finish) in + self.doStopTunnel { (result) in + switch result { + case .success: + os_log(.default, log: tunnelProviderLog, "Stopped the tunnel") + case .failure(let error): + error.logChain(message: "Failed to stop the tunnel", log: tunnelProviderLog) + } + completionHandler() - }) + finish() + } + } + + exclusivityController.addOperation(operation, categories: [.exclusive]) } override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { - PacketTunnelIpcHandler.decodeRequest(messageData: messageData) - .mapError { PacketTunnelProviderError.ipcHandler($0) } - .receive(on: executionQueue) - .flatMap { (request) -> AnyPublisher<AnyEncodable, PacketTunnelProviderError> in - os_log(.default, log: tunnelProviderLog, "IPC request: %{public}s", "\(request)") + dispatchQueue.async { + let finishWithResult = { (result: Result<AnyEncodable, PacketTunnelProviderError>) in + let result = result.flatMap { (response) -> Result<Data, PacketTunnelProviderError> in + return PacketTunnelIpcHandler.encodeResponse(response: response) + .mapError { PacketTunnelProviderError.ipcHandler($0) } + } - switch request { + switch result { + case .success(let data): + completionHandler?(data) + + case .failure(let error): + error.logChain(log: tunnelProviderLog) + completionHandler?(nil) + } + } - case .reloadConfiguration: - return self.reloadTunnel() - .map { AnyEncodable(true) } - .eraseToAnyPublisher() + let decodeResult = PacketTunnelIpcHandler.decodeRequest(messageData: messageData) + .mapError { PacketTunnelProviderError.ipcHandler($0) } - case .tunnelInformation: - return Result.Publisher(AnyEncodable(self.connectionInfo)) - .eraseToAnyPublisher() + switch decodeResult { + case .success(let request): + switch request { + case .reloadTunnelSettings: + self.reloadTunnelSettings { (result) in + finishWithResult(result.map { AnyEncodable(true) }) + } + case .tunnelInformation: + finishWithResult(.success(AnyEncodable(self.connectionInfo))) } - }.flatMap({ (response) in - return PacketTunnelIpcHandler.encodeResponse(response: response) - .mapError { PacketTunnelProviderError.ipcHandler($0) } - }).autoDisposableSink(cancellableSet: cancellableSet, receiveCompletion: { (completion) in - if case .failure(let error) = completion { - os_log(.error, log: tunnelProviderLog, - "Failed to handle the app message: %{public}s", error.localizedDescription) - completionHandler?(nil) + + case .failure(let error): + finishWithResult(.failure(error)) } - }, receiveValue: { (responseData) in - completionHandler?(responseData) - }) + } } override func sleep(completionHandler: @escaping () -> Void) { @@ -186,111 +226,131 @@ class PacketTunnelProvider: NEPacketTunnelProvider { // Add code here to wake up. } - private func configureLogger() { - WireguardDevice.setLogger { (level, message) in - os_log(level.osLogType, log: wireguardLog, "%{public}s", message) - } - } + // MARK: - Tunnel management - private func startTunnel() -> AnyPublisher<(), PacketTunnelProviderError> { - MutuallyExclusive( - exclusivityQueue: exclusivityQueue, - executionQueue: executionQueue - ) { () -> AnyPublisher<(), PacketTunnelProviderError> in - os_log(.default, log: tunnelProviderLog, "Start the tunnel") - - self.startedTunnel = true + private func doStartTunnel(completionHandler: @escaping (Result<(), PacketTunnelProviderError>) -> Void) { + makePacketTunnelConfig { (result) in + guard case .success(let packetTunnelConfig) = result else { + completionHandler(result.map { _ in () }) + return + } - return self.makePacketTunnelConfigAndApplyNetworkSettings() - .flatMap { (packetTunnelConfiguration) in - Self.startWireguard( - packetFlow: self.packetFlow, - configuration: packetTunnelConfiguration.wireguardConfig - ) - .receive(on: self.executionQueue) - .handleEvents(receiveOutput: { (wireguardDevice) in - self.wireguardDevice = wireguardDevice + self.updateNetworkSettings(packetTunnelConfig: packetTunnelConfig) { (result) in + guard case .success = result else { + completionHandler(result) + return + } - self.startKeyRotation( - persistentKeychainReference: packetTunnelConfiguration - .persistentKeychainReference - ) - }).map { _ in () } - }.eraseToAnyPublisher() - }.eraseToAnyPublisher() - } + Self.startWireguardDevice(packetFlow: self.packetFlow, configuration: packetTunnelConfig.wireguardConfig) { (result) in + self.dispatchQueue.async { + guard case .success(let device) = result else { + completionHandler(result.map { _ in () }) + return + } - private func stopTunnel(reason: NEProviderStopReason) -> AnyPublisher<(), Never> { - MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { () -> AnyPublisher<(), Never> in - os_log(.default, log: tunnelProviderLog, - "Stop the tunnel. Reason: %{public}s", "\(String(reflecting: reason))") + let persistentKeychainReference = packetTunnelConfig.persistentKeychainReference + let keyRotationManager = AutomaticKeyRotationManager(persistentKeychainReference: persistentKeychainReference) + keyRotationManager.eventHandler = { (keyRotationEvent) in + self.dispatchQueue.async { + self.reloadTunnelSettings { (result) in + switch result { + case .success: + break - self.startedTunnel = false - self.stopKeyRotation() + case .failure(let error): + error.logChain(message: "Failed to reload tunnel settings", log: tunnelProviderLog) + } + } + } + } - if let device = self.wireguardDevice { - self.wireguardDevice = nil + self.wireguardDevice = device + self.keyRotationManager = keyRotationManager - // ignore errors at this point - return device.stop() - .replaceError(with: ()) - .assertNoFailure() - .eraseToAnyPublisher() - } else { - return Just(()) - .eraseToAnyPublisher() + RelayCache.shared.startPeriodicUpdates { + keyRotationManager.startAutomaticRotation { + self.dispatchQueue.async { + completionHandler(.success(())) + } + } + } + } + } } - }.eraseToAnyPublisher() + } } - private func reloadTunnel() -> AnyPublisher<(), PacketTunnelProviderError> { - MutuallyExclusive(exclusivityQueue: exclusivityQueue, executionQueue: executionQueue) { - () -> AnyPublisher<(), PacketTunnelProviderError> in - guard self.startedTunnel else { - os_log(.default, log: tunnelProviderLog, - "Ignore reloading tunnel settings. The tunnel has not started yet.") + private func doStopTunnel(completionHandler: @escaping (Result<(), PacketTunnelProviderError>) -> Void) { + guard let device = self.wireguardDevice, let keyRotationManager = self.keyRotationManager + else { + completionHandler(.success(())) + return + } - return Result.Publisher(()).eraseToAnyPublisher() + RelayCache.shared.stopPeriodicUpdates { + keyRotationManager.stopAutomaticRotation { + device.stop { (result) in + self.dispatchQueue.async { + self.wireguardDevice = nil + self.keyRotationManager = nil + + let result = result.mapError({ (error) -> PacketTunnelProviderError in + return .stopWireguardDevice(error) + }) + completionHandler(result) + } + } } + } + } - guard let wireguardDevice = self.wireguardDevice else { - os_log(.default, log: tunnelProviderLog, - "Ignore reloading tunnel settings. The WireguardDevice is not set yet.") + private func doReloadTunnelSettings(completionHandler: @escaping (Result<(), PacketTunnelProviderError>) -> Void) { + guard let device = self.wireguardDevice else { + os_log(.default, log: tunnelProviderLog, "Ignore reloading tunnel settings. The WireguardDevice is not set yet.") - return Result.Publisher(()).eraseToAnyPublisher() - } + completionHandler(.success(())) + return + } - os_log(.default, log: tunnelProviderLog, "Reload tunnel settings") + os_log(.default, log: tunnelProviderLog, "Reload tunnel settings") - return self.makePacketTunnelConfigAndApplyNetworkSettings() - .flatMap { (packetTunnelConfig) in - wireguardDevice - .setConfig(configuration: packetTunnelConfig.wireguardConfig) - .mapError { PacketTunnelProviderError.updateWireguardConfiguration($0) } + makePacketTunnelConfig { (result) in + guard case .success(let packetTunnelConfig) = result else { + completionHandler(result.map { _ in () }) + return } - .receive(on: self.executionQueue) - .handleEvents(receiveSubscription: { _ in - // Tell the system that the tunnel is about to reconnect with the new endpoint - self.reasserting = true - }, receiveCompletion: { (completion) in - switch completion { - case .finished: - os_log(.default, log: tunnelProviderLog, "Reloaded the tunnel with new settings") - case .failure(let error): - os_log(.default, log: tunnelProviderLog, - "Failed to reload the tunnel with new settings: %{public}s", - error.localizedDescription) - } + // Tell the system that the tunnel is about to reconnect with the new endpoint + self.reasserting = true + let finishReconnecting = { (result: Result<(), PacketTunnelProviderError>) in // Tell the system that the tunnel has finished reconnecting self.reasserting = false - }, receiveCancel: { - // Tell the system that the tunnel has finished reconnecting - // in the event of task cancellation - self.reasserting = false - }).eraseToAnyPublisher() - }.eraseToAnyPublisher() + + completionHandler(result) + } + + self.updateNetworkSettings(packetTunnelConfig: packetTunnelConfig) { (result) in + guard case .success = result else { + finishReconnecting(result) + return + } + + device.setConfiguration(packetTunnelConfig.wireguardConfig) { (result) in + self.dispatchQueue.async { + finishReconnecting(result.mapError { PacketTunnelProviderError.updateWireguardConfiguration($0) }) + } + } + } + } + } + + // MARK: - Private + + private func configureLogger() { + WireguardDevice.setLogger { (level, message) in + os_log(level.osLogType, log: wireguardLog, "%{public}s", message) + } } private func setTunnelConnectionInfo(selectorResult: RelaySelectorResult) { @@ -301,143 +361,135 @@ class PacketTunnelProvider: NEPacketTunnelProvider { location: selectorResult.location ) - os_log(.default, log: tunnelProviderLog, "Select relay: %{public}s", + os_log(.default, log: tunnelProviderLog, "Tunnel connection info: %{public}s", selectorResult.relay.hostname) } - /// Make and return `PacketTunnelConfig` after applying network settings and setting the - /// tunnel connection info - private func makePacketTunnelConfigAndApplyNetworkSettings() - -> AnyPublisher<PacketTunnelConfiguration, PacketTunnelProviderError> { - self.makePacketTunnelConfig() - .receive(on: executionQueue) - .flatMap { (packetTunnelConfig) -> AnyPublisher<PacketTunnelConfiguration, PacketTunnelProviderError> in - self.setTunnelConnectionInfo(selectorResult: packetTunnelConfig.selectorResult) - - return self.applyNetworkSettings(packetTunnelConfig: packetTunnelConfig) - .map { packetTunnelConfig } - .eraseToAnyPublisher() - }.eraseToAnyPublisher() - } + private func makePacketTunnelConfig(completionHandler: @escaping (Result<PacketTunnelConfiguration, PacketTunnelProviderError>) -> Void) { + guard let keychainReference = protocolConfiguration.passwordReference else { + completionHandler(.failure(.missingKeychainConfigurationReference)) + return + } - /// Returns a `PacketTunnelConfig` that contains the tunnel configuration and selected relay - private func makePacketTunnelConfig() -> AnyPublisher<PacketTunnelConfiguration, PacketTunnelProviderError> { - return getConfigurationPersistentKeychainReference() - .publisher - .flatMap { (persistentKeychainReference) in - Self.readTunnelConfiguration(keychainReference: persistentKeychainReference) - .publisher - .flatMap { (tunnelConfiguration) in - Self.selectRelayEndpoint(relayConstraints: tunnelConfiguration.relayConstraints) - .map { (selectorResult) -> PacketTunnelConfiguration in - PacketTunnelConfiguration( - persistentKeychainReference: persistentKeychainReference, - tunnelConfig: tunnelConfiguration, - selectorResult: selectorResult) - } + Self.makePacketTunnelConfig(keychainReference: keychainReference) { (result) in + self.dispatchQueue.async { + guard case .success(let packetTunnelConfig) = result else { + completionHandler(result) + return } - }.eraseToAnyPublisher() + + self.setTunnelConnectionInfo(selectorResult: packetTunnelConfig.selectorResult) + + completionHandler(result) + } + } } - /// Set system network settings using `PacketTunnelConfig` - private func applyNetworkSettings(packetTunnelConfig: PacketTunnelConfiguration) -> AnyPublisher<(), PacketTunnelProviderError> { + private func updateNetworkSettings(packetTunnelConfig: PacketTunnelConfiguration, completionHandler: @escaping (Result<(), PacketTunnelProviderError>) -> Void) { let settingsGenerator = PacketTunnelSettingsGenerator( mullvadEndpoint: packetTunnelConfig.selectorResult.endpoint, - tunnelConfiguration: packetTunnelConfig.tunnelConfig + tunnelSettings: packetTunnelConfig.tunnelSettings ) - os_log(.default, log: tunnelProviderLog, "Set tunnel network settings") + os_log(.default, log: tunnelProviderLog, "Updating network settings...") - return self.setTunnelNetworkSettings(settingsGenerator.networkSettings()) - .mapError { (error) in - os_log(.error, log: tunnelProviderLog, "Cannot set network settings: %{public}s", error.localizedDescription) + setTunnelNetworkSettings(settingsGenerator.networkSettings()) { (error) in + self.dispatchQueue.async { + if let error = error { + os_log(.error, log: tunnelProviderLog, + "Cannot update network settings: %{public}s", + error.localizedDescription) - return PacketTunnelProviderError.setNetworkSettings(error) - } - .receive(on: self.executionQueue) - .eraseToAnyPublisher() - } + completionHandler(.failure(.setNetworkSettings(error))) + } else { + os_log(.default, log: tunnelProviderLog, "Updated network settings") - /// Returns the persistent keychain reference for the VPN configuration or an error if it's - /// missing - private func getConfigurationPersistentKeychainReference() -> Result<Data, PacketTunnelProviderError> { - return protocolConfiguration.passwordReference.map { .success($0) } - ?? .failure(.missingKeychainConfigurationReference) + completionHandler(.success(())) + } + } + } } - private func startKeyRotation(persistentKeychainReference: Data) { - let keyRotationManager = AutomaticKeyRotationManager( - persistentKeychainReference: persistentKeychainReference - ) - - keyRotationManager.eventHandler = { (keyRotationEvent) in - self.reloadTunnel().autoDisposableSink( - cancellableSet: self.cancellableSet, - receiveCompletion: { (completion) in - // no-op - }) + private func reloadTunnelSettings(completionHandler: @escaping (Result<(), PacketTunnelProviderError>) -> Void) { + let operation = ResultOperation<(), PacketTunnelProviderError> { (finish) in + self.doReloadTunnelSettings { (result) in + finish(result) + } } - stopKeyRotation() - self.keyRotationManager = keyRotationManager + operation.addDidFinishBlockObserver { (operation, result) in + self.dispatchQueue.async { + completionHandler(result) + } + } - keyRotationManager.startAutomaticRotation() + exclusivityController.addOperation(operation, categories: [.exclusive]) } + /// Returns a `PacketTunnelConfig` that contains the tunnel settings and selected relay + private class func makePacketTunnelConfig(keychainReference: Data, completionHandler: @escaping (Result<PacketTunnelConfiguration, PacketTunnelProviderError>) -> Void) { + switch Self.readTunnelSettings(keychainReference: keychainReference) { + case .success(let tunnelSettings): + Self.selectRelayEndpoint(relayConstraints: tunnelSettings.relayConstraints) { (result) in + let result = result.map { (selectorResult) -> PacketTunnelConfiguration in + return PacketTunnelConfiguration( + persistentKeychainReference: keychainReference, + tunnelSettings: tunnelSettings, + selectorResult: selectorResult + ) + } + completionHandler(result) + } - private func stopKeyRotation() { - keyRotationManager?.stopAutomaticRotation() - keyRotationManager = nil + case .failure(let error): + completionHandler(.failure(error)) + } } - /// Read tunnel configuration from Keychain - private class func readTunnelConfiguration(keychainReference: Data) -> Result<TunnelConfiguration, PacketTunnelProviderError> { - TunnelConfigurationManager.load(searchTerm: .persistentReference(keychainReference)) - .mapError { PacketTunnelProviderError.cannotReadTunnelConfiguration($0) } - .map { $0.tunnelConfiguration } + /// Read tunnel settings from Keychain + private class func readTunnelSettings(keychainReference: Data) -> Result<TunnelSettings, PacketTunnelProviderError> { + TunnelSettingsManager.load(searchTerm: .persistentReference(keychainReference)) + .mapError { PacketTunnelProviderError.cannotReadTunnelSettings($0) } + .map { $0.tunnelSettings } } /// Load relay cache with potential networking to refresh the cache and pick the relay for the /// given relay constraints. - private class func selectRelayEndpoint(relayConstraints: RelayConstraints) -> AnyPublisher<RelaySelectorResult, PacketTunnelProviderError> { - return RelaySelector.loadedFromRelayCache() - .mapError { PacketTunnelProviderError.readRelayCache($0) } - .flatMap { (relaySelector) -> Result<RelaySelectorResult, PacketTunnelProviderError>.Publisher in - return relaySelector - .evaluate(with: relayConstraints) - .flatMap { .init($0) } ?? .init(.noRelaySatisfyingConstraint) - }.eraseToAnyPublisher() - } + private class func selectRelayEndpoint(relayConstraints: RelayConstraints, completionHandler: @escaping (Result<RelaySelectorResult, PacketTunnelProviderError>) -> Void) { + RelayCache.shared.read { (result) in + switch result { + case .success(let cachedRelayList): + let relaySelector = RelaySelector(relayList: cachedRelayList.relayList) - private class func startWireguard(packetFlow: NEPacketTunnelFlow, configuration: WireguardConfiguration) -> AnyPublisher<WireguardDevice, PacketTunnelProviderError> { - WireguardDevice.fromPacketFlow(packetFlow) - .publisher - .flatMap { (device) -> AnyPublisher<WireguardDevice, WireguardDevice.Error> in - os_log(.default, log: tunnelProviderLog, - "Tunnel interface is %{public}s", - device.getInterfaceName() ?? "unknown") + if let selectorResult = relaySelector.evaluate(with: relayConstraints) { + completionHandler(.success(selectorResult)) + } else { + completionHandler(.failure(.noRelaySatisfyingConstraint)) + } - return device.start(configuration: configuration) - .map { device } - .eraseToAnyPublisher() + case .failure(let error): + completionHandler(.failure(.readRelayCache(error))) + } } - .mapError { PacketTunnelProviderError.startWireguardDevice($0) } - .eraseToAnyPublisher() } -} -extension NETunnelProvider { + private class func startWireguardDevice(packetFlow: NEPacketTunnelFlow, configuration: WireguardConfiguration, completionHandler: @escaping (Result<WireguardDevice, PacketTunnelProviderError>) -> Void) { + let result = WireguardDevice.fromPacketFlow(packetFlow) - func setTunnelNetworkSettings(_ tunnelNetworkSettings: NETunnelNetworkSettings?) -> Future<(), Error> { - return Future { (fulfill) in - self.setTunnelNetworkSettings(tunnelNetworkSettings) { (error) in - if let error = error { - fulfill(.failure(error)) - } else { - fulfill(.success(())) - } - } + guard case .success(let device) = result else { + completionHandler(result.mapError { PacketTunnelProviderError.startWireguardDevice($0) }) + return } - } + let tunnelDeviceName = device.getInterfaceName() ?? "unknown" + + os_log(.default, log: tunnelProviderLog, "Tunnel interface is %{public}s", tunnelDeviceName) + + device.start(configuration: configuration) { (result) in + let result = result.map { device } + .mapError { PacketTunnelProviderError.startWireguardDevice($0) } + + completionHandler(result) + } + } } diff --git a/ios/PacketTunnel/PacketTunnelSettingsGenerator.swift b/ios/PacketTunnel/PacketTunnelSettingsGenerator.swift index 82fca24762..b91c7bc326 100644 --- a/ios/PacketTunnel/PacketTunnelSettingsGenerator.swift +++ b/ios/PacketTunnel/PacketTunnelSettingsGenerator.swift @@ -13,7 +13,7 @@ import os struct PacketTunnelSettingsGenerator { let mullvadEndpoint: MullvadEndpoint - let tunnelConfiguration: TunnelConfiguration + let tunnelSettings: TunnelSettings func networkSettings() -> NEPacketTunnelNetworkSettings { let tunnelRemoteAddress = "\(mullvadEndpoint.ipv4Relay.ip)" @@ -40,7 +40,7 @@ struct PacketTunnelSettingsGenerator { } private func ipv4Settings() -> NEIPv4Settings { - let interfaceAddresses = tunnelConfiguration.interface.addresses + let interfaceAddresses = tunnelSettings.interface.addresses let ipv4AddressRanges = interfaceAddresses.filter { $0.address is IPv4Address } let ipv4Settings = NEIPv4Settings( @@ -55,7 +55,7 @@ struct PacketTunnelSettingsGenerator { } private func ipv6Settings() -> NEIPv6Settings { - let interfaceAddresses = tunnelConfiguration.interface.addresses + let interfaceAddresses = tunnelSettings.interface.addresses let ipv6AddressRanges = interfaceAddresses.filter { $0.address is IPv6Address } let addresses = ipv6AddressRanges.map { "\($0.address)" } diff --git a/ios/PacketTunnel/WireguardDevice.swift b/ios/PacketTunnel/WireguardDevice.swift index f0c3dd033e..86af84473c 100644 --- a/ios/PacketTunnel/WireguardDevice.swift +++ b/ios/PacketTunnel/WireguardDevice.swift @@ -6,7 +6,6 @@ // Copyright © 2019 Mullvad VPN AB. All rights reserved. // -import Combine import Foundation import NetworkExtension import os @@ -20,7 +19,7 @@ class WireguardDevice { typealias WireguardLogHandler = (WireguardLogLevel, String) -> Void /// An error type describing the errors returned by `WireguardDevice` - enum Error: Swift.Error { + enum Error: ChainedError { /// A failure to obtain the tunnel device file descriptor case cannotLocateSocketDescriptor @@ -36,7 +35,7 @@ class WireguardDevice { /// A failure to resolve an endpoint case resolveEndpoint(AnyIPEndpoint, Swift.Error) - var localizedDescription: String { + var errorDescription: String? { switch self { case .cannotLocateSocketDescriptor: return "Unable to locate the file descriptor for socket." @@ -46,8 +45,8 @@ class WireguardDevice { return "Wireguard has not been started yet" case .alreadyStarted: return "Wireguard has already been started" - case .resolveEndpoint(let endpoint, let error): - return "Failed to resolve the endpoint: \(endpoint). Error: \(error.localizedDescription)" + case .resolveEndpoint(let endpoint, _): + return "Failed to resolve the endpoint: \(endpoint)" } } } @@ -85,9 +84,6 @@ class WireguardDevice { /// Active configuration private var configuration: WireguardConfiguration? - /// Active configuration with resolved endpoints - private var resolvedConfiguration: WireguardConfiguration? - /// Returns a Wireguard version class var version: String { String(cString: wgVersion()) @@ -135,26 +131,61 @@ class WireguardDevice { // MARK: - Public methods - func start(configuration: WireguardConfiguration) -> Future<(), Error> { - return Future { (fulfill) in - self.workQueue.async { - fulfill(self._start(configuration: configuration)) + func start(configuration: WireguardConfiguration, completionHandler: @escaping (Result<(), Error>) -> Void) { + workQueue.async { + guard self.wireguardHandle == nil else { + completionHandler(.failure(.alreadyStarted)) + return + } + + let resolvedConfiguration = Self.resolveConfiguration(configuration) + let handle = resolvedConfiguration + .uapiConfiguration() + .toRawWireguardConfigString() + .withCString { wgTurnOn($0, self.tunFd) } + + if handle >= 0 { + self.wireguardHandle = handle + self.configuration = configuration + + self.startNetworkMonitor() + + completionHandler(.success(())) + } else { + completionHandler(.failure(.start(handle))) } } } - func stop() -> Future<(), Error> { - return Future { (fulfill) in - self.workQueue.async { - fulfill(self._stop()) + func stop(completionHandler: @escaping (Result<(), Error>) -> Void) { + workQueue.async { + if let handle = self.wireguardHandle { + self.networkMonitor?.cancel() + self.networkMonitor = nil + + wgTurnOff(handle) + self.wireguardHandle = nil + + completionHandler(.success(())) + } else { + completionHandler(.failure(.notStarted)) } } } - func setConfig(configuration: WireguardConfiguration) -> Future<(), Error> { - return Future { (fulfill) in - self.workQueue.async { - fulfill(self._setConfig(configuration: configuration)) + func setConfiguration(_ newConfiguration: WireguardConfiguration, completionHandler: @escaping (Result<(), Error>) -> Void) { + workQueue.async { + if let handle = self.wireguardHandle { + let resolvedConfiguration = Self.resolveConfiguration(newConfiguration) + let commands = resolvedConfiguration.uapiConfiguration() + + Self.setWireguardConfig(handle: handle, commands: commands) + + self.configuration = newConfiguration + + completionHandler(.success(())) + } else { + completionHandler(.failure(.notStarted)) } } } @@ -183,60 +214,6 @@ class WireguardDevice { // MARK: - Private methods - private func _start(configuration: WireguardConfiguration) -> Result<(), Error> { - guard wireguardHandle == nil else { - return .failure(.alreadyStarted) - } - - let resolvedConfiguration = Self.resolveConfiguration(configuration) - let handle = resolvedConfiguration - .uapiConfiguration() - .toRawWireguardConfigString() - .withCString { wgTurnOn($0, self.tunFd) } - - if handle < 0 { - return .failure(.start(handle)) - } else { - self.wireguardHandle = handle - self.configuration = configuration - self.resolvedConfiguration = resolvedConfiguration - - startNetworkMonitor() - - return .success(()) - } - } - - private func _stop() -> Result<(), Error> { - if let handle = wireguardHandle { - networkMonitor?.cancel() - networkMonitor = nil - - wgTurnOff(handle) - wireguardHandle = nil - - return .success(()) - } else { - return .failure(.notStarted) - } - } - - private func _setConfig(configuration newConfiguration: WireguardConfiguration) -> Result<(), Error> { - if let handle = wireguardHandle { - let newResolvedConfiguration = Self.resolveConfiguration(newConfiguration) - let commands = newResolvedConfiguration.uapiConfiguration() - - Self.setWireguardConfig(handle: handle, commands: commands) - - self.configuration = newConfiguration - self.resolvedConfiguration = newResolvedConfiguration - - return .success(()) - } else { - return .failure(.notStarted) - } - } - private class func setWireguardConfig(handle: Int32, commands: [WireguardCommand]) { // Ignore empty payloads guard !commands.isEmpty else { return } @@ -318,11 +295,10 @@ class WireguardDevice { // Re-resolve endpoints on network changes if let currentConfiguration = self.configuration { - let newResolvedConfiguration = Self.resolveConfiguration(currentConfiguration) - let commands = newResolvedConfiguration.endpointUapiConfiguration() + let resolvedConfiguration = Self.resolveConfiguration(currentConfiguration) + let commands = resolvedConfiguration.endpointUapiConfiguration() Self.setWireguardConfig(handle: handle, commands: commands) - self.resolvedConfiguration = newResolvedConfiguration } // Tell Wireguard to re-open sockets and bind them to the new network interface |
