From 1d03677f7835f38dc78307cec1da50e6cf88f773 Mon Sep 17 00:00:00 2001 From: mojganii Date: Thu, 13 Jun 2024 11:19:13 +0200 Subject: [PATCH] Add multi-hop toggle to settings view --- .../Shadowsocks/ShadowsocksLoader.swift | 29 ++++++++-- .../ShadowsocksRelaySelector.swift | 27 ++-------- ios/MullvadSettings/MultihopSettings.swift | 4 ++ .../TunnelSettingsUpdate.swift | 2 +- ios/MullvadVPN.xcodeproj/project.pbxproj | 34 ++++++++---- ios/MullvadVPN/AppDelegate.swift | 14 ++--- .../Classes/AccessbilityIdentifier.swift | 6 +++ .../VPNSettings/CustomDNSDataSource.swift | 2 +- .../VPNSettings/CustomDNSViewController.swift | 18 ++----- .../VPNSettings/VPNSettingsCellFactory.swift | 18 +++++++ .../VPNSettings/VPNSettingsDataSource.swift | 44 +++++++++++++-- .../VPNSettingsDataSourceDelegate.swift | 7 +++ .../VPNSettingsViewController.swift | 52 ++++++++++++++++-- .../VPNSettings/VPNSettingsViewModel.swift | 6 +++ .../Shadowsocks/ShadowsocksLoaderTests.swift | 53 +++++++++---------- .../Pages/MultihopPromptAlert.swift | 29 ++++++++++ .../Pages/VPNSettingsPage.swift | 30 +++++++++++ .../SettingsMigrationTests.swift | 2 + .../PacketTunnelProvider.swift | 34 +++++------- .../RelaySelectorWrapper.swift | 15 +----- .../Actor/ObservedState+Extensions.swift | 14 ----- .../Protocols/SettingsReaderProtocol.swift | 16 +++++- .../Actor/TunnelSettingsManager.swift | 23 ++++++++ .../TunnelSettingsManagerTests.swift | 28 ++++++++++ 24 files changed, 362 insertions(+), 145 deletions(-) create mode 100644 ios/MullvadVPNUITests/Pages/MultihopPromptAlert.swift create mode 100644 ios/PacketTunnelCore/Actor/TunnelSettingsManager.swift create mode 100644 ios/PacketTunnelCoreTests/TunnelSettingsManagerTests.swift diff --git a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift index f079fbf8608a..f91980566520 100644 --- a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift +++ b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift @@ -19,25 +19,48 @@ public class ShadowsocksLoader: ShadowsocksLoaderProtocol { let cache: ShadowsocksConfigurationCacheProtocol let relaySelector: ShadowsocksRelaySelectorProtocol let constraintsUpdater: RelayConstraintsUpdater + let multihopUpdater: MultihopUpdater + private var multihopState: MultihopState = .off + private var observer: MultihopObserverBlock! + + deinit { + self.multihopUpdater.removeObserver(observer) + } private var relayConstraints = RelayConstraints() public init( cache: ShadowsocksConfigurationCacheProtocol, relaySelector: ShadowsocksRelaySelectorProtocol, - constraintsUpdater: RelayConstraintsUpdater + constraintsUpdater: RelayConstraintsUpdater, + multihopUpdater: MultihopUpdater, + multihopState: MultihopState = .off ) { self.cache = cache self.relaySelector = relaySelector self.constraintsUpdater = constraintsUpdater + self.multihopUpdater = multihopUpdater + self.multihopState = multihopState + self.addObservers() + } - // The constraints gets updated a lot when observing the tunnel, avoid clearing the cache if the constraints haven't changed. + private func addObservers() { + // The constraints gets updated a lot when observing the tunnel, clear the cache if the constraints have changed. constraintsUpdater.onNewConstraints = { [weak self] newConstraints in if self?.relayConstraints != newConstraints { self?.relayConstraints = newConstraints try? self?.clear() } } + + // The multihop state gets updated a lot when observing the tunnel, clear the cache if the multihop settings have changed. + self.observer = MultihopObserverBlock(didUpdateMultihop: { [weak self] _, newMultihopState in + if self?.multihopState != newMultihopState { + self?.multihopState = newMultihopState + try? self?.clear() + } + }) + multihopUpdater.addObserver(self.observer) } public func clear() throws { @@ -60,7 +83,7 @@ public class ShadowsocksLoader: ShadowsocksLoaderProtocol { /// Returns a randomly selected shadowsocks configuration. private func create() throws -> ShadowsocksConfiguration { let bridgeConfiguration = try relaySelector.getBridges() - let closestRelay = try relaySelector.selectRelay(with: relayConstraints) + let closestRelay = try relaySelector.selectRelay(with: relayConstraints, multihopState: multihopState) guard let bridgeAddress = closestRelay?.ipv4AddrIn, let bridgeConfiguration else { throw POSIXError(.ENOENT) } diff --git a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksRelaySelector.swift b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksRelaySelector.swift index 2c9efa5ca2c7..2519c4065a31 100644 --- a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksRelaySelector.swift +++ b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksRelaySelector.swift @@ -12,7 +12,8 @@ import MullvadTypes public protocol ShadowsocksRelaySelectorProtocol { func selectRelay( - with constraints: RelayConstraints + with constraints: RelayConstraints, + multihopState: MultihopState ) throws -> REST.BridgeRelay? func getBridges() throws -> REST.ServerShadowsocks? @@ -20,34 +21,16 @@ public protocol ShadowsocksRelaySelectorProtocol { final public class ShadowsocksRelaySelector: ShadowsocksRelaySelectorProtocol { let relayCache: RelayCacheProtocol - let multihopUpdater: MultihopUpdater - private var multihopState: MultihopState - private var observer: MultihopObserverBlock! - - deinit { - self.multihopUpdater.removeObserver(observer) - } public init( - relayCache: RelayCacheProtocol, - multihopUpdater: MultihopUpdater, - multihopState: MultihopState + relayCache: RelayCacheProtocol ) { self.relayCache = relayCache - self.multihopUpdater = multihopUpdater - self.multihopState = multihopState - self.addObserver() - } - - private func addObserver() { - self.observer = MultihopObserverBlock(didUpdateMultihop: { [weak self] _, multihopState in - self?.multihopState = multihopState - }) - multihopUpdater.addObserver(observer) } public func selectRelay( - with constraints: RelayConstraints + with constraints: RelayConstraints, + multihopState: MultihopState ) throws -> REST.BridgeRelay? { let cachedRelays = try relayCache.read().relays diff --git a/ios/MullvadSettings/MultihopSettings.swift b/ios/MullvadSettings/MultihopSettings.swift index 881324e792a1..ba64c12e485f 100644 --- a/ios/MullvadSettings/MultihopSettings.swift +++ b/ios/MullvadSettings/MultihopSettings.swift @@ -69,4 +69,8 @@ public class MultihopUpdater { public enum MultihopState: Codable { case on case off + + public var isEnabled: Bool { + self == .on + } } diff --git a/ios/MullvadSettings/TunnelSettingsUpdate.swift b/ios/MullvadSettings/TunnelSettingsUpdate.swift index 92349a38ed79..5ab46b84132d 100644 --- a/ios/MullvadSettings/TunnelSettingsUpdate.swift +++ b/ios/MullvadSettings/TunnelSettingsUpdate.swift @@ -39,7 +39,7 @@ extension TunnelSettingsUpdate { case .obfuscation: "obfuscation settings" case .relayConstraints: "relay constraints" case .quantumResistance: "quantum resistance" - case .multihop: "Multihop" + case .multihop: "multihop" } } } diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index bb540b34be0a..60b89d512da5 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -875,6 +875,7 @@ F06045E62B231EB700B2D37A /* URLSessionTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = F06045E52B231EB700B2D37A /* URLSessionTransport.swift */; }; F06045EA2B23217E00B2D37A /* ShadowsocksTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = F06045E92B23217E00B2D37A /* ShadowsocksTransport.swift */; }; F06045EC2B2322A500B2D37A /* Jittered.swift in Sources */ = {isa = PBXBuildFile; fileRef = F06045EB2B2322A500B2D37A /* Jittered.swift */; }; + F062B94D2C16E09700B6D47A /* TunnelSettingsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F062B94C2C16E09700B6D47A /* TunnelSettingsManagerTests.swift */; }; F072D3CF2C07122400906F64 /* MultihopUpdaterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F072D3CE2C07122400906F64 /* MultihopUpdaterTests.swift */; }; F072D3D22C071AD100906F64 /* ShadowsocksLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F072D3D12C071AD100906F64 /* ShadowsocksLoaderTests.swift */; }; F07BF2622A26279100042943 /* RedeemVoucherOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07BF2612A26279100042943 /* RedeemVoucherOperation.swift */; }; @@ -928,6 +929,8 @@ F0DA87472A9CB9A2006044F1 /* AccountExpiryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DA87462A9CB9A2006044F1 /* AccountExpiryRow.swift */; }; F0DA87492A9CBA9F006044F1 /* AccountDeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DA87482A9CBA9F006044F1 /* AccountDeviceRow.swift */; }; F0DA874B2A9CBACB006044F1 /* AccountNumberRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DA874A2A9CBACB006044F1 /* AccountNumberRow.swift */; }; + F0DAC8AD2C16EFE400F80144 /* TunnelSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04DD3D72C130DF600E03E28 /* TunnelSettingsManager.swift */; }; + F0DAC8AF2C1712C300F80144 /* MultihopPromptAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DAC8AE2C1712C300F80144 /* MultihopPromptAlert.swift */; }; F0DDE4142B220458006B57A7 /* ShadowSocksProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DDE40F2B220458006B57A7 /* ShadowSocksProxy.swift */; }; F0DDE4152B220458006B57A7 /* ShadowsocksConfigurationCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DDE4102B220458006B57A7 /* ShadowsocksConfigurationCache.swift */; }; F0DDE4162B220458006B57A7 /* TransportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DDE4112B220458006B57A7 /* TransportProvider.swift */; }; @@ -2101,6 +2104,7 @@ F02F419C2B9723AF00625A4F /* AddLocationsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddLocationsCoordinator.swift; sourceTree = ""; }; F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncreasedHitButton.swift; sourceTree = ""; }; F04413602BA45CD70018A6EE /* CustomListLocationNodeBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListLocationNodeBuilder.swift; sourceTree = ""; }; + F04DD3D72C130DF600E03E28 /* TunnelSettingsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelSettingsManager.swift; sourceTree = ""; }; F04F95A02B21D24400431E08 /* shadowsocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = shadowsocks.h; sourceTree = ""; }; F04FBE602A8379EE009278D7 /* AppPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPreferences.swift; sourceTree = ""; }; F050AE4D2B70D7F8003F4EDB /* LocationCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationCellViewModel.swift; sourceTree = ""; }; @@ -2115,6 +2119,7 @@ F06045E52B231EB700B2D37A /* URLSessionTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTransport.swift; sourceTree = ""; }; F06045E92B23217E00B2D37A /* ShadowsocksTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksTransport.swift; sourceTree = ""; }; F06045EB2B2322A500B2D37A /* Jittered.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Jittered.swift; sourceTree = ""; }; + F062B94C2C16E09700B6D47A /* TunnelSettingsManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsManagerTests.swift; sourceTree = ""; }; F072D3CE2C07122400906F64 /* MultihopUpdaterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopUpdaterTests.swift; sourceTree = ""; }; F072D3D12C071AD100906F64 /* ShadowsocksLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoaderTests.swift; sourceTree = ""; }; F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextFormatterTests.swift; sourceTree = ""; }; @@ -2147,6 +2152,7 @@ F0DA87462A9CB9A2006044F1 /* AccountExpiryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryRow.swift; sourceTree = ""; }; F0DA87482A9CBA9F006044F1 /* AccountDeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeviceRow.swift; sourceTree = ""; }; F0DA874A2A9CBACB006044F1 /* AccountNumberRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountNumberRow.swift; sourceTree = ""; }; + F0DAC8AE2C1712C300F80144 /* MultihopPromptAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopPromptAlert.swift; sourceTree = ""; }; F0DDE40F2B220458006B57A7 /* ShadowSocksProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowSocksProxy.swift; sourceTree = ""; }; F0DDE4102B220458006B57A7 /* ShadowsocksConfigurationCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksConfigurationCache.swift; sourceTree = ""; }; F0DDE4112B220458006B57A7 /* TransportProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransportProvider.swift; sourceTree = ""; }; @@ -3105,9 +3111,8 @@ children = ( 58BDEBA02A9CA14B00F578F2 /* AnyTask.swift */, 58F3F3652AA086A400D3B0A4 /* AutoCancellingTask.swift */, - 583832282AC3DF1300EA2071 /* PacketTunnelActorCommand.swift */, - 5838322A2AC3EF9600EA2071 /* EventChannel.swift */, 583E60952A9F6D0800DC61EF /* ConfigurationBuilder.swift */, + 5838322A2AC3EF9600EA2071 /* EventChannel.swift */, 580D6B892AB31AB400B2D6E0 /* NetworkPath+NetworkReachability.swift */, 58CF95A12AD6F35800B59F5D /* ObservedState.swift */, 587A5E512ADD7569003A70F1 /* ObservedState+Extensions.swift */, @@ -3117,9 +3122,12 @@ 58FE25F32AA9D730003D1918 /* PacketTunnelActor+Extensions.swift */, 5838321E2AC3160A00EA2071 /* PacketTunnelActor+KeyPolicy.swift */, 583832202AC3174700EA2071 /* PacketTunnelActor+NetworkReachability.swift */, + 44DF8AC32BF20BD200869CA4 /* PacketTunnelActor+PostQuantum.swift */, 586C14592AC4735F00245C01 /* PacketTunnelActor+Public.swift */, 583832262AC3193600EA2071 /* PacketTunnelActor+SleepCycle.swift */, + 583832282AC3DF1300EA2071 /* PacketTunnelActorCommand.swift */, 7AD0AA192AD69B6E00119E10 /* PacketTunnelActorProtocol.swift */, + 44B3C4392BFE2C800079782C /* PacketTunnelActorReducer.swift */, A97D25AD2B0BB18100946B2D /* ProtocolObfuscator.swift */, 58E7A0312AA0715100C57861 /* Protocols */, 58ED3A132A7C199C0085CE65 /* StartOptions.swift */, @@ -3127,8 +3135,7 @@ 58342C032AAB61FB003BA12D /* State+Extensions.swift */, 586E8DB72AAF4AC4007BF3DA /* Task+Duration.swift */, 58DDA18E2ABC32380039C360 /* Timings.swift */, - 44DF8AC32BF20BD200869CA4 /* PacketTunnelActor+PostQuantum.swift */, - 44B3C4392BFE2C800079782C /* PacketTunnelActorReducer.swift */, + F04DD3D72C130DF600E03E28 /* TunnelSettingsManager.swift */, ); path = Actor; sourceTree = ""; @@ -3383,14 +3390,15 @@ 58C7A4432A863F490060C66F /* PacketTunnelCoreTests */ = { isa = PBXGroup; children = ( - 58EC067D2A8D2B0700BEB973 /* Mocks */, 7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */, 586C14572AC463BB00245C01 /* EventChannelTests.swift */, + 58EC067D2A8D2B0700BEB973 /* Mocks */, 58FE25D32AA729B5003D1918 /* PacketTunnelActorTests.swift */, 58C7A46F2A8649ED0060C66F /* PingerTests.swift */, + A97D25B12B0CB02D00946B2D /* ProtocolObfuscatorTests.swift */, 5838321C2AC1C54600EA2071 /* TaskSleepTests.swift */, 58092E532A8B832E00C3CC72 /* TunnelMonitorTests.swift */, - A97D25B12B0CB02D00946B2D /* ProtocolObfuscatorTests.swift */, + F062B94C2C16E09700B6D47A /* TunnelSettingsManagerTests.swift */, ); path = PacketTunnelCoreTests; sourceTree = ""; @@ -3731,14 +3739,14 @@ 58F3F3682AA08E2200D3B0A4 /* PacketTunnelProvider */ = { isa = PBXGroup; children = ( - 58F3F3692AA08E3C00D3B0A4 /* PacketTunnelProvider.swift */, - 580D6B912AB360BE00B2D6E0 /* DeviceCheck+BlockedStateReason.swift */, - 5864AF7C2A9F4DC9008BC928 /* SettingsReader.swift */, 580D6B8D2AB33BBF00B2D6E0 /* BlockedStateErrorMapper.swift */, - 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */, + 580D6B912AB360BE00B2D6E0 /* DeviceCheck+BlockedStateReason.swift */, 58FF23A22AB09BEE003A2AF2 /* DeviceChecker.swift */, - 58225D272A84F23B0083D7F1 /* PacketTunnelPathObserver.swift */, 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */, + 58225D272A84F23B0083D7F1 /* PacketTunnelPathObserver.swift */, + 58F3F3692AA08E3C00D3B0A4 /* PacketTunnelProvider.swift */, + 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */, + 5864AF7C2A9F4DC9008BC928 /* SettingsReader.swift */, ); path = PacketTunnelProvider; sourceTree = ""; @@ -3943,6 +3951,7 @@ 85557B1D2B5FB8C700795FE1 /* HeaderBar.swift */, A998DA802BD147AD001D61A2 /* ListCustomListsPage.swift */, 852969342B4E9270007EAD4C /* LoginPage.swift */, + F0DAC8AE2C1712C300F80144 /* MultihopPromptAlert.swift */, 85139B2C2B84B4A700734217 /* OutOfTimePage.swift */, 852969322B4E9232007EAD4C /* Page.swift */, 855D9F5A2B63E56B00D7C64D /* ProblemReportPage.swift */, @@ -5517,6 +5526,7 @@ 586E8DB82AAF4AC4007BF3DA /* Task+Duration.swift in Sources */, 5838322B2AC3EF9600EA2071 /* EventChannel.swift in Sources */, 586C145A2AC4735F00245C01 /* PacketTunnelActor+Public.swift in Sources */, + F0DAC8AD2C16EFE400F80144 /* TunnelSettingsManager.swift in Sources */, 58342C042AAB61FB003BA12D /* State+Extensions.swift in Sources */, A95EEE382B722DFC00A8A39B /* PingStats.swift in Sources */, 583832272AC3193600EA2071 /* PacketTunnelActor+SleepCycle.swift in Sources */, @@ -5555,6 +5565,7 @@ 7A3FD1B52AD4465A0042BEA6 /* AppMessageHandlerTests.swift in Sources */, 58C7A4702A8649ED0060C66F /* PingerTests.swift in Sources */, A97D25B22B0CB02D00946B2D /* ProtocolObfuscatorTests.swift in Sources */, + F062B94D2C16E09700B6D47A /* TunnelSettingsManagerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -6109,6 +6120,7 @@ 7A45CFC72C071DD400D80B21 /* SnapshotHelper.swift in Sources */, 856952DC2BD2922A008C1F84 /* PartnerAPIClient.swift in Sources */, 85557B162B5ABBBE00795FE1 /* XCUIElementQuery+Extensions.swift in Sources */, + F0DAC8AF2C1712C300F80144 /* MultihopPromptAlert.swift in Sources */, 855D9F5B2B63E56B00D7C64D /* ProblemReportPage.swift in Sources */, 8529693A2B4F0238007EAD4C /* TermsOfServicePage.swift in Sources */, 85A42B882BB44D31007BABF7 /* DeviceManagementPage.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 645acdeb0f51..fc6c746cd6fc 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -92,9 +92,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD let constraintsUpdater = RelayConstraintsUpdater() let multihopListener = MultihopStateListener() let multihopUpdater = MultihopUpdater(listener: multihopListener) - let multihopState = (try? SettingsManager.readSettings().tunnelMultihopState) ?? .off - settingsObserver = TunnelBlockObserver(didUpdateTunnelSettings: { _, settings in + settingsObserver = TunnelBlockObserver(didLoadConfiguration: { tunnelManager in + multihopListener.onNewMultihop?(tunnelManager.settings.tunnelMultihopState) + constraintsUpdater.onNewConstraints?(tunnelManager.settings.relayConstraints) + }, didUpdateTunnelSettings: { _, settings in multihopListener.onNewMultihop?(settings.tunnelMultihopState) constraintsUpdater.onNewConstraints?(settings.relayConstraints) }) @@ -110,15 +112,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD let urlSessionTransport = URLSessionTransport(urlSession: REST.makeURLSession()) let shadowsocksCache = ShadowsocksConfigurationCache(cacheDirectory: containerURL) let shadowsocksRelaySelector = ShadowsocksRelaySelector( - relayCache: ipOverrideWrapper, - multihopUpdater: multihopUpdater, - multihopState: multihopState + relayCache: ipOverrideWrapper ) shadowsocksLoader = ShadowsocksLoader( cache: shadowsocksCache, relaySelector: shadowsocksRelaySelector, - constraintsUpdater: constraintsUpdater + constraintsUpdater: constraintsUpdater, + multihopUpdater: multihopUpdater, + multihopState: tunnelManager.settings.tunnelMultihopState ) configuredTransportProvider = ProxyConfigurationTransportProvider( diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift index 2f773a880d65..d434d39b7151 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -89,6 +89,8 @@ public enum AccessibilityIdentifier: String { case cityLocationCell case relayLocationCell case customListLocationCell + case multihopConfirmAlertBackButton + case multihopConfirmAlertEnableButton // Labels case accountPageDeviceNameLabel @@ -193,6 +195,10 @@ public enum AccessibilityIdentifier: String { case quantumResistanceOff case quantumResistanceOn + // Multihop + case multihopSwitch + case multihopPromptAlert + // Error case unknown } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSDataSource.swift b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSDataSource.swift index fc559499a0c3..a2bce16dac83 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSDataSource.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSDataSource.swift @@ -120,7 +120,7 @@ final class CustomDNSDataSource: UITableViewDiffableDataSource< private let cellFactory: CustomDNSCellFactory private weak var tableView: UITableView? - weak var delegate: VPNSettingsDataSourceDelegate? + weak var delegate: DNSSettingsDataSourceDelegate? init(tableView: UITableView) { self.tableView = tableView diff --git a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSViewController.swift b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSViewController.swift index df109d3ff3b7..34fdf670d8f9 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSViewController.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSViewController.swift @@ -9,7 +9,7 @@ import MullvadSettings import UIKit -class CustomDNSViewController: UITableViewController, VPNSettingsDataSourceDelegate { +class CustomDNSViewController: UITableViewController { private let interactor: VPNSettingsInteractor private var dataSource: CustomDNSDataSource? private let alertPresenter: AlertPresenter @@ -94,9 +94,9 @@ class CustomDNSViewController: UITableViewController, VPNSettingsDataSourceDeleg alertPresenter.showAlert(presentation: presentation, animated: true) } +} - // MARK: - VPNSettingsDataSourceDelegate - +extension CustomDNSViewController: DNSSettingsDataSourceDelegate { func didChangeViewModel(_ viewModel: VPNSettingsViewModel) { interactor.updateSettings([.dnsSettings(viewModel.asDNSSettings())]) } @@ -136,16 +136,4 @@ class CustomDNSViewController: UITableViewController, VPNSettingsDataSourceDeleg showInfo(with: message) } - - func showDNSSettings() { - // No op. - } - - func showIPOverrides() { - // No op. - } - - func didSelectWireGuardPort(_ port: UInt16?) { - // No op. - } } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift index 2934e7d4571c..ae460818a9f6 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift @@ -14,6 +14,7 @@ protocol VPNSettingsCellEventHandler { func addCustomPort(_ port: UInt16) func selectCustomPortEntry(_ port: UInt16) -> Bool func selectObfuscationState(_ state: WireGuardObfuscationState) + func switchMultihop(_ state: MultihopState) } final class VPNSettingsCellFactory: CellFactoryProtocol { @@ -204,6 +205,23 @@ final class VPNSettingsCellFactory: CellFactoryProtocol { ) cell.accessibilityIdentifier = item.accessibilityIdentifier cell.applySubCellStyling() + + case .multihop: + guard let cell = cell as? SettingsSwitchCell else { return } + + cell.titleLabel.text = NSLocalizedString( + "MULTIHOP_LABEL", + tableName: "VPNSettings", + value: "Enable multihop", + comment: "" + ) + cell.accessibilityIdentifier = item.accessibilityIdentifier + cell.setOn(viewModel.multihopState.isEnabled, animated: false) + + cell.action = { [weak self] isEnabled in + let state: MultihopState = isEnabled ? .on : .off + self?.delegate?.switchMultihop(state) + } } } } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift index 85d683b587e2..da8e9cdacc51 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift @@ -23,6 +23,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case wireGuardObfuscation case wireGuardObfuscationPort case quantumResistance + case multihop var reusableViewClass: AnyClass { switch self { case .dnsSettings: @@ -39,6 +40,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< return SelectableSettingsCell.self case .quantumResistance: return SelectableSettingsCell.self + case .multihop: + return SettingsSwitchCell.self } } } @@ -58,6 +61,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case wireGuardObfuscation case wireGuardObfuscationPort case quantumResistance + case multiHop } enum Item: Hashable { @@ -72,6 +76,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< case quantumResistanceAutomatic case quantumResistanceOn case quantumResistanceOff + case multihop static var wireGuardPorts: [Item] { let defaultPorts = VPNSettingsViewModel.defaultWireGuardPorts.map { @@ -116,6 +121,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< return .quantumResistanceOn case .quantumResistanceOff: return .quantumResistanceOff + case .multihop: + return .multihopSwitch } } @@ -135,6 +142,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< return .wireGuardObfuscationPort case .quantumResistanceAutomatic, .quantumResistanceOn, .quantumResistanceOff: return .quantumResistance + case .multihop: + return .multihop } } } @@ -344,9 +353,8 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< let sectionIdentifier = snapshot().sectionIdentifiers[section] switch sectionIdentifier { - case .dnsSettings, .ipOverrides: + case .dnsSettings, .ipOverrides, .multiHop: return 0 - default: return tableView.estimatedRowHeight } @@ -358,12 +366,20 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< return switch sectionIdentifier { // 0 due to there already being a separator between .dnsSettings and .ipOverrides. case .dnsSettings: 0 - case .ipOverrides: UIMetrics.TableView.sectionSpacing - case .quantumResistance: tableView.estimatedRowHeight + case .ipOverrides, .quantumResistance: UIMetrics.TableView.sectionSpacing default: 0.5 } } + func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { + let sectionIdentifier = snapshot().sectionIdentifiers[indexPath.section] + + return switch sectionIdentifier { + case .multiHop: false + default: true + } + } + // MARK: - Private private func registerClasses() { @@ -396,6 +412,10 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< snapshot.appendItems([.dnsSettings], toSection: .dnsSettings) snapshot.appendItems([.ipOverrides], toSection: .ipOverrides) + #if DEBUG + snapshot.appendItems([.multihop], toSection: .multiHop) + #endif + applySnapshot(snapshot, animated: animated, completion: completion) } @@ -597,6 +617,22 @@ extension VPNSettingsDataSource: VPNSettingsCellEventHandler { func selectQuantumResistance(_ state: TunnelQuantumResistance) { viewModel.setQuantumResistance(state) } + + func switchMultihop(_ state: MultihopState) { + if state == .on { + delegate?.showMultihopConfirmation({ [weak self] in + guard let self else { return } + viewModel.setMultihop(state) + self.delegate?.didChangeViewModel(viewModel) + }, onDiscard: { [weak self] in + guard let self else { return } + reload(item: .multihop) + }) + } else { + viewModel.setMultihop(state) + delegate?.didChangeViewModel(viewModel) + } + } } // swiftlint:disable:this file_length diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift index 70ecf368be10..5a9a06ff059f 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift @@ -7,6 +7,12 @@ // import Foundation +import MullvadSettings + +protocol DNSSettingsDataSourceDelegate: AnyObject { + func didChangeViewModel(_ viewModel: VPNSettingsViewModel) + func showInfo(for: VPNSettingsInfoButtonItem) +} protocol VPNSettingsDataSourceDelegate: AnyObject { func didChangeViewModel(_ viewModel: VPNSettingsViewModel) @@ -14,4 +20,5 @@ protocol VPNSettingsDataSourceDelegate: AnyObject { func showDNSSettings() func showIPOverrides() func didSelectWireGuardPort(_ port: UInt16?) + func showMultihopConfirmation(_ onSave: @escaping () -> Void, onDiscard: @escaping () -> Void) } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift index 059502446691..90744b6ce1b2 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift @@ -13,7 +13,7 @@ protocol VPNSettingsViewControllerDelegate: AnyObject { func showIPOverrides() } -class VPNSettingsViewController: UITableViewController, VPNSettingsDataSourceDelegate { +class VPNSettingsViewController: UITableViewController { private let interactor: VPNSettingsInteractor private var dataSource: VPNSettingsDataSource? private let alertPresenter: AlertPresenter @@ -103,9 +103,9 @@ class VPNSettingsViewController: UITableViewController, VPNSettingsDataSourceDel } .joined(separator: ", ") } +} - // MARK: - VPNSettingsDataSourceDelegate - +extension VPNSettingsViewController: VPNSettingsDataSourceDelegate { func didChangeViewModel(_ viewModel: VPNSettingsViewModel) { interactor.updateSettings( [ @@ -114,6 +114,7 @@ class VPNSettingsViewController: UITableViewController, VPNSettingsDataSourceDel port: viewModel.obfuscationPort )), .quantumResistance(viewModel.quantumResistance), + .multihop(viewModel.multihopState), ] ) } @@ -174,7 +175,6 @@ class VPNSettingsViewController: UITableViewController, VPNSettingsDataSourceDel """, comment: "" ) - default: assertionFailure("No matching InfoButtonItem") } @@ -194,4 +194,48 @@ class VPNSettingsViewController: UITableViewController, VPNSettingsDataSourceDel func didSelectWireGuardPort(_ port: UInt16?) { interactor.setPort(port) } + + func showMultihopConfirmation(_ onSave: @escaping () -> Void, onDiscard: @escaping () -> Void) { + let presentation = AlertPresentation( + id: "multihop-confirm-alert", + accessibilityIdentifier: .multihopPromptAlert, + icon: .info, + message: NSLocalizedString( + "MULTIHOP_CONFIRM_ALERT_TEXT", + tableName: "Multihop", + value: "This setting increases latency. Use only if needed.", + comment: "" + ), + buttons: [ + AlertAction( + title: NSLocalizedString( + "MULTIHOP_CONFIRM_ALERT_ENABLE_BUTTON", + tableName: "Multihop", + value: "Enable anyway", + comment: "" + ), + style: .destructive, + accessibilityId: .multihopConfirmAlertEnableButton, + handler: { + onSave() + } + ), + AlertAction( + title: NSLocalizedString( + "MULTIHOP_CONFIRM_ALERT_BACK_BUTTON", + tableName: "Multihop", + value: "Back", + comment: "" + ), + style: .default, + accessibilityId: .multihopConfirmAlertBackButton, + handler: { + onDiscard() + } + ), + ] + ) + + alertPresenter.showAlert(presentation: presentation, animated: true) + } } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift index 3dc119362e68..85993c3dee69 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift @@ -99,6 +99,7 @@ struct VPNSettingsViewModel: Equatable { private(set) var obfuscationPort: WireGuardObfuscationPort private(set) var quantumResistance: TunnelQuantumResistance + private(set) var multihopState: MultihopState static let defaultWireGuardPorts: [UInt16] = [51820, 53] @@ -154,6 +155,10 @@ struct VPNSettingsViewModel: Equatable { quantumResistance = newState } + mutating func setMultihop(_ newState: MultihopState) { + multihopState = newState + } + /// Precondition for enabling Custom DNS. var customDNSPrecondition: CustomDNSPrecondition { if blockAdvertising || blockTracking || blockMalware || @@ -201,6 +206,7 @@ struct VPNSettingsViewModel: Equatable { obfuscationPort = tunnelSettings.wireGuardObfuscation.port quantumResistance = tunnelSettings.tunnelQuantumResistance + multihopState = tunnelSettings.tunnelMultihopState } /// Produce merged view model keeping entry `identifier` for matching DNS entries. diff --git a/ios/MullvadVPNTests/MullvadREST/Shadowsocks/ShadowsocksLoaderTests.swift b/ios/MullvadVPNTests/MullvadREST/Shadowsocks/ShadowsocksLoaderTests.swift index dbd74f0519db..da049b95cdab 100644 --- a/ios/MullvadVPNTests/MullvadREST/Shadowsocks/ShadowsocksLoaderTests.swift +++ b/ios/MullvadVPNTests/MullvadREST/Shadowsocks/ShadowsocksLoaderTests.swift @@ -20,56 +20,50 @@ class ShadowsocksLoaderTests: XCTestCase { private var relaySelector: ShadowsocksRelaySelectorStub! private var shadowsocksLoader: ShadowsocksLoader! private var relayConstraints = RelayConstraints() + private var multihopStateListener = MultihopStateListener() override func setUpWithError() throws { relayConstraintsUpdater = RelayConstraintsUpdater() shadowsocksConfigurationCache = ShadowsocksConfigurationCacheStub() relaySelector = ShadowsocksRelaySelectorStub(relays: sampleRelays) - shadowsocksLoader = ShadowsocksLoader( - cache: shadowsocksConfigurationCache, - relaySelector: relaySelector, - constraintsUpdater: relayConstraintsUpdater - ) - } - - func testLoadConfigWithMultihopDisabled() throws { - relaySelector.multihopState = .off relaySelector.exitBridgeResult = .success(try XCTUnwrap(closetRelayTo( location: relayConstraints.exitLocations, port: relayConstraints.port, filter: relayConstraints.filter, in: sampleRelays ))) - relaySelector.entryBridgeResult = .failure(ShadowsocksRelaySelectorStubError()) - - let configuration = try XCTUnwrap(shadowsocksLoader.load()) - XCTAssertEqual(configuration, try XCTUnwrap(shadowsocksConfigurationCache.read())) - } - func testLoadConfigWithMultihopEnabled() throws { - relaySelector.multihopState = .on relaySelector.entryBridgeResult = .success(try XCTUnwrap(closetRelayTo( location: relayConstraints.entryLocations, port: relayConstraints.port, filter: relayConstraints.filter, in: sampleRelays ))) - relaySelector.exitBridgeResult = .failure(ShadowsocksRelaySelectorStubError()) + shadowsocksLoader = ShadowsocksLoader( + cache: shadowsocksConfigurationCache, + relaySelector: relaySelector, + constraintsUpdater: relayConstraintsUpdater, + multihopUpdater: MultihopUpdater(listener: multihopStateListener) + ) + } + + func testLoadConfigWithMultihopDisabled() throws { + multihopStateListener.onNewMultihop?(.off) + relaySelector.entryBridgeResult = .failure(ShadowsocksRelaySelectorStubError()) let configuration = try XCTUnwrap(shadowsocksLoader.load()) XCTAssertEqual(configuration, try XCTUnwrap(shadowsocksConfigurationCache.read())) } - func testConstraintsUpdateClearsCache() throws { - relaySelector.exitBridgeResult = .success(try XCTUnwrap(closetRelayTo( - location: relayConstraints.exitLocations, - port: relayConstraints.port, - filter: relayConstraints.filter, - in: sampleRelays - ))) - relaySelector.entryBridgeResult = .failure(ShadowsocksRelaySelectorStubError()) + func testLoadConfigWithMultihopEnabled() throws { + multihopStateListener.onNewMultihop?(.on) + relaySelector.exitBridgeResult = .failure(ShadowsocksRelaySelectorStubError()) + let configuration = try XCTUnwrap(shadowsocksLoader.load()) + XCTAssertEqual(configuration, try XCTUnwrap(shadowsocksConfigurationCache.read())) + } + func testConstraintsUpdateClearsCache() throws { relayConstraints = RelayConstraints( entryLocations: .only(UserSelectedRelays(locations: [.city("ca", "tor")])), exitLocations: .only(UserSelectedRelays(locations: [.country("ae")])) @@ -80,6 +74,11 @@ class ShadowsocksLoaderTests: XCTestCase { XCTAssertNil(shadowsocksConfigurationCache.cachedConfiguration) } + func testMultihopUpdateClearsCache() throws { + multihopStateListener.onNewMultihop?(.off) + XCTAssertNil(shadowsocksConfigurationCache.cachedConfiguration) + } + private func closetRelayTo( location: RelayConstraint, port: RelayConstraint, @@ -98,15 +97,13 @@ class ShadowsocksLoaderTests: XCTestCase { private class ShadowsocksRelaySelectorStub: ShadowsocksRelaySelectorProtocol { var entryBridgeResult: Result = .failure(ShadowsocksRelaySelectorStubError()) var exitBridgeResult: Result = .failure(ShadowsocksRelaySelectorStubError()) - var multihopState: MultihopState = .off - private let relays: REST.ServerRelaysResponse init(relays: REST.ServerRelaysResponse) { self.relays = relays } - func selectRelay(with constraints: RelayConstraints) throws -> REST.BridgeRelay? { + func selectRelay(with constraints: RelayConstraints, multihopState: MultihopState) throws -> REST.BridgeRelay? { switch multihopState { case .on: try entryBridgeResult.get() diff --git a/ios/MullvadVPNUITests/Pages/MultihopPromptAlert.swift b/ios/MullvadVPNUITests/Pages/MultihopPromptAlert.swift new file mode 100644 index 000000000000..c8afb9f505f3 --- /dev/null +++ b/ios/MullvadVPNUITests/Pages/MultihopPromptAlert.swift @@ -0,0 +1,29 @@ +// +// MultihopPromptAlert.swift +// MullvadVPNUITests +// +// Created by Mojgan on 2024-06-10. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import XCTest + +class MultihopPromptAlert: Page { + @discardableResult override init(_ app: XCUIApplication) { + super.init(app) + + self.pageAccessibilityIdentifier = .multihopPromptAlert + waitForPageToBeShown() + } + + @discardableResult func tapEnableAnyway() -> Self { + app.buttons[AccessibilityIdentifier.multihopConfirmAlertEnableButton].tap() + return self + } + + @discardableResult func tapBack() -> Self { + app.buttons[AccessibilityIdentifier.multihopConfirmAlertBackButton].tap() + return self + } +} diff --git a/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift b/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift index c89a2fb04562..2545a9a5d761 100644 --- a/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift +++ b/ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift @@ -119,6 +119,23 @@ class VPNSettingsPage: Page { return self } + @discardableResult func tapMultihopSwitch() -> Self { + app.cells[AccessibilityIdentifier.multihopSwitch] + .switches[AccessibilityIdentifier.customSwitch] + .tap() + + let promptIsShown = app + .otherElements[AccessibilityIdentifier.multihopPromptAlert.rawValue] + .waitForExistence(timeout: 1.0) + + if promptIsShown { + MultihopPromptAlert(app) + .tapEnableAnyway() + } + + return self + } + @discardableResult func verifyCustomWireGuardPortSelected(portNumber: String) -> Self { let cell = app.cells[AccessibilityIdentifier.wireGuardCustomPort] XCTAssertTrue(cell.isSelected) @@ -150,4 +167,17 @@ class VPNSettingsPage: Page { XCTAssertTrue(cell.isSelected) return self } + + @discardableResult func verifyMultihopSwitchOn() -> Self { + let switchElement = app.cells[AccessibilityIdentifier.multihopSwitch] + .switches[AccessibilityIdentifier.customSwitch] + + guard let switchValue = switchElement.value as? String else { + XCTFail("Failed to read switch state") + return self + } + + XCTAssertEqual(switchValue, "1") + return self + } } diff --git a/ios/MullvadVPNUITests/SettingsMigrationTests.swift b/ios/MullvadVPNUITests/SettingsMigrationTests.swift index 22139fd555b0..f9898f703b4e 100644 --- a/ios/MullvadVPNUITests/SettingsMigrationTests.swift +++ b/ios/MullvadVPNUITests/SettingsMigrationTests.swift @@ -98,6 +98,7 @@ class SettingsMigrationTests: BaseUITestCase { .tapUDPOverTCPPortExpandButton() .tapUDPOverTCPPort80Cell() .tapUDPOverTCPPortExpandButton() + .tapMultihopSwitch() } func testVerifyCustomDNSSettingsStillChanged() { @@ -145,6 +146,7 @@ class SettingsMigrationTests: BaseUITestCase { .tapWireGuardObfuscationExpandButton() .tapUDPOverTCPPortExpandButton() .verifyUDPOverTCPPort80Selected() + .verifyMultihopSwitchOn() .tapBackButton() } } diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift index 08e9c129fe92..aae7677d9a96 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift @@ -20,11 +20,6 @@ import WireGuardKitTypes class PacketTunnelProvider: NEPacketTunnelProvider { private let internalQueue = DispatchQueue(label: "PacketTunnel-internalQueue") private let providerLogger: Logger - private let constraintsUpdater = RelayConstraintsUpdater() - private let multihopStateListener = MultihopStateListener() - - private var multihopUpdater: MultihopUpdater - private let settingsReader = SettingsReader() private var actor: PacketTunnelActor! private var postQuantumActor: PostQuantumKeyExchangeActor! @@ -34,6 +29,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider { private var adapter: WgAdapter! private var relaySelector: RelaySelectorWrapper! + private let multihopStateListener = MultihopStateListener() + private let multihopUpdater: MultihopUpdater + private let constraintsUpdater = RelayConstraintsUpdater() + override init() { Self.configureLogging() @@ -73,17 +72,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider { ) let accountsProxy = proxyFactory.createAccountsProxy() let devicesProxy = proxyFactory.createDevicesProxy() - let multihopState = (try? settingsReader.read().multihopState) ?? .off deviceChecker = DeviceChecker(accountsProxy: accountsProxy, devicesProxy: devicesProxy) relaySelector = RelaySelectorWrapper( relayCache: ipOverrideWrapper, - multihopUpdater: multihopUpdater, - multihopState: multihopState + multihopUpdater: multihopUpdater ) - multihopStateListener.onNewMultihop?(multihopState) - actor = PacketTunnelActor( timings: PacketTunnelActorTimings(), tunnelAdapter: adapter, @@ -91,7 +86,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider { defaultPathObserver: PacketTunnelPathObserver(packetTunnelProvider: self, eventQueue: internalQueue), blockedStateErrorMapper: BlockedStateErrorMapper(), relaySelector: relaySelector, - settingsReader: settingsReader, + settingsReader: TunnelSettingsManager(settingsReader: SettingsReader()) { [weak self] settings in + guard let self = self else { return } + multihopStateListener.onNewMultihop?(settings.multihopState) + constraintsUpdater.onNewConstraints?(settings.relayConstraints) + }, protocolObfuscator: ProtocolObfuscator() ) @@ -169,12 +168,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider { let urlSession = REST.makeURLSession() let urlSessionTransport = URLSessionTransport(urlSession: urlSession) let shadowsocksCache = ShadowsocksConfigurationCache(cacheDirectory: appContainerURL) - let multihopState = (try? settingsReader.read().multihopState) ?? .off let shadowsocksRelaySelector = ShadowsocksRelaySelector( - relayCache: ipOverrideWrapper, - multihopUpdater: multihopUpdater, - multihopState: multihopState + relayCache: ipOverrideWrapper ) let transportStrategy = TransportStrategy( @@ -182,7 +178,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider { shadowsocksLoader: ShadowsocksLoader( cache: shadowsocksCache, relaySelector: shadowsocksRelaySelector, - constraintsUpdater: constraintsUpdater + constraintsUpdater: constraintsUpdater, + multihopUpdater: multihopUpdater ) ) @@ -241,11 +238,6 @@ extension PacketTunnelProvider { var lastConnectionAttempt: UInt = 0 for await newState in stateStream { - // Pass relay constraints retrieved during the last read from setting into transport provider. - if let relayConstraints = newState.relayConstraints { - constraintsUpdater.onNewConstraints?(relayConstraints) - } - // Tell packet tunnel when reconnection begins. // Packet tunnel moves to `NEVPNStatus.reasserting` state once `reasserting` flag is set to `true`. if case .reconnecting = newState, !self.reasserting { diff --git a/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift b/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift index 8254ebb5fddc..8db65968a2b8 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift @@ -12,12 +12,6 @@ import MullvadSettings import MullvadTypes import PacketTunnelCore -struct MultihopNotImplementedError: LocalizedError { - public var errorDescription: String? { - "Picking relays for Multihop is not implemented yet." - } -} - final class RelaySelectorWrapper: RelaySelectorProtocol { let relayCache: RelayCacheProtocol let multihopUpdater: MultihopUpdater @@ -30,11 +24,9 @@ final class RelaySelectorWrapper: RelaySelectorProtocol { public init( relayCache: RelayCacheProtocol, - multihopUpdater: MultihopUpdater, - multihopState: MultihopState + multihopUpdater: MultihopUpdater ) { self.relayCache = relayCache - self.multihopState = multihopState self.multihopUpdater = multihopUpdater self.addObserver() } @@ -52,7 +44,7 @@ final class RelaySelectorWrapper: RelaySelectorProtocol { connectionAttemptFailureCount: UInt ) throws -> SelectedRelay { switch multihopState { - case .off: + case .off, .on: let selectorResult = try RelaySelector.WireGuard.evaluate( by: constraints, in: relayCache.read().relays, @@ -65,9 +57,6 @@ final class RelaySelectorWrapper: RelaySelectorProtocol { location: selectorResult.location, retryAttempts: connectionAttemptFailureCount ) - - case .on: - throw MultihopNotImplementedError() } } } diff --git a/ios/PacketTunnelCore/Actor/ObservedState+Extensions.swift b/ios/PacketTunnelCore/Actor/ObservedState+Extensions.swift index 08f023a2d878..f43c937e943a 100644 --- a/ios/PacketTunnelCore/Actor/ObservedState+Extensions.swift +++ b/ios/PacketTunnelCore/Actor/ObservedState+Extensions.swift @@ -10,20 +10,6 @@ import Foundation import MullvadTypes extension ObservedState { - public var relayConstraints: RelayConstraints? { - switch self { - case let .connecting(connState), let .connected(connState), let .reconnecting(connState), - let .negotiatingPostQuantumKey(connState, _): - connState.relayConstraints - - case let .error(blockedState): - blockedState.relayConstraints - - case .initial, .disconnecting, .disconnected: - nil - } - } - public var name: String { switch self { case .connected: diff --git a/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift b/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift index 05d60a23f2b2..8df91bd31bfd 100644 --- a/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift +++ b/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift @@ -24,7 +24,7 @@ public protocol SettingsReaderProtocol { } /// Struct holding settings necessary to configure packet tunnel adapter. -public struct Settings { +public struct Settings: Equatable { /// Private key used by device. public var privateKey: PrivateKey @@ -65,11 +65,23 @@ public struct Settings { } /// Enum describing selected DNS servers option. -public enum SelectedDNSServers { +public enum SelectedDNSServers: Equatable { /// Custom DNS servers. case custom([IPAddress]) /// Mullvad server acting as a blocking DNS proxy. case blocking(IPAddress) /// Gateway IP will be used as DNS automatically. case gateway + + public static func == (lhs: SelectedDNSServers, rhs: SelectedDNSServers) -> Bool { + return switch (lhs, rhs) { + case let (.custom(lhsAddresss), .custom(rhsAddresses)): + lhsAddresss.map { $0.rawValue } == rhsAddresses.map { $0.rawValue } + case let (.blocking(lhsAddress), .blocking(rhsAddress)): + lhsAddress.rawValue == rhsAddress.rawValue + case (.gateway, .gateway): + true + default: false + } + } } diff --git a/ios/PacketTunnelCore/Actor/TunnelSettingsManager.swift b/ios/PacketTunnelCore/Actor/TunnelSettingsManager.swift new file mode 100644 index 000000000000..378efbd0e961 --- /dev/null +++ b/ios/PacketTunnelCore/Actor/TunnelSettingsManager.swift @@ -0,0 +1,23 @@ +// +// TunnelSettingsManager.swift +// PacketTunnelCore +// +// Created by Mojgan on 2024-06-05. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +public struct TunnelSettingsManager: SettingsReaderProtocol { + let settingsReader: SettingsReaderProtocol + let onLoadSettingsHandler: ((Settings) -> Void)? + + public init(settingsReader: SettingsReaderProtocol, onLoadSettingsHandler: ((Settings) -> Void)? = nil) { + self.settingsReader = settingsReader + self.onLoadSettingsHandler = onLoadSettingsHandler + } + + public func read() throws -> Settings { + let settings = try settingsReader.read() + onLoadSettingsHandler?(settings) + return settings + } +} diff --git a/ios/PacketTunnelCoreTests/TunnelSettingsManagerTests.swift b/ios/PacketTunnelCoreTests/TunnelSettingsManagerTests.swift new file mode 100644 index 000000000000..844a2faa8165 --- /dev/null +++ b/ios/PacketTunnelCoreTests/TunnelSettingsManagerTests.swift @@ -0,0 +1,28 @@ +// +// TunnelSettingsManagerTests.swift +// PacketTunnelCoreTests +// +// Created by Mojgan on 2024-06-10. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +@testable import MullvadSettings +import MullvadTypes +import PacketTunnelCore +import XCTest + +class TunnelSettingsManagerTests: XCTestCase { + func notifyWhenSettingsLoadedTest() throws { + var loadedConfiguration: Settings? + let tunnelSettingsManager = TunnelSettingsManager( + settingsReader: SettingsReaderStub.staticConfiguration(), + onLoadSettingsHandler: { settings in + loadedConfiguration = settings + } + ) + + let mock = try XCTUnwrap(tunnelSettingsManager.read()) + XCTAssertEqual(loadedConfiguration, mock) + } +}