summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2020-07-15 14:40:39 +0200
committerAndrej Mihajlov <and@mullvad.net>2020-07-15 14:40:39 +0200
commite906abb2c3847e0fa3cfa54d9335f56fc9c8f8c7 (patch)
tree054c1d7feaf6309ef073362f60e870b33b639072
parent8b7b7175ea5c93374daf60abfda8b560be374048 (diff)
parent8b32eb69d489adf7a4dc06ab4fa3494bef726029 (diff)
downloadmullvadvpn-e906abb2c3847e0fa3cfa54d9335f56fc9c8f8c7.tar.xz
mullvadvpn-e906abb2c3847e0fa3cfa54d9335f56fc9c8f8c7.zip
Merge branch 'uncombine-tunnel-manager'
-rw-r--r--ios/CHANGELOG.md12
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj56
-rw-r--r--ios/MullvadVPN/Account.swift262
-rw-r--r--ios/MullvadVPN/AlertPresenter.swift97
-rw-r--r--ios/MullvadVPN/AppDelegate.swift23
-rw-r--r--ios/MullvadVPN/AutomaticKeyRotationManager.swift268
-rw-r--r--ios/MullvadVPN/Base.lproj/Main.storyboard6
-rw-r--r--ios/MullvadVPN/ConnectViewController.swift96
-rw-r--r--ios/MullvadVPN/Curve25519.swift40
-rw-r--r--ios/MullvadVPN/DisplayChainedError.swift112
-rw-r--r--ios/MullvadVPN/KeychainError.swift1
-rw-r--r--ios/MullvadVPN/LoginState.swift20
-rw-r--r--ios/MullvadVPN/LoginViewController.swift70
-rw-r--r--ios/MullvadVPN/MullvadVPN-Bridging-Header.h1
-rw-r--r--ios/MullvadVPN/PacketTunnelIpc.swift227
-rw-r--r--ios/MullvadVPN/RelaySelector+RelayCache.swift21
-rw-r--r--ios/MullvadVPN/SettingsNavigationController.swift19
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProviderHost.swift38
-rw-r--r--ios/MullvadVPN/TunnelControlViewController.swift24
-rw-r--r--ios/MullvadVPN/TunnelManager.swift1475
-rw-r--r--ios/MullvadVPN/TunnelSettings.swift (renamed from ios/MullvadVPN/TunnelConfiguration.swift)8
-rw-r--r--ios/MullvadVPN/TunnelSettingsManager.swift (renamed from ios/MullvadVPN/TunnelConfigurationManager.swift)36
-rw-r--r--ios/MullvadVPN/WireguardKeysViewController.swift171
-rw-r--r--ios/MullvadVPN/WireguardPrivateKey.swift30
-rw-r--r--ios/MullvadVPN/x25519.c178
-rw-r--r--ios/MullvadVPN/x25519.h7
-rw-r--r--ios/PacketTunnel/PacketTunnel-Bridging-Header.h1
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider.swift548
-rw-r--r--ios/PacketTunnel/PacketTunnelSettingsGenerator.swift6
-rw-r--r--ios/PacketTunnel/WireguardDevice.swift130
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