diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2021-05-27 14:37:39 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2021-05-27 14:37:39 +0200 |
| commit | 1dc96b8ac54f3ff99ae64c32cd79a68fbd08c146 (patch) | |
| tree | b9dd17fc97d3c83d30b4bf9ec37c70fbc3eab150 /ios | |
| parent | 23ba82a76b74c2ca11c7b67e40b0c200601ec987 (diff) | |
| parent | dc352b54a2fcf05a36204efaf18133331ae32aee (diff) | |
| download | mullvadvpn-1dc96b8ac54f3ff99ae64c32cd79a68fbd08c146.tar.xz mullvadvpn-1dc96b8ac54f3ff99ae64c32cd79a68fbd08c146.zip | |
Merge branch 'adblocking-dns'
Diffstat (limited to 'ios')
| -rw-r--r-- | ios/CHANGELOG.md | 1 | ||||
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 48 | ||||
| -rw-r--r-- | ios/MullvadVPN/AppDelegate.swift | 72 | ||||
| -rw-r--r-- | ios/MullvadVPN/ConnectViewController.swift | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN/CustomSwitch.swift | 72 | ||||
| -rw-r--r-- | ios/MullvadVPN/CustomSwitchContainer.swift | 66 | ||||
| -rw-r--r-- | ios/MullvadVPN/EmptyTableViewHeaderFooterView.swift | 26 | ||||
| -rw-r--r-- | ios/MullvadVPN/PreferencesViewController.swift | 123 | ||||
| -rw-r--r-- | ios/MullvadVPN/PrivateKeyWithMetadata.swift | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayConstraints.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsNavigationController.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsSwitchCell.swift | 36 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsViewController.swift | 25 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelManager.swift | 193 | ||||
| -rw-r--r-- | ios/MullvadVPN/TunnelSettings.swift | 50 | ||||
| -rw-r--r-- | ios/MullvadVPN/UIColor+Palette.swift | 6 | ||||
| -rw-r--r-- | ios/MullvadVPN/WireguardKeysViewController.swift | 20 | ||||
| -rw-r--r-- | ios/PacketTunnel/PacketTunnelProvider.swift | 17 |
18 files changed, 566 insertions, 201 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index f27785e79f..81fe049d90 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -30,6 +30,7 @@ Line wrap the file at 100 chars. Th - Reduce network traffic consumption by leveraging HTTP caching via ETag HTTP header to avoid re-downloading the relay list if it hasn't changed. - Pin root SSL certificates. +- Add option to use Mullvad's ad-blocking DNS servers. ### Fixed - Fix bug which caused the tunnel manager to become unresponsive in the rare event of failure to diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index ac4e13422f..5a3cffd849 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -141,6 +141,7 @@ 58907D9524D17B4E00CFC3F5 /* DisconnectSplitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */; }; 5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */; }; 5891BF5125E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */; }; + 5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */; }; 5896AE7E246ACE65005B36CB /* KeychainAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDEB245059F000CB0F5B /* KeychainAttributes.swift */; }; 5896AE7F246ACE76005B36CB /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEDF6245088E100CB0F5B /* Keychain.swift */; }; 5896AE80246ACE79005B36CB /* KeychainClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FAEE0024533A9C00CB0F5B /* KeychainClass.swift */; }; @@ -153,6 +154,10 @@ 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */; }; 58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; }; 58A99ED3240014A0006599E9 /* ConsentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A99ED2240014A0006599E9 /* ConsentViewController.swift */; }; + 58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */; }; + 58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */; }; + 58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */; }; + 58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */; }; 58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; }; 58AEEF662344A37400C9BBD5 /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF642344A36000C9BBD5 /* KeychainError.swift */; }; 58AEEF6B2344A46200C9BBD5 /* TunnelSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */; }; @@ -362,6 +367,7 @@ 58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisconnectSplitButton.swift; sourceTree = "<group>"; }; 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+ProductVersion.swift"; sourceTree = "<group>"; }; 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+KeyboardNavigation.swift"; sourceTree = "<group>"; }; + 5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyTableViewHeaderFooterView.swift; sourceTree = "<group>"; }; 5894E725236B2801008A2793 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDateComponentsFormatting.swift; sourceTree = "<group>"; }; 5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDateComponentsFormattingTests.swift; sourceTree = "<group>"; }; @@ -369,6 +375,10 @@ 58A1AA8623F43901009F7EA6 /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = "<group>"; }; 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionPanelView.swift; sourceTree = "<group>"; }; 58A99ED2240014A0006599E9 /* ConsentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentViewController.swift; sourceTree = "<group>"; }; + 58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewController.swift; sourceTree = "<group>"; }; + 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSwitchCell.swift; sourceTree = "<group>"; }; + 58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSwitch.swift; sourceTree = "<group>"; }; + 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSwitchContainer.swift; sourceTree = "<group>"; }; 58AEEF642344A36000C9BBD5 /* KeychainError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainError.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; }; @@ -588,22 +598,29 @@ 58DF28A42417CB4B00E836B0 /* AppStorePaymentManager.swift */, 58FD5BE624192A2B00112C88 /* AppStoreReceipt.swift */, 58CE5E6A224146210008646E /* Assets.xcassets */, + 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */, 588534BD246193C00018B744 /* AutomaticKeyRotationManager.swift */, 589AB4F6227B64450039131E /* BasicTableViewCell.swift */, + 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */, 58F840B12464491D0044E708 /* ChainedError.swift */, 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */, - 58CCA00F224249A1004F3011 /* ConnectViewController.swift */, 58B43C1825F77DB60002C8C3 /* ConnectMainContentView.swift */, - 58A99ED2240014A0006599E9 /* ConsentViewController.swift */, + 58CCA00F224249A1004F3011 /* ConnectViewController.swift */, 584592602639B4A200EF967F /* ConsentContentView.swift */, + 58A99ED2240014A0006599E9 /* ConsentViewController.swift */, + 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */, 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */, 582BB1B0229569620055B6EF /* CustomNavigationBar.swift */, 58293FB625138B88005D0BB5 /* CustomNavigationController.swift */, + 5868BD32261DCD2600E6027F /* CustomSplitViewController.swift */, + 58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */, + 58ACF64E26567A7100ACE4B7 /* CustomSwitchContainer.swift */, 58293FB025124117005D0BB5 /* CustomTextField.swift */, 58293FB2251241B3005D0BB5 /* CustomTextView.swift */, 58907D9424D17B4E00CFC3F5 /* DisconnectSplitButton.swift */, 58B9EB142489139B00095626 /* DisplayChainedError.swift */, 5873884C239E6D7E00E96C4E /* EmbeddedViewContainerView.swift */, + 5892A45D265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift */, 58FEEB45260A028D00A621A8 /* GeoJSON.swift */, 58F3C0A3249CB069003E76BE /* HeaderBarView.swift */, 58FD5BF32428C67600112C88 /* InAppPurchaseButton.swift */, @@ -619,8 +636,10 @@ 58FAEDFE24533A7000CB0F5B /* KeychainReturn.swift */, 58CE5E6C224146210008646E /* LaunchScreen.storyboard */, 58A1AA8623F43901009F7EA6 /* Location.swift */, + 583DA21325FA4B5C00318683 /* LocationDataSource.swift */, 58BA692D23E99EFF009DC256 /* Locking.swift */, 5815039F24D6ECF200C9C50E /* Logging */, + 58B993B02608A34500BA7811 /* LoginContentView.swift */, 58CE5E65224146200008646E /* LoginViewController.swift */, 58C3B06624EA768100C0348E /* LogStreamerViewController.swift */, 58CE5E67224146200008646E /* Main.storyboard */, @@ -634,10 +653,11 @@ 580EE1FF24B3218800F9D8A1 /* Operations */, 583BC70624FE4DC400C9DE04 /* Optional+DispatchQueue.swift */, 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */, + 58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */, 58C6B35322BB87C4003C19AD /* PrivateKeyWithMetadata.swift */, - 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */, - 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */, 58F8AC0D25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift */, + 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */, + 58293FAC2510CA58005D0BB5 /* ProblemReportViewController.swift */, 58BFA5C522A7C97F00A6173D /* RelayCache.swift */, 58781CC822AE7CA8009B9D8E /* RelayConstraints.swift */, 58781CD422AFBA39009B9D8E /* RelaySelector.swift */, @@ -650,34 +670,29 @@ 582BB1B2229574F40055B6EF /* SettingsAccountCell.swift */, 582BB1AE229566420055B6EF /* SettingsCell.swift */, 58E6771E24ADFE7800AA26E7 /* SettingsNavigationController.swift */, + 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */, 58CCA01122424D11004F3011 /* SettingsViewController.swift */, 58BA693023EADA6A009DC256 /* SimulatorTunnelProvider.swift */, 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */, 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */, 58F19E34228C15BA00C7710B /* SpinnerActivityIndicatorView.swift */, + 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */, 581CBCED229826FD00727D7F /* StaticTableViewDataSource.swift */, + 58EF581025D69DB400AEBA94 /* StatusImageView.swift */, 5807E2BF2432038B00F5FF30 /* String+Split.swift */, 5871FB8225498CA20051A0A4 /* Swizzle.swift */, 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */, 5835B7CB233B76CB0096D79F /* TunnelManager.swift */, 587AD7C523421D7000E93A53 /* TunnelSettings.swift */, 58AEEF6A2344A46200C9BBD5 /* TunnelSettingsManager.swift */, + 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */, 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */, 58CCA0152242560B004F3011 /* UIColor+Palette.swift */, + 585CA70E25F8C44600B47C62 /* UIMetrics.swift */, 58FD5BF12424F7D700112C88 /* UserInterfaceInteractionRestriction.swift */, 58B8743122B25A7600015324 /* WireguardAssociatedAddresses.swift */, 5877152F23981F7B001F8237 /* WireguardKeysViewController.swift */, 58B9814D24FEA70D00C0D59E /* WireguardKeysViewController.xib */, - 58EF580A25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift */, - 58EF581025D69DB400AEBA94 /* StatusImageView.swift */, - 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */, - 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */, - 585CA70E25F8C44600B47C62 /* UIMetrics.swift */, - 583DA21325FA4B5C00318683 /* LocationDataSource.swift */, - 58B993B02608A34500BA7811 /* LoginContentView.swift */, - 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */, - 5868BD32261DCD2600E6027F /* CustomSplitViewController.swift */, - 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */, ); path = MullvadVPN; sourceTree = "<group>"; @@ -1007,8 +1022,10 @@ 58BFA5C622A7C97F00A6173D /* RelayCache.swift in Sources */, 582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */, 584789E026529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */, + 58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */, 588D2FE3248AC27F00E313F7 /* AsyncOperation.swift in Sources */, 5877153023981F7B001F8237 /* WireguardKeysViewController.swift in Sources */, + 58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */, 5850367F25A481D800A43E93 /* IPAddressRange+Codable.swift in Sources */, 5871FB96254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift in Sources */, 58FEEB58260B662E00A621A8 /* AutomaticKeyboardResponder.swift in Sources */, @@ -1064,12 +1081,14 @@ 580EE21B24B3236900F9D8A1 /* InputOperation.swift in Sources */, 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */, 58FD5BE724192A2C00112C88 /* AppStoreReceipt.swift in Sources */, + 5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */, 5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */, 58FEEB46260A028D00A621A8 /* GeoJSON.swift in Sources */, 5815039724D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */, 5815039D24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */, 581CBCEE229826FD00727D7F /* StaticTableViewDataSource.swift in Sources */, 58CE5E64224146200008646E /* AppDelegate.swift in Sources */, + 58ACF64F26567A7100ACE4B7 /* CustomSwitchContainer.swift in Sources */, 5857F24324C8662600CF6F47 /* SelectLocationHeaderView.swift in Sources */, 58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */, 58C3B06924EAA25000C0348E /* StringStreamIterator.swift in Sources */, @@ -1105,6 +1124,7 @@ 58C4CB0124EBE5A700A22D49 /* LogEntryParser.swift in Sources */, 58F840B22464491D0044E708 /* ChainedError.swift in Sources */, 58FAEDFF24533A7000CB0F5B /* KeychainReturn.swift in Sources */, + 58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */, 580EE20C24B3225F00F9D8A1 /* DelayOperation.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index c5bd912b40..50a57c4d97 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -94,28 +94,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { switch result { case .success: self.logger?.debug("Loaded tunnels") - - // Fetch relay constraints when logged in. - if Account.shared.isLoggedIn { - self.logger?.debug("Load relay constraints") - - TunnelManager.shared.getRelayConstraints { (result) in - DispatchQueue.main.async { - switch result { - case .success(let relayConstraints): - self.relayConstraints = relayConstraints - self.logger?.debug("Loaded relay constraints: \(relayConstraints)") - - case .failure(let error): - self.logger?.error(chainedError: error, message: "Failed to load relay constraints") - } - - self.didFinishInitialization() - } - } - } else { - self.didFinishInitialization() - } + self.relayConstraints = TunnelManager.shared.tunnelSettings?.relayConstraints + self.didFinishInitialization() case .failure(let error): self.logger?.error(chainedError: error, message: "Failed to load tunnels") @@ -425,40 +405,28 @@ extension AppDelegate: LoginViewControllerDelegate { // Move the settings button back into header bar self.rootContainer?.removeSettingsButtonFromPresentationContainer() - TunnelManager.shared.getRelayConstraints { [weak self] (result) in - guard let self = self else { return } - - DispatchQueue.main.async { - switch result { - case .success(let relayConstraints): - self.relayConstraints = relayConstraints - self.selectLocationViewController?.setSelectedRelayLocation(relayConstraints.location.value, animated: false, scrollPosition: .middle) - - case .failure(let error): - self.logger?.error(chainedError: error, message: "Failed to load relay constraints after log in") - } - - switch UIDevice.current.userInterfaceIdiom { - case .phone: - let connectController = self.makeConnectViewController() - self.rootContainer?.pushViewController(connectController, animated: true) { - self.showAccountSettingsControllerIfAccountExpired() - } - self.connectController = connectController - case .pad: - self.showSplitViewMaster(true, animated: true) + self.relayConstraints = TunnelManager.shared.tunnelSettings?.relayConstraints + self.selectLocationViewController?.setSelectedRelayLocation(relayConstraints?.location.value, animated: false, scrollPosition: .middle) - controller.dismiss(animated: true) { - self.showAccountSettingsControllerIfAccountExpired() - } - default: - fatalError() - } + switch UIDevice.current.userInterfaceIdiom { + case .phone: + let connectController = self.makeConnectViewController() + self.rootContainer?.pushViewController(connectController, animated: true) { + self.showAccountSettingsControllerIfAccountExpired() + } + self.connectController = connectController + case .pad: + self.showSplitViewMaster(true, animated: true) - self.window?.isUserInteractionEnabled = true - self.rootContainer?.setEnableSettingsButton(true) + controller.dismiss(animated: true) { + self.showAccountSettingsControllerIfAccountExpired() } + default: + fatalError() } + + self.window?.isUserInteractionEnabled = true + self.rootContainer?.setEnableSettingsButton(true) } } diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift index b2f2260dbf..6c641f1e65 100644 --- a/ios/MullvadVPN/ConnectViewController.swift +++ b/ios/MullvadVPN/ConnectViewController.swift @@ -148,7 +148,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen } } - func tunnelPublicKeyDidChange(publicKeyWithMetadata: PublicKeyWithMetadata?) { + func tunnelSettingsDidChange(tunnelSettings: TunnelSettings?) { // no-op } diff --git a/ios/MullvadVPN/CustomSwitch.swift b/ios/MullvadVPN/CustomSwitch.swift new file mode 100644 index 0000000000..b804dae7ae --- /dev/null +++ b/ios/MullvadVPN/CustomSwitch.swift @@ -0,0 +1,72 @@ +// +// CustomSwitch.swift +// MullvadVPN +// +// Created by pronebird on 20/05/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +class CustomSwitch: UISwitch { + + /// Returns the private `UISwitch` background view + private var backgroundView: UIView? { + // Go two levels deep only + let subviewsToExamine = subviews.flatMap { (view) -> [UIView] in + return [view] + view.subviews + } + + // Find the first subview that has background color set. + let backgroundView = subviewsToExamine.first { (subview) in + return subview.backgroundColor != nil + } + + return backgroundView + } + + override init(frame: CGRect) { + super.init(frame: frame) + + self.tintColor = .clear + self.onTintColor = .clear + + updateThumbColor(isOn: self.isOn, animated: false) + + addTarget(self, action: #selector(valueChanged(_:)), for: .valueChanged) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func setOn(_ on: Bool, animated: Bool) { + super.setOn(on, animated: animated) + + updateThumbColor(isOn: on, animated: animated) + } + + private func updateThumbColor(isOn: Bool, animated: Bool) { + let actions = { + self.thumbTintColor = isOn ? UIColor.Switch.onThumbColor : UIColor.Switch.offThumbColor + self.backgroundView?.backgroundColor = .clear + } + + if animated { + UIView.animate(withDuration: 0.25, animations: actions) + } else { + actions() + } + } + + @objc private func valueChanged(_ sender: Any) { + if #available(iOS 13, *) { + self.updateThumbColor(isOn: self.isOn, animated: true) + } else { + // Wait for animations to finish before changing the thumb color to prevent the jumpy behaviour. + CATransaction.setCompletionBlock { + self.updateThumbColor(isOn: self.isOn, animated: false) + } + } + } +} diff --git a/ios/MullvadVPN/CustomSwitchContainer.swift b/ios/MullvadVPN/CustomSwitchContainer.swift new file mode 100644 index 0000000000..ebdcc33b13 --- /dev/null +++ b/ios/MullvadVPN/CustomSwitchContainer.swift @@ -0,0 +1,66 @@ +// +// CustomSwitchContainer.swift +// MullvadVPN +// +// Created by pronebird on 20/05/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +class CustomSwitchContainer: UIView { + static let borderEdgeInsets = UIEdgeInsets(top: 3, left: 3, bottom: 3, right: 3) + + private let borderShape: CAShapeLayer = { + let shapeLayer = CAShapeLayer() + shapeLayer.borderColor = UIColor.Switch.borderColor.cgColor + shapeLayer.borderWidth = 2 + if #available(iOS 13.0, *) { + shapeLayer.cornerCurve = .continuous + } + return shapeLayer + }() + + let control = CustomSwitch() + + override var intrinsicContentSize: CGSize { + return controlSize() + } + + override func sizeThatFits(_ size: CGSize) -> CGSize { + return controlSize() + } + + override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(control) + layer.addSublayer(borderShape) + + control.sizeToFit() + sizeToFit() + + borderShape.cornerRadius = self.bounds.height * 0.5 + borderShape.frame = self.bounds + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + control.frame.origin = CGPoint(x: Self.borderEdgeInsets.left, y: Self.borderEdgeInsets.top) + } + + // MARK: - Private + + private func controlSize() -> CGSize { + var size = control.bounds.size + size.width += Self.borderEdgeInsets.left + Self.borderEdgeInsets.right + size.height += Self.borderEdgeInsets.top + Self.borderEdgeInsets.bottom + return size + } + +} diff --git a/ios/MullvadVPN/EmptyTableViewHeaderFooterView.swift b/ios/MullvadVPN/EmptyTableViewHeaderFooterView.swift new file mode 100644 index 0000000000..0d047ab045 --- /dev/null +++ b/ios/MullvadVPN/EmptyTableViewHeaderFooterView.swift @@ -0,0 +1,26 @@ +// +// EmptyTableViewHeaderFooterView.swift +// MullvadVPN +// +// Created by pronebird on 27/05/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +class EmptyTableViewHeaderFooterView: UITableViewHeaderFooterView { + + static var reuseIdentifier = "EmptyTableViewHeaderFooterView" + + override init(reuseIdentifier: String?) { + super.init(reuseIdentifier: reuseIdentifier) + + self.textLabel?.isHidden = true + self.contentView.backgroundColor = .clear + self.backgroundView?.backgroundColor = .clear + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/ios/MullvadVPN/PreferencesViewController.swift b/ios/MullvadVPN/PreferencesViewController.swift new file mode 100644 index 0000000000..726bba49d3 --- /dev/null +++ b/ios/MullvadVPN/PreferencesViewController.swift @@ -0,0 +1,123 @@ +// +// PreferencesViewController.swift +// MullvadVPN +// +// Created by pronebird on 19/05/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import UIKit +import Logging + +class PreferencesViewController: UITableViewController, TunnelObserver { + + private let logger = Logger(label: "PreferencesViewController") + private var dnsSettings: DNSSettings? + + private enum CellIdentifier: String { + case switchCell + } + + private let staticDataSource = PreferencesTableViewDataSource() + + init() { + super.init(style: .grouped) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.backgroundColor = .secondaryColor + tableView.separatorColor = .secondaryColor + tableView.rowHeight = UITableView.automaticDimension + tableView.estimatedRowHeight = 60 + tableView.sectionHeaderHeight = UIMetrics.contentLayoutMargins.top + tableView.sectionFooterHeight = 0 + + tableView.dataSource = staticDataSource + tableView.delegate = staticDataSource + + tableView.register(SettingsSwitchCell.self, forCellReuseIdentifier: CellIdentifier.switchCell.rawValue) + tableView.register(EmptyTableViewHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: EmptyTableViewHeaderFooterView.reuseIdentifier) + + navigationItem.title = NSLocalizedString("Preferences", comment: "Navigation title") + navigationItem.largeTitleDisplayMode = .always + + TunnelManager.shared.addObserver(self) + self.dnsSettings = TunnelManager.shared.tunnelSettings?.interface.dnsSettings + + setupDataSource() + } + + // MARK: - TunnelObserver + + func tunnelStateDidChange(tunnelState: TunnelState) { + // no-op + } + + func tunnelSettingsDidChange(tunnelSettings: TunnelSettings?) { + DispatchQueue.main.async { + if tunnelSettings?.interface.dnsSettings != self.dnsSettings { + self.dnsSettings = tunnelSettings?.interface.dnsSettings + self.tableView.reloadData() + } + } + } + + // MARK: - Private + + private func setupDataSource() { + let blockAdvertisingRow = StaticTableViewRow(reuseIdentifier: CellIdentifier.switchCell.rawValue) { (indexPath, cell) in + let cell = cell as! SettingsSwitchCell + + cell.titleLabel.text = NSLocalizedString("Block ads", comment: "") + cell.switchControl.setOn(self.dnsSettings?.blockAdvertising ?? false, animated: false) + cell.action = { [weak self] (isOn) in + self?.dnsSettings?.blockAdvertising = isOn + self?.saveDNSSettings() + } + } + blockAdvertisingRow.isSelectable = false + + let blockTrackingRow = StaticTableViewRow(reuseIdentifier: CellIdentifier.switchCell.rawValue) { (indexPath, cell) in + let cell = cell as! SettingsSwitchCell + + cell.titleLabel.text = NSLocalizedString("Block trackers", comment: "") + cell.switchControl.setOn(self.dnsSettings?.blockTracking ?? false, animated: false) + cell.action = { [weak self] (isOn) in + self?.dnsSettings?.blockTracking = isOn + self?.saveDNSSettings() + } + } + blockTrackingRow.isSelectable = false + + let section = StaticTableViewSection() + section.addRows([blockAdvertisingRow, blockTrackingRow]) + staticDataSource.addSections([section]) + } + + private func saveDNSSettings() { + guard let dnsSettings = dnsSettings else { return } + + TunnelManager.shared.setDNSSettings(dnsSettings) { [weak self] (result) in + if case .failure(let error) = result { + self?.logger.error(chainedError: error, message: "Failed to save DNS settings") + } + } + } + +} + +class PreferencesTableViewDataSource: StaticTableViewDataSource { + + // MARK: - UITableViewDelegate + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return tableView.dequeueReusableHeaderFooterView(withIdentifier: EmptyTableViewHeaderFooterView.reuseIdentifier) + } + +} diff --git a/ios/MullvadVPN/PrivateKeyWithMetadata.swift b/ios/MullvadVPN/PrivateKeyWithMetadata.swift index 6714414390..b3e4990aee 100644 --- a/ios/MullvadVPN/PrivateKeyWithMetadata.swift +++ b/ios/MullvadVPN/PrivateKeyWithMetadata.swift @@ -10,7 +10,7 @@ import Foundation import WireGuardKit /// A struct holding a private WireGuard key with associated metadata -struct PrivateKeyWithMetadata { +struct PrivateKeyWithMetadata: Equatable { /// When the key was created let creationDate: Date diff --git a/ios/MullvadVPN/RelayConstraints.swift b/ios/MullvadVPN/RelayConstraints.swift index 430cbc9645..7bedd611a1 100644 --- a/ios/MullvadVPN/RelayConstraints.swift +++ b/ios/MullvadVPN/RelayConstraints.swift @@ -10,7 +10,7 @@ import Foundation private let kRelayConstraintAnyRepr = "any" -enum RelayConstraint<T: Codable>: Codable { +enum RelayConstraint<T>: Codable, Equatable where T: Codable & Equatable { case any case only(T) @@ -166,7 +166,7 @@ extension RelayLocation: CustomDebugStringConvertible { } } -struct RelayConstraints: Codable { +struct RelayConstraints: Codable, Equatable { var location: RelayConstraint<RelayLocation> = .only(.country("se")) } diff --git a/ios/MullvadVPN/SettingsNavigationController.swift b/ios/MullvadVPN/SettingsNavigationController.swift index 59ff50d499..0bb586f8b6 100644 --- a/ios/MullvadVPN/SettingsNavigationController.swift +++ b/ios/MullvadVPN/SettingsNavigationController.swift @@ -11,6 +11,7 @@ import UIKit enum SettingsNavigationRoute { case account + case preferences case wireguardKeys case problemReport } @@ -79,6 +80,9 @@ class SettingsNavigationController: CustomNavigationController, SettingsViewCont controller.delegate = self pushViewController(controller, animated: animated) + case .preferences: + pushViewController(PreferencesViewController(), animated: animated) + case .wireguardKeys: pushViewController(WireguardKeysViewController(), animated: animated) diff --git a/ios/MullvadVPN/SettingsSwitchCell.swift b/ios/MullvadVPN/SettingsSwitchCell.swift new file mode 100644 index 0000000000..d644c5cf77 --- /dev/null +++ b/ios/MullvadVPN/SettingsSwitchCell.swift @@ -0,0 +1,36 @@ +// +// SettingsSwitchCell.swift +// MullvadVPN +// +// Created by pronebird on 19/05/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +class SettingsSwitchCell: SettingsCell { + + let switchContainer = CustomSwitchContainer() + var switchControl: CustomSwitch { + return switchContainer.control + } + + var action: ((Bool) -> Void)? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + self.accessoryView = switchContainer + + switchControl.addTarget(self, action: #selector(switchValueDidChange), for: .valueChanged) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func switchValueDidChange() { + self.action?(self.switchControl.isOn) + } + +} diff --git a/ios/MullvadVPN/SettingsViewController.swift b/ios/MullvadVPN/SettingsViewController.swift index 31eef92835..0735c60542 100644 --- a/ios/MullvadVPN/SettingsViewController.swift +++ b/ios/MullvadVPN/SettingsViewController.swift @@ -46,14 +46,15 @@ class SettingsViewController: UITableViewController, AccountObserver { tableView.separatorColor = .secondaryColor tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 60 - tableView.sectionHeaderHeight = 18 - tableView.sectionFooterHeight = 18 + tableView.sectionHeaderHeight = UIMetrics.contentLayoutMargins.top + tableView.sectionFooterHeight = 0 tableView.dataSource = staticDataSource tableView.delegate = staticDataSource tableView.register(SettingsAccountCell.self, forCellReuseIdentifier: CellIdentifier.accountCell.rawValue) tableView.register(SettingsCell.self, forCellReuseIdentifier: CellIdentifier.basicCell.rawValue) + tableView.register(EmptyTableViewHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: EmptyTableViewHeaderFooterView.reuseIdentifier) navigationItem.title = NSLocalizedString("Settings", comment: "Navigation title") navigationItem.largeTitleDisplayMode = .always @@ -103,6 +104,16 @@ class SettingsViewController: UITableViewController, AccountObserver { self?.settingsNavigationController?.navigate(to: .account, animated: true) } + let preferencesRow = StaticTableViewRow(reuseIdentifier: CellIdentifier.basicCell.rawValue) { (_, cell) in + let cell = cell as! SettingsCell + cell.titleLabel.text = NSLocalizedString("Preferences", comment: "") + cell.accessoryType = .disclosureIndicator + } + + preferencesRow.actionBlock = { [weak self] (indexPath) in + self?.settingsNavigationController?.navigate(to: .preferences, animated: true) + } + let wireguardKeyRow = StaticTableViewRow(reuseIdentifier: CellIdentifier.basicCell.rawValue) { (_, cell) in let cell = cell as! SettingsCell @@ -117,7 +128,7 @@ class SettingsViewController: UITableViewController, AccountObserver { self.accountRow = accountRow - topSection.addRows([accountRow, wireguardKeyRow]) + topSection.addRows([accountRow, preferencesRow, wireguardKeyRow]) staticDataSource.addSections([topSection]) } @@ -172,12 +183,8 @@ class SettingsTableViewDataSource: StaticTableViewDataSource { // MARK: - UITableViewDelegate - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return 24 - } - - func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - return 0.01 + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return tableView.dequeueReusableHeaderFooterView(withIdentifier: EmptyTableViewHeaderFooterView.reuseIdentifier) } } diff --git a/ios/MullvadVPN/TunnelManager.swift b/ios/MullvadVPN/TunnelManager.swift index 86ba65e2c4..23ccd3d324 100644 --- a/ios/MullvadVPN/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager.swift @@ -109,7 +109,7 @@ extension TunnelState: CustomStringConvertible, CustomDebugStringConvertible { protocol TunnelObserver: AnyObject { func tunnelStateDidChange(tunnelState: TunnelState) - func tunnelPublicKeyDidChange(publicKeyWithMetadata: PublicKeyWithMetadata?) + func tunnelSettingsDidChange(tunnelSettings: TunnelSettings?) } private class AnyTunnelObserver: WeakObserverBox, TunnelObserver { @@ -126,8 +126,8 @@ private class AnyTunnelObserver: WeakObserverBox, TunnelObserver { self.inner?.tunnelStateDidChange(tunnelState: tunnelState) } - func tunnelPublicKeyDidChange(publicKeyWithMetadata: PublicKeyWithMetadata?) { - self.inner?.tunnelPublicKeyDidChange(publicKeyWithMetadata: publicKeyWithMetadata) + func tunnelSettingsDidChange(tunnelSettings: TunnelSettings?) { + self.inner?.tunnelSettingsDidChange(tunnelSettings: tunnelSettings) } static func == (lhs: AnyTunnelObserver, rhs: AnyTunnelObserver) -> Bool { @@ -261,7 +261,7 @@ class TunnelManager { private var accountToken: String? private var _tunnelState = TunnelState.disconnected - private var _publicKeyWithMetadata: PublicKeyWithMetadata? + private var _tunnelSettings: TunnelSettings? private init() {} @@ -289,21 +289,21 @@ class TunnelManager { } /// The last known public key - private(set) var publicKeyWithMetadata: PublicKeyWithMetadata? { + private(set) var tunnelSettings: TunnelSettings? { set { stateLock.withCriticalBlock { - guard _publicKeyWithMetadata != newValue else { return } + guard _tunnelSettings != newValue else { return } - _publicKeyWithMetadata = newValue + _tunnelSettings = newValue observerList.forEach { (observer) in - observer.tunnelPublicKeyDidChange(publicKeyWithMetadata: newValue) + observer.tunnelSettingsDidChange(tunnelSettings: newValue) } } } get { stateLock.withCriticalBlock { - return _publicKeyWithMetadata + return _tunnelSettings } } } @@ -339,7 +339,12 @@ class TunnelManager { let operation = BlockOperation { // Reload the last known public key if let accountToken = self.accountToken { - self.loadPublicKey(accountToken: accountToken) + switch Self.loadTunnelSettings(accountToken: accountToken) { + case .success(let keychainEntry): + self.tunnelSettings = keychainEntry.tunnelSettings + case .failure(let error): + self.logger.error(chainedError: error, message: "Failed to reload tunnel settings when refreshing tunnel state.") + } } if let status = self.tunnelProvider?.connection.status { @@ -438,24 +443,21 @@ class TunnelManager { let interfaceSettings = tunnelSettings.interface let publicKeyWithMetadata = interfaceSettings.privateKey.publicKeyWithMetadata - let saveAccountData = { - // Save the last known public key - self.publicKeyWithMetadata = publicKeyWithMetadata + guard interfaceSettings.addresses.isEmpty else { + self.tunnelSettings = tunnelSettings self.accountToken = accountToken - } - guard interfaceSettings.addresses.isEmpty else { - saveAccountData() finish(.success(())) return } // Push wireguard key if addresses were not received yet self.pushWireguardKeyAndUpdateSettings(accountToken: accountToken, publicKey: publicKeyWithMetadata.publicKey) { (result) in - if case .success = result { - saveAccountData() + if case .success(let newTunnelSettings) = result { + self.tunnelSettings = newTunnelSettings + self.accountToken = accountToken } - finish(result) + finish(result.map { _ in () }) } } operation.addDidFinishBlockObserver { (operation, result) in @@ -475,7 +477,7 @@ class TunnelManager { let completeOperation = { self.accountToken = nil - self.publicKeyWithMetadata = nil + self.tunnelSettings = nil finish(.success(())) } @@ -615,13 +617,12 @@ class TunnelManager { .publicKeyWithMetadata self.replaceWireguardKeyAndUpdateSettings(accountToken: accountToken, oldPublicKey: oldPublicKeyMetadata, newPrivateKey: newPrivateKey) { (result) in - guard case .success = result else { - finish(result) + guard case .success(let newTunnelSettings) = result else { + finish(result.map { _ in () }) return } - // Save new public key - self.publicKeyWithMetadata = newPrivateKey.publicKeyWithMetadata + self.tunnelSettings = newTunnelSettings guard let tunnelIpc = self.tunnelIpc else { finish(.success(())) @@ -647,59 +648,15 @@ class TunnelManager { } 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 { - self.logger.error(chainedError: error, message: "Failed to reload tunnel settings") - } - - finish(.success(())) - } - } - - operation.addDidFinishBlockObserver { (operation, result) in - completionHandler(result) - } - - exclusityController.addOperation(operation, categories: [.tunnelControl]) + self.addOperationToModifyTunnelSettingsAndNotifyPacketTunnel(usingBlock: { (tunnelSettings) in + tunnelSettings.relayConstraints = constraints + }, completionHandler: completionHandler) } - func getRelayConstraints(completionHandler: @escaping (Result<RelayConstraints, TunnelManager.Error>) -> Void) { - let operation = BlockOperation { - guard let accountToken = self.accountToken else { - completionHandler(.failure(.missingAccount)) - return - } - - let result = Self.loadTunnelSettings(accountToken: accountToken) - .map { (keychainEntry) -> RelayConstraints in - return keychainEntry.tunnelSettings.relayConstraints - } - - completionHandler(result) - } - - exclusityController.addOperation(operation, categories: [.tunnelControl]) + func setDNSSettings(_ dnsSettings: DNSSettings, completionHandler: @escaping (Result<(), TunnelManager.Error>) -> Void) { + self.addOperationToModifyTunnelSettingsAndNotifyPacketTunnel(usingBlock: { (tunnelSettings) in + tunnelSettings.interface.dnsSettings = dnsSettings + }, completionHandler: completionHandler) } // MARK: - Tunnel observeration @@ -751,14 +708,12 @@ class TunnelManager { // stored in `passwordReference` field of VPN configuration. case (.some(let tunnelProvider), .some(let accountToken)): let verificationResult = self.verifyTunnel(tunnelProvider: tunnelProvider, expectedAccountToken: accountToken) - let tunnelSettingsResult = TunnelSettingsManager.load(searchTerm: .accountToken(accountToken)).mapError { (error) -> Error in - return .readTunnelSettings(error) - } + let tunnelSettingsResult = Self.loadTunnelSettings(accountToken: accountToken) switch (verificationResult, tunnelSettingsResult) { case (.success(true), .success(let keychainEntry)): self.accountToken = accountToken - self.publicKeyWithMetadata = keychainEntry.tunnelSettings.interface.privateKey.publicKeyWithMetadata + self.tunnelSettings = keychainEntry.tunnelSettings self.setTunnelProvider(tunnelProvider: tunnelProvider) completionHandler(.success(())) @@ -780,7 +735,7 @@ class TunnelManager { completionHandler(.failure(.removeInconsistentVPNConfiguration(error))) } else { self.accountToken = accountToken - self.publicKeyWithMetadata = keychainEntry.tunnelSettings.interface.privateKey.publicKeyWithMetadata + self.tunnelSettings = keychainEntry.tunnelSettings completionHandler(.success(())) } @@ -828,15 +783,15 @@ class TunnelManager { // Case 3: tunnel does not exist but the account token is set. // Verify that tunnel settings exists in keychain. case (.none, .some(let accountToken)): - switch TunnelSettingsManager.load(searchTerm: .accountToken(accountToken)) { + switch Self.loadTunnelSettings(accountToken: accountToken) { case .success(let keychainEntry): self.accountToken = accountToken - self.publicKeyWithMetadata = keychainEntry.tunnelSettings.interface.privateKey.publicKeyWithMetadata + self.tunnelSettings = keychainEntry.tunnelSettings completionHandler(.success(())) case .failure(let error): - completionHandler(.failure(.readTunnelSettings(error))) + completionHandler(.failure(error)) } // Case 4: no tunnels exist and account token is unset. @@ -909,22 +864,10 @@ class TunnelManager { } } - private func loadPublicKey(accountToken: String) { - switch TunnelSettingsManager.load(searchTerm: .accountToken(accountToken)) { - case .success(let entry): - self.publicKeyWithMetadata = entry.tunnelSettings.interface.privateKey.publicKeyWithMetadata - - case .failure(let error): - self.logger.error(chainedError: error, message: "Failed to load the public key") - - self.publicKeyWithMetadata = nil - } - } - private func pushWireguardKeyAndUpdateSettings( accountToken: String, publicKey: PublicKey, - completionHandler: @escaping (Result<(), Error>) -> Void) + completionHandler: @escaping (Result<TunnelSettings, Error>) -> Void) { let payload = TokenPayload(token: accountToken, payload: PushWireguardKeyRequest(pubkey: publicKey.rawValue)) let operation = rest.pushWireguardKey().operation(payload: payload) @@ -934,14 +877,14 @@ class TunnelManager { .mapError({ (restError) -> Error in return .pushWireguardKey(restError) }) - .flatMap { (associatedAddresses) -> Result<(), Error> in + .flatMap { (associatedAddresses) -> Result<TunnelSettings, Error> in return Self.updateTunnelSettings(accountToken: accountToken) { (tunnelSettings) in tunnelSettings.interface.addresses = [ associatedAddresses.ipv4Address, associatedAddresses.ipv6Address ] - }.map { _ in () } - } + } + } completionHandler(updateResult) } @@ -974,7 +917,7 @@ class TunnelManager { accountToken: String, oldPublicKey: PublicKeyWithMetadata, newPrivateKey: PrivateKeyWithMetadata, - completionHandler: @escaping (Result<(), Error>) -> Void) + completionHandler: @escaping (Result<TunnelSettings, Error>) -> Void) { let payload = TokenPayload( token: accountToken, @@ -991,14 +934,14 @@ class TunnelManager { .mapError({ (restError) -> Error in return .replaceWireguardKey(restError) }) - .flatMap { (associatedAddresses) -> Result<(), Error> in + .flatMap { (associatedAddresses) -> Result<TunnelSettings, Error> in return Self.updateTunnelSettings(accountToken: accountToken) { (tunnelSettings) in tunnelSettings.interface.privateKey = newPrivateKey tunnelSettings.interface.addresses = [ associatedAddresses.ipv4Address, associatedAddresses.ipv6Address ] - }.map { _ in () } + } } completionHandler(updateResult) @@ -1007,6 +950,45 @@ class TunnelManager { operationQueue.addOperation(operation) } + /// Modify tunnel settings in Keychain and tell Packet Tunnel to reload. + private func addOperationToModifyTunnelSettingsAndNotifyPacketTunnel(usingBlock block: @escaping (inout TunnelSettings) -> Void, 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, block: block) + + guard case .success(let newTunnelSettings) = result else { + finish(result.map { _ in () }) + return + } + + self.tunnelSettings = newTunnelSettings + + 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 { + self.logger.error(chainedError: error, message: "Failed to reload tunnel settings") + } + + finish(.success(())) + } + } + + operation.addDidFinishBlockObserver { (operation, result) in + completionHandler(result) + } + + exclusityController.addOperation(operation, categories: [.tunnelControl]) + } + /// Initiates the `tunnelState` update private func updateTunnelState(connectionStatus: NEVPNStatus) { let operation = AsyncBlockOperation { (finish) in @@ -1059,7 +1041,12 @@ class TunnelManager { // 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) + switch Self.loadTunnelSettings(accountToken: accountToken) { + case .success(let keychainEntry): + self.tunnelSettings = keychainEntry.tunnelSettings + case .failure(let error): + self.logger.error(chainedError: error, message: "Failed to refresh tunnel settings upon receiving the .reasserting tunnel state.") + } } guard let tunnelIpc = tunnelIpc else { diff --git a/ios/MullvadVPN/TunnelSettings.swift b/ios/MullvadVPN/TunnelSettings.swift index 750538dd95..a2a6b24029 100644 --- a/ios/MullvadVPN/TunnelSettings.swift +++ b/ios/MullvadVPN/TunnelSettings.swift @@ -11,15 +11,53 @@ import Network import NetworkExtension import WireGuardKit -/// A struct that holds a tun interface configuration -struct InterfaceSettings: Codable { - var privateKey = PrivateKeyWithMetadata() - var addresses = [IPAddressRange]() +/// A struct that holds a tun interface configuration. +struct InterfaceSettings: Codable, Equatable { + var privateKey: PrivateKeyWithMetadata + var addresses: [IPAddressRange] + var dnsSettings: DNSSettings + + private enum CodingKeys: String, CodingKey { + case privateKey, addresses, dnsSettings + } + + init(privateKey: PrivateKeyWithMetadata = PrivateKeyWithMetadata(), addresses: [IPAddressRange] = [], dnsSettings: DNSSettings = DNSSettings()) { + self.privateKey = privateKey + self.addresses = addresses + self.dnsSettings = dnsSettings + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + privateKey = try container.decode(PrivateKeyWithMetadata.self, forKey: .privateKey) + addresses = try container.decode([IPAddressRange].self, forKey: .addresses) + + // Provide default value, since `dnsSettings` key does not exist in <= 2021.2 + dnsSettings = try container.decodeIfPresent(DNSSettings.self, forKey: .dnsSettings) + ?? DNSSettings() + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(privateKey, forKey: .privateKey) + try container.encode(addresses, forKey: .addresses) + try container.encode(dnsSettings, forKey: .dnsSettings) + } } -/// A struct that holds the configuration passed via NETunnelProviderProtocol -struct TunnelSettings: Codable { +/// A struct that holds the configuration passed via `NETunnelProviderProtocol`. +struct TunnelSettings: Codable, Equatable { var relayConstraints = RelayConstraints() var interface = InterfaceSettings() } +/// A struct that holds DNS settings. +struct DNSSettings: Codable, Equatable { + /// Block advertising. + var blockAdvertising: Bool = false + + /// Block tracking. + var blockTracking: Bool = false +} diff --git a/ios/MullvadVPN/UIColor+Palette.swift b/ios/MullvadVPN/UIColor+Palette.swift index b883cbf2b7..b50b31fe70 100644 --- a/ios/MullvadVPN/UIColor+Palette.swift +++ b/ios/MullvadVPN/UIColor+Palette.swift @@ -41,6 +41,12 @@ extension UIColor { static let disabledTitleColor = UIColor.lightGray } + enum Switch { + static let borderColor = UIColor(white: 1.0, alpha: 0.8) + static let onThumbColor = successColor + static let offThumbColor = dangerColor + } + // Relay availability indicator view enum RelayStatusIndicator { static let activeColor = successColor.withAlphaComponent(0.9) diff --git a/ios/MullvadVPN/WireguardKeysViewController.swift b/ios/MullvadVPN/WireguardKeysViewController.swift index 618867a8d7..9c78e489ea 100644 --- a/ios/MullvadVPN/WireguardKeysViewController.swift +++ b/ios/MullvadVPN/WireguardKeysViewController.swift @@ -49,7 +49,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { navigationItem.title = NSLocalizedString("WireGuard key", comment: "Navigation title") TunnelManager.shared.addObserver(self) - updatePublicKeyWithMetadata(publicKeyWithMetadata: TunnelManager.shared.publicKeyWithMetadata, animated: false) + updatePublicKey(tunnelSettings: TunnelManager.shared.tunnelSettings, animated: false) startPublicKeyPeriodicUpdate() } @@ -58,9 +58,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { let interval = DispatchTimeInterval.seconds(kCreationDateRefreshInterval) let timerSource = DispatchSource.makeTimerSource(queue: .main) timerSource.setEventHandler { [weak self] () -> Void in - let metadata = TunnelManager.shared.publicKeyWithMetadata - - self?.updatePublicKeyWithMetadata(publicKeyWithMetadata: metadata, animated: true) + self?.updatePublicKey(tunnelSettings: TunnelManager.shared.tunnelSettings, animated: true) } timerSource.schedule(deadline: .now() + interval, repeating: interval) timerSource.activate() @@ -74,16 +72,16 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { // no-op } - func tunnelPublicKeyDidChange(publicKeyWithMetadata: PublicKeyWithMetadata?) { + func tunnelSettingsDidChange(tunnelSettings: TunnelSettings?) { DispatchQueue.main.async { - self.updatePublicKeyWithMetadata(publicKeyWithMetadata: publicKeyWithMetadata, animated: true) + self.updatePublicKey(tunnelSettings: tunnelSettings, animated: true) } } // MARK: - IBActions @IBAction func copyPublicKey(_ sender: Any) { - guard let metadata = TunnelManager.shared.publicKeyWithMetadata else { return } + guard let metadata = TunnelManager.shared.tunnelSettings?.interface.privateKey.publicKeyWithMetadata else { return } UIPasteboard.general.string = metadata.stringRepresentation() @@ -92,9 +90,7 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { animated: true) let dispatchWork = DispatchWorkItem { [weak self] in - let metadata = TunnelManager.shared.publicKeyWithMetadata - - self?.updatePublicKeyWithMetadata(publicKeyWithMetadata: metadata, animated: true) + self?.updatePublicKey(tunnelSettings: TunnelManager.shared.tunnelSettings, animated: true) } DispatchQueue.main.asyncAfter(wallDeadline: .now() + .seconds(3), execute: dispatchWork) @@ -127,8 +123,8 @@ class WireguardKeysViewController: UIViewController, TunnelObserver { creationDateLabel.text = formatKeyGenerationElapsedTime(with: creationDate) ?? "-" } - private func updatePublicKeyWithMetadata(publicKeyWithMetadata: PublicKeyWithMetadata?, animated: Bool) { - if let publicKey = publicKeyWithMetadata { + private func updatePublicKey(tunnelSettings: TunnelSettings?, animated: Bool) { + if let publicKey = tunnelSettings?.interface.privateKey.publicKeyWithMetadata { let displayKey = publicKey .stringRepresentation(maxLength: kDisplayPublicKeyMaxLength) diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index 8e12bdb9f7..69800ef006 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -440,7 +440,6 @@ extension PacketTunnelConfiguration { return peerConfig } - let dnsServers: [IPAddress] = [mullvadEndpoint.ipv4Gateway, mullvadEndpoint.ipv6Gateway] var interfaceConfig = InterfaceConfiguration(privateKey: tunnelSettings.interface.privateKey.privateKey) interfaceConfig.listenPort = 0 interfaceConfig.dns = dnsServers.map { DNSServer(address: $0) } @@ -448,6 +447,22 @@ extension PacketTunnelConfiguration { return TunnelConfiguration(name: nil, interface: interfaceConfig, peers: peerConfigs) } + + var dnsServers: [IPAddress] { + let mullvadEndpoint = selectorResult.endpoint + let dnsSettings = tunnelSettings.interface.dnsSettings + + switch (dnsSettings.blockAdvertising, dnsSettings.blockTracking) { + case (true, false): + return [IPv4Address("100.64.0.1")!] + case (false, true): + return [IPv4Address("100.64.0.2")!] + case (true, true): + return [IPv4Address("100.64.0.3")!] + case (false, false): + return [mullvadEndpoint.ipv4Gateway, mullvadEndpoint.ipv6Gateway] + } + } } struct PacketTunnelContext { |
