summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--ios/CHANGELOG.md1
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj48
-rw-r--r--ios/MullvadVPN/AppDelegate.swift72
-rw-r--r--ios/MullvadVPN/ConnectViewController.swift2
-rw-r--r--ios/MullvadVPN/CustomSwitch.swift72
-rw-r--r--ios/MullvadVPN/CustomSwitchContainer.swift66
-rw-r--r--ios/MullvadVPN/EmptyTableViewHeaderFooterView.swift26
-rw-r--r--ios/MullvadVPN/PreferencesViewController.swift123
-rw-r--r--ios/MullvadVPN/PrivateKeyWithMetadata.swift2
-rw-r--r--ios/MullvadVPN/RelayConstraints.swift4
-rw-r--r--ios/MullvadVPN/SettingsNavigationController.swift4
-rw-r--r--ios/MullvadVPN/SettingsSwitchCell.swift36
-rw-r--r--ios/MullvadVPN/SettingsViewController.swift25
-rw-r--r--ios/MullvadVPN/TunnelManager.swift193
-rw-r--r--ios/MullvadVPN/TunnelSettings.swift50
-rw-r--r--ios/MullvadVPN/UIColor+Palette.swift6
-rw-r--r--ios/MullvadVPN/WireguardKeysViewController.swift20
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider.swift17
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 {