summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2024-06-13 14:06:50 +0200
committerBug Magnet <marco.nikic@mullvad.net>2024-06-13 14:06:50 +0200
commit57d678c5e2fb170305bbcb32a67533fa335af731 (patch)
tree671383b980dcfa9ad4de4ae22b95f3ef3ae05512
parent889af23e04e9f26e501f392c21472c14b10ee3e0 (diff)
parent1d03677f7835f38dc78307cec1da50e6cf88f773 (diff)
downloadmullvadvpn-57d678c5e2fb170305bbcb32a67533fa335af731.tar.xz
mullvadvpn-57d678c5e2fb170305bbcb32a67533fa335af731.zip
Merge branch 'add-multihop-toggle-to-settings-view-ios-687'
-rw-r--r--ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift29
-rw-r--r--ios/MullvadREST/Transport/Shadowsocks/ShadowsocksRelaySelector.swift27
-rw-r--r--ios/MullvadSettings/MultihopSettings.swift4
-rw-r--r--ios/MullvadSettings/TunnelSettingsUpdate.swift2
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj34
-rw-r--r--ios/MullvadVPN/AppDelegate.swift14
-rw-r--r--ios/MullvadVPN/Classes/AccessbilityIdentifier.swift6
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/CustomDNSDataSource.swift2
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/CustomDNSViewController.swift18
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift18
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift44
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSourceDelegate.swift7
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift52
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewModel.swift6
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Shadowsocks/ShadowsocksLoaderTests.swift53
-rw-r--r--ios/MullvadVPNUITests/Pages/MultihopPromptAlert.swift29
-rw-r--r--ios/MullvadVPNUITests/Pages/VPNSettingsPage.swift30
-rw-r--r--ios/MullvadVPNUITests/SettingsMigrationTests.swift2
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift34
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift15
-rw-r--r--ios/PacketTunnelCore/Actor/ObservedState+Extensions.swift14
-rw-r--r--ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift16
-rw-r--r--ios/PacketTunnelCore/Actor/TunnelSettingsManager.swift23
-rw-r--r--ios/PacketTunnelCoreTests/TunnelSettingsManagerTests.swift28
24 files changed, 362 insertions, 145 deletions
diff --git a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift
index f079fbf860..f919805665 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 2c9efa5ca2..2519c4065a 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 881324e792..ba64c12e48 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 92349a38ed..5ab46b8413 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 bb540b34be..60b89d512d 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 = "<group>"; };
F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncreasedHitButton.swift; sourceTree = "<group>"; };
F04413602BA45CD70018A6EE /* CustomListLocationNodeBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomListLocationNodeBuilder.swift; sourceTree = "<group>"; };
+ F04DD3D72C130DF600E03E28 /* TunnelSettingsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelSettingsManager.swift; sourceTree = "<group>"; };
F04F95A02B21D24400431E08 /* shadowsocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = shadowsocks.h; sourceTree = "<group>"; };
F04FBE602A8379EE009278D7 /* AppPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPreferences.swift; sourceTree = "<group>"; };
F050AE4D2B70D7F8003F4EDB /* LocationCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationCellViewModel.swift; sourceTree = "<group>"; };
@@ -2115,6 +2119,7 @@
F06045E52B231EB700B2D37A /* URLSessionTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionTransport.swift; sourceTree = "<group>"; };
F06045E92B23217E00B2D37A /* ShadowsocksTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksTransport.swift; sourceTree = "<group>"; };
F06045EB2B2322A500B2D37A /* Jittered.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Jittered.swift; sourceTree = "<group>"; };
+ F062B94C2C16E09700B6D47A /* TunnelSettingsManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsManagerTests.swift; sourceTree = "<group>"; };
F072D3CE2C07122400906F64 /* MultihopUpdaterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopUpdaterTests.swift; sourceTree = "<group>"; };
F072D3D12C071AD100906F64 /* ShadowsocksLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksLoaderTests.swift; sourceTree = "<group>"; };
F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextFormatterTests.swift; sourceTree = "<group>"; };
@@ -2147,6 +2152,7 @@
F0DA87462A9CB9A2006044F1 /* AccountExpiryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryRow.swift; sourceTree = "<group>"; };
F0DA87482A9CBA9F006044F1 /* AccountDeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeviceRow.swift; sourceTree = "<group>"; };
F0DA874A2A9CBACB006044F1 /* AccountNumberRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountNumberRow.swift; sourceTree = "<group>"; };
+ F0DAC8AE2C1712C300F80144 /* MultihopPromptAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopPromptAlert.swift; sourceTree = "<group>"; };
F0DDE40F2B220458006B57A7 /* ShadowSocksProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowSocksProxy.swift; sourceTree = "<group>"; };
F0DDE4102B220458006B57A7 /* ShadowsocksConfigurationCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksConfigurationCache.swift; sourceTree = "<group>"; };
F0DDE4112B220458006B57A7 /* TransportProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransportProvider.swift; sourceTree = "<group>"; };
@@ -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 = "<group>";
@@ -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 = "<group>";
@@ -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 = "<group>";
@@ -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 645acdeb0f..fc6c746cd6 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 2f773a880d..d434d39b71 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 fc559499a0..a2bce16dac 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 df109d3ff3..34fdf670d8 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 2934e7d457..ae460818a9 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 85d683b587..da8e9cdacc 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 70ecf368be..5a9a06ff05 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 0595024466..90744b6ce1 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 3dc119362e..85993c3dee 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 dbd74f0519..da049b95cd 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<UserSelectedRelays>,
port: RelayConstraint<UInt16>,
@@ -98,15 +97,13 @@ class ShadowsocksLoaderTests: XCTestCase {
private class ShadowsocksRelaySelectorStub: ShadowsocksRelaySelectorProtocol {
var entryBridgeResult: Result<REST.BridgeRelay, Error> = .failure(ShadowsocksRelaySelectorStubError())
var exitBridgeResult: Result<REST.BridgeRelay, Error> = .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 0000000000..c8afb9f505
--- /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 c89a2fb045..2545a9a5d7 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 22139fd555..f9898f703b 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 08e9c129fe..aae7677d9a 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<UDPOverTCPObfuscator>()
)
@@ -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 8254ebb5fd..8db65968a2 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 08f023a2d8..f43c937e94 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 05d60a23f2..8df91bd31b 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 0000000000..378efbd0e9
--- /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 0000000000..844a2faa81
--- /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)
+ }
+}