From e6f0fccede55db61c86d9470e85ebe7693541368 Mon Sep 17 00:00:00 2001 From: Jon Petersson Date: Wed, 12 Jun 2024 09:48:19 +0200 Subject: [PATCH] Add RelaySelectorWrapper tests --- .../Relay/MultihopDecisionFlow.swift | 116 ++++++++++ ios/MullvadREST/Relay/RelayPicking.swift | 107 ++++++++++ .../Relay/RelaySelectorPicker.swift | 200 ------------------ .../Relay/RelaySelectorWrapper.swift | 4 +- ios/MullvadREST/Relay/RelayWithLocation.swift | 2 +- .../Shadowsocks/ShadowsocksLoader.swift | 4 +- ios/MullvadVPN.xcodeproj/project.pbxproj | 33 ++- .../xcshareddata/swiftpm/Package.resolved | 22 ++ ios/MullvadVPN/AppDelegate.swift | 6 +- .../Relay/MultihopDecisionFlowTests.swift | 156 ++++++++++++++ .../MullvadREST/Relay/RelayPickingTests.swift | 68 ++++++ .../Relay/RelaySelectorWrapperTests.swift | 55 +++++ .../PacketTunnelActorReducerTests.swift | 1 - .../RelaySelectorWrapper.swift | 62 ------ 14 files changed, 553 insertions(+), 283 deletions(-) create mode 100644 ios/MullvadREST/Relay/MultihopDecisionFlow.swift create mode 100644 ios/MullvadREST/Relay/RelayPicking.swift delete mode 100644 ios/MullvadREST/Relay/RelaySelectorPicker.swift create mode 100644 ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift create mode 100644 ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift create mode 100644 ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift delete mode 100644 ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift diff --git a/ios/MullvadREST/Relay/MultihopDecisionFlow.swift b/ios/MullvadREST/Relay/MultihopDecisionFlow.swift new file mode 100644 index 000000000000..fa8431ed9901 --- /dev/null +++ b/ios/MullvadREST/Relay/MultihopDecisionFlow.swift @@ -0,0 +1,116 @@ +// +// MultihopDecisionFlow.swift +// MullvadREST +// +// Created by Jon Petersson on 2024-06-14. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +protocol MultihopDecisionFlow { + typealias RelayCandidate = RelayWithLocation + init(next: MultihopDecisionFlow?, relayPicker: RelayPicking) + func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool + func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays +} + +struct OneToOne: MultihopDecisionFlow { + let next: MultihopDecisionFlow? + let relayPicker: RelayPicking + init(next: (any MultihopDecisionFlow)?, relayPicker: RelayPicking) { + self.next = next + self.relayPicker = relayPicker + } + + func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays { + guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else { + guard let next else { + throw NoRelaysSatisfyingConstraintsError() + } + return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) + } + + guard entryCandidates.first != exitCandidates.first else { + throw NoRelaysSatisfyingConstraintsError() + } + + let entryMatch = try relayPicker.findBestMatch(from: entryCandidates) + let exitMatch = try relayPicker.findBestMatch(from: exitCandidates) + return SelectedRelays(entry: entryMatch, exit: exitMatch) + } + + func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { + entryCandidates.count == 1 && exitCandidates.count == 1 + } +} + +struct OneToMany: MultihopDecisionFlow { + let next: MultihopDecisionFlow? + let relayPicker: RelayPicking + + init(next: (any MultihopDecisionFlow)?, relayPicker: RelayPicking) { + self.next = next + self.relayPicker = relayPicker + } + + func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays { + guard let multihopPicker = relayPicker as? MultihopPicker else { + fatalError("Could not cast picker to MultihopPicker") + } + + guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else { + guard let next else { + throw NoRelaysSatisfyingConstraintsError() + } + return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) + } + + switch (entryCandidates.count, exitCandidates.count) { + case let (1, count) where count > 1: + let entryMatch = try multihopPicker.findBestMatch(from: entryCandidates) + let exitMatch = try multihopPicker.exclude(relay: entryMatch, from: exitCandidates) + return SelectedRelays(entry: entryMatch, exit: exitMatch) + default: + let exitMatch = try multihopPicker.findBestMatch(from: exitCandidates) + let entryMatch = try multihopPicker.exclude(relay: exitMatch, from: entryCandidates) + return SelectedRelays(entry: entryMatch, exit: exitMatch) + } + } + + func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { + (entryCandidates.count == 1 && exitCandidates.count > 1) || + (entryCandidates.count > 1 && exitCandidates.count == 1) + } +} + +struct ManyToMany: MultihopDecisionFlow { + let next: MultihopDecisionFlow? + let relayPicker: RelayPicking + + init(next: (any MultihopDecisionFlow)?, relayPicker: RelayPicking) { + self.next = next + self.relayPicker = relayPicker + } + + func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays { + guard let multihopPicker = relayPicker as? MultihopPicker else { + fatalError("Could not cast picker to MultihopPicker") + } + + guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else { + guard let next else { + throw NoRelaysSatisfyingConstraintsError() + } + return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) + } + + let exitMatch = try multihopPicker.findBestMatch(from: exitCandidates) + let entryMatch = try multihopPicker.exclude(relay: exitMatch, from: entryCandidates) + return SelectedRelays(entry: entryMatch, exit: exitMatch) + } + + func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { + entryCandidates.count > 1 && exitCandidates.count > 1 + } +} diff --git a/ios/MullvadREST/Relay/RelayPicking.swift b/ios/MullvadREST/Relay/RelayPicking.swift new file mode 100644 index 000000000000..eec1003a1ca5 --- /dev/null +++ b/ios/MullvadREST/Relay/RelayPicking.swift @@ -0,0 +1,107 @@ +// +// RelaySelectorPicker.swift +// MullvadREST +// +// Created by Jon Petersson on 2024-06-05. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import MullvadSettings +import MullvadTypes + +protocol RelayPicking { + var relays: REST.ServerRelaysResponse { get } + var constraints: RelayConstraints { get } + var connectionAttemptCount: UInt { get } + func pick() throws -> SelectedRelays +} + +extension RelayPicking { + func findBestMatch( + from candidates: [RelayWithLocation] + ) throws -> SelectedRelay { + let match = try RelaySelector.WireGuard.pickCandidate( + from: candidates, + relays: relays, + portConstraint: constraints.port, + numberOfFailedAttempts: connectionAttemptCount + ) + + return SelectedRelay( + endpoint: match.endpoint, + hostname: match.relay.hostname, + location: match.location, + retryAttempts: connectionAttemptCount + ) + } +} + +struct SinglehopPicker: RelayPicking { + let constraints: RelayConstraints + let relays: REST.ServerRelaysResponse + let connectionAttemptCount: UInt + + func pick() throws -> SelectedRelays { + let candidates = try RelaySelector.WireGuard.findCandidates( + by: constraints.exitLocations, + in: relays, + filterConstraint: constraints.filter + ) + + let match = try findBestMatch(from: candidates) + + return SelectedRelays(entry: nil, exit: match) + } +} + +struct MultihopPicker: RelayPicking { + let constraints: RelayConstraints + let relays: REST.ServerRelaysResponse + let connectionAttemptCount: UInt + + func pick() throws -> SelectedRelays { + let entryCandidates = try RelaySelector.WireGuard.findCandidates( + by: constraints.entryLocations, + in: relays, + filterConstraint: constraints.filter + ) + + let exitCandidates = try RelaySelector.WireGuard.findCandidates( + by: constraints.exitLocations, + in: relays, + filterConstraint: constraints.filter + ) + + /* + Relay selection is prioritised in the following order: + 1. Both entry and exit constraints match only a single relay. Both relays are selected. + 2. Either entry or exit constraint matches only a single relay and the other multiple relays. The single relays + is selected and excluded from the list of multiple relays. + 3. Both entry and exit constraints match multiple relays. Exit relay is picked first and then excluded from + the list of entry relays. + */ + let decisionFlow = OneToOne( + next: OneToMany( + next: ManyToMany( + next: nil, + relayPicker: self + ), + relayPicker: self + ), + relayPicker: self + ) + + return try decisionFlow.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) + } + + func exclude( + relay: SelectedRelay, + from candidates: [RelayWithLocation] + ) throws -> SelectedRelay { + let filteredCandidates = candidates.filter { relayWithLocation in + relayWithLocation.relay.hostname != relay.hostname + } + + return try findBestMatch(from: filteredCandidates) + } +} diff --git a/ios/MullvadREST/Relay/RelaySelectorPicker.swift b/ios/MullvadREST/Relay/RelaySelectorPicker.swift deleted file mode 100644 index 2ca41cb30e8c..000000000000 --- a/ios/MullvadREST/Relay/RelaySelectorPicker.swift +++ /dev/null @@ -1,200 +0,0 @@ -// -// RelaySelectorPicker.swift -// MullvadREST -// -// Created by Jon Petersson on 2024-06-05. -// Copyright © 2024 Mullvad VPN AB. All rights reserved. -// - -import MullvadSettings -import MullvadTypes - -protocol RelaySelectorPicker { - var relays: REST.ServerRelaysResponse { get } - var constraints: RelayConstraints { get } - var connectionAttemptCount: UInt { get } - func pick() throws -> SelectedRelays -} - -extension RelaySelectorPicker { - func findBestMatch( - from candidates: [RelayWithLocation] - ) throws -> SelectedRelay { - let match = try RelaySelector.WireGuard.pickCandidate( - from: candidates, - relays: relays, - portConstraint: constraints.port, - numberOfFailedAttempts: connectionAttemptCount - ) - - return SelectedRelay( - endpoint: match.endpoint, - hostname: match.relay.hostname, - location: match.location, - retryAttempts: connectionAttemptCount - ) - } -} - -struct SinglehopPicker: RelaySelectorPicker { - let constraints: RelayConstraints - let relays: REST.ServerRelaysResponse - let connectionAttemptCount: UInt - - func pick() throws -> SelectedRelays { - let candidates = try RelaySelector.WireGuard.findCandidates( - by: constraints.exitLocations, - in: relays, - filterConstraint: constraints.filter - ) - - let match = try findBestMatch(from: candidates) - - return SelectedRelays(entry: nil, exit: match) - } -} - -struct MultihopPicker: RelaySelectorPicker { - let constraints: RelayConstraints - let relays: REST.ServerRelaysResponse - let connectionAttemptCount: UInt - - func pick() throws -> SelectedRelays { - let entryCandidates = try RelaySelector.WireGuard.findCandidates( - by: constraints.entryLocations, - in: relays, - filterConstraint: constraints.filter - ) - - let exitCandidates = try RelaySelector.WireGuard.findCandidates( - by: constraints.exitLocations, - in: relays, - filterConstraint: constraints.filter - ) - - let decisionChain = OneToOne( - next: OneToMany(next: ManyToMany(next: nil, relaySelectorPicker: self), relaySelectorPicker: self), - relaySelectorPicker: self - ) - - return try decisionChain.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) - } - - func exclude( - relay: SelectedRelay, - from candidates: [RelayWithLocation] - ) throws -> SelectedRelay { - let filteredCandidates = candidates.filter { relayWithLocation in - relayWithLocation.serverLocation != relay.location - } - - return try findBestMatch(from: filteredCandidates) - } -} - -protocol MultihopDescionMaker { - typealias RelayCandidate = RelayWithLocation - init(next: MultihopDescionMaker?, relaySelectorPicker: RelaySelectorPicker) - func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool - func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays -} - -private struct OneToOne: MultihopDescionMaker { - let next: MultihopDescionMaker? - let relaySelectorPicker: RelaySelectorPicker - init(next: (any MultihopDescionMaker)?, relaySelectorPicker: RelaySelectorPicker) { - self.next = next - self.relaySelectorPicker = relaySelectorPicker - } - - func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays { - guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else { - guard let next else { - throw NoRelaysSatisfyingConstraintsError() - } - return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) - } - - guard entryCandidates.first != exitCandidates.first else { - throw NoRelaysSatisfyingConstraintsError() - } - - let entryMatch = try relaySelectorPicker.findBestMatch(from: entryCandidates) - let exitMatch = try relaySelectorPicker.findBestMatch(from: exitCandidates) - return SelectedRelays(entry: entryMatch, exit: exitMatch) - } - - func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { - entryCandidates.count == 1 && exitCandidates.count == 1 - } -} - -private struct OneToMany: MultihopDescionMaker { - let next: MultihopDescionMaker? - let relaySelectorPicker: RelaySelectorPicker - - init(next: (any MultihopDescionMaker)?, relaySelectorPicker: RelaySelectorPicker) { - self.next = next - self.relaySelectorPicker = relaySelectorPicker - } - - func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays { - guard let multihopPicker = relaySelectorPicker as? MultihopPicker else { - fatalError("Could not cast picker to MultihopPicker") - } - - guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else { - guard let next else { - throw NoRelaysSatisfyingConstraintsError() - } - return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) - } - - switch (entryCandidates.count, exitCandidates.count) { - case let (1, count) where count > 1: - let entryMatch = try multihopPicker.findBestMatch(from: entryCandidates) - let exitMatch = try multihopPicker.exclude(relay: entryMatch, from: exitCandidates) - return SelectedRelays(entry: entryMatch, exit: exitMatch) - default: - let exitMatch = try multihopPicker.findBestMatch(from: exitCandidates) - let entryMatch = try multihopPicker.exclude(relay: exitMatch, from: entryCandidates) - return SelectedRelays(entry: entryMatch, exit: exitMatch) - } - } - - func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { - (entryCandidates.count == 1 && exitCandidates.count > 1) || - (entryCandidates.count > 1 && exitCandidates.count == 1) - } -} - -private struct ManyToMany: MultihopDescionMaker { - let next: MultihopDescionMaker? - let relaySelectorPicker: RelaySelectorPicker - - init(next: (any MultihopDescionMaker)?, relaySelectorPicker: RelaySelectorPicker) { - self.next = next - self.relaySelectorPicker = relaySelectorPicker - } - - func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays { - guard let multihopPicker = relaySelectorPicker as? MultihopPicker else { - fatalError("Could not cast picker to MultihopPicker") - } - - guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else { - guard let next else { - throw NoRelaysSatisfyingConstraintsError() - } - return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) - } - - let exitMatch = try multihopPicker.findBestMatch(from: exitCandidates) - let entryMatch = try multihopPicker.exclude(relay: exitMatch, from: entryCandidates) - return SelectedRelays(entry: entryMatch, exit: exitMatch) - } - - func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { - entryCandidates.count > 1 && exitCandidates.count > 1 - } -} diff --git a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift index eed453e16363..3ee447d0ab59 100644 --- a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift +++ b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift @@ -21,11 +21,9 @@ public 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() diff --git a/ios/MullvadREST/Relay/RelayWithLocation.swift b/ios/MullvadREST/Relay/RelayWithLocation.swift index e6cdcac8afe1..0cba62661bc0 100644 --- a/ios/MullvadREST/Relay/RelayWithLocation.swift +++ b/ios/MullvadREST/Relay/RelayWithLocation.swift @@ -32,6 +32,6 @@ public struct RelayWithLocation { extension RelayWithLocation: Equatable { public static func == (lhs: RelayWithLocation, rhs: RelayWithLocation) -> Bool { - lhs.serverLocation == rhs.serverLocation + lhs.relay.hostname == rhs.relay.hostname } } diff --git a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift index f91980566520..2b46571bc820 100644 --- a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift +++ b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift @@ -33,14 +33,12 @@ public class ShadowsocksLoader: ShadowsocksLoaderProtocol { cache: ShadowsocksConfigurationCacheProtocol, relaySelector: ShadowsocksRelaySelectorProtocol, constraintsUpdater: RelayConstraintsUpdater, - multihopUpdater: MultihopUpdater, - multihopState: MultihopState = .off + multihopUpdater: MultihopUpdater ) { self.cache = cache self.relaySelector = relaySelector self.constraintsUpdater = constraintsUpdater self.multihopUpdater = multihopUpdater - self.multihopState = multihopState self.addObservers() } diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 1e25f622903d..b039c4cb0f24 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -486,20 +486,20 @@ 7A3353912AAA014400F0A71C /* SimulatorVPNConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */; }; 7A3353932AAA089000F0A71C /* SimulatorTunnelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */; }; 7A3353972AAA0F8600F0A71C /* OperationBlockObserverSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */; }; + 7A3AD5012C1068A800E9AD90 /* RelayPicking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3AD5002C1068A800E9AD90 /* RelayPicking.swift */; }; 7A3EFAAB2BDFDAE800318736 /* RelaySelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3EFAAA2BDFDAE800318736 /* RelaySelection.swift */; }; - 7A3AD5012C1068A800E9AD90 /* RelaySelectorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3AD5002C1068A800E9AD90 /* RelaySelectorPicker.swift */; }; 7A3FD1B52AD4465A0042BEA6 /* AppMessageHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */; }; 7A3FD1B72AD54ABD0042BEA6 /* AnyTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB982A98F4ED00F578F2 /* AnyTransport.swift */; }; 7A3FD1B82AD54AE60042BEA6 /* TimeServerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB9A2A98F58600F578F2 /* TimeServerProxy.swift */; }; 7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */; }; 7A45CFC62C05FF6A00D80B21 /* ScreenshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A45CFC22C05FF2F00D80B21 /* ScreenshotTests.swift */; }; 7A45CFC72C071DD400D80B21 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D0C79D23F1CEBA00FE9BA7 /* SnapshotHelper.swift */; }; - 7A4D849D2C0F289400687980 /* RelaySelectorWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */; }; 7A4D849E2C0F289800687980 /* RelaySelectorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */; }; 7A516C2E2B6D357500BBD33D /* URL+Scoping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A516C2D2B6D357500BBD33D /* URL+Scoping.swift */; }; 7A516C3A2B7111A700BBD33D /* IPOverrideWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A516C392B7111A700BBD33D /* IPOverrideWrapper.swift */; }; 7A516C3C2B712F0B00BBD33D /* IPOverrideWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A516C3B2B712F0B00BBD33D /* IPOverrideWrapperTests.swift */; }; 7A52F96A2C1735AE00B133B9 /* RelaySelectorStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */; }; + 7A52F96C2C17450C00B133B9 /* RelaySelectorWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A52F96B2C17450C00B133B9 /* RelaySelectorWrapperTests.swift */; }; 7A5869952B32E9C700640D27 /* LinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869942B32E9C700640D27 /* LinkButton.swift */; }; 7A5869972B32EA4500640D27 /* AppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869962B32EA4500640D27 /* AppButton.swift */; }; 7A58699B2B482FE200640D27 /* UITableViewCell+Disable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A58699A2B482FE200640D27 /* UITableViewCell+Disable.swift */; }; @@ -598,6 +598,9 @@ 7AC8A3AE2ABC6FBB00DC4939 /* SettingsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC8A3AD2ABC6FBB00DC4939 /* SettingsHeaderView.swift */; }; 7AC8A3AF2ABC71D600DC4939 /* TermsOfServiceCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCA22A96302700DD6A34 /* TermsOfServiceCoordinator.swift */; }; 7ACD79392C0DAADD00DBEE14 /* AddCustomListLocationsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACD79382C0DAADC00DBEE14 /* AddCustomListLocationsPage.swift */; }; + 7ACE19112C1C349200260BB6 /* MultihopDecisionFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACE19102C1C349200260BB6 /* MultihopDecisionFlow.swift */; }; + 7ACE19132C1C352100260BB6 /* RelayPickingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACE19122C1C352100260BB6 /* RelayPickingTests.swift */; }; + 7ACE19152C1C429A00260BB6 /* MultihopDecisionFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACE19142C1C429A00260BB6 /* MultihopDecisionFlowTests.swift */; }; 7AD0AA1C2AD6A63F00119E10 /* PacketTunnelActorStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD0AA1B2AD6A63F00119E10 /* PacketTunnelActorStub.swift */; }; 7AD0AA1D2AD6A86700119E10 /* PacketTunnelActorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD0AA192AD69B6E00119E10 /* PacketTunnelActorProtocol.swift */; }; 7AD0AA1F2AD6C8B900119E10 /* URLRequestProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD0AA1E2AD6C8B900119E10 /* URLRequestProxyProtocol.swift */; }; @@ -607,6 +610,7 @@ 7AE044BB2A935726003915D8 /* Routing.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A88DCD02A8FABBE00D2FF0E /* Routing.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7AE2414A2C20682B0076CE33 /* FormsheetPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE241482C20682B0076CE33 /* FormsheetPresentationController.swift */; }; 7AE90B682C2D726000375A60 /* NSParagraphStyle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE90B672C2D726000375A60 /* NSParagraphStyle+Extensions.swift */; }; + 7AEBA52A2C2179F20018BEC5 /* RelaySelectorWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEBA5292C2179F20018BEC5 /* RelaySelectorWrapper.swift */; }; 7AEBA52C2C22C65B0018BEC5 /* TimeInterval+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEBA52B2C22C65B0018BEC5 /* TimeInterval+Timeout.swift */; }; 7AED35CC2BD13F60002A67D1 /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; 7AED35CD2BD13FC4002A67D1 /* ApplicationTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C76A072A33850E00100D75 /* ApplicationTarget.swift */; }; @@ -1529,7 +1533,6 @@ 5824030C2A811B0000163DE8 /* State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = State.swift; sourceTree = ""; }; 582403162A821FD700163DE8 /* TunnelDeviceInfoProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelDeviceInfoProtocol.swift; sourceTree = ""; }; 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorProtocol.swift; sourceTree = ""; }; - 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorWrapper.swift; sourceTree = ""; }; 5827B08F2B0CAA0500CCBBA1 /* EditAccessMethodCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessMethodCoordinator.swift; sourceTree = ""; }; 5827B0912B0CAB2800CCBBA1 /* MethodSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSettingsViewController.swift; sourceTree = ""; }; 5827B0952B0DB2C100CCBBA1 /* MethodSettingsItemIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSettingsItemIdentifier.swift; sourceTree = ""; }; @@ -1893,14 +1896,15 @@ 7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorVPNConnection.swift; sourceTree = ""; }; 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelInfo.swift; sourceTree = ""; }; 7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationBlockObserverSupport.swift; sourceTree = ""; }; + 7A3AD5002C1068A800E9AD90 /* RelayPicking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPicking.swift; sourceTree = ""; }; 7A3EFAAA2BDFDAE800318736 /* RelaySelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelection.swift; sourceTree = ""; }; - 7A3AD5002C1068A800E9AD90 /* RelaySelectorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorPicker.swift; sourceTree = ""; }; 7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandlerTests.swift; sourceTree = ""; }; 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = ""; }; 7A45CFC22C05FF2F00D80B21 /* ScreenshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotTests.swift; sourceTree = ""; }; 7A516C2D2B6D357500BBD33D /* URL+Scoping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Scoping.swift"; sourceTree = ""; }; 7A516C392B7111A700BBD33D /* IPOverrideWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideWrapper.swift; sourceTree = ""; }; 7A516C3B2B712F0B00BBD33D /* IPOverrideWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideWrapperTests.swift; sourceTree = ""; }; + 7A52F96B2C17450C00B133B9 /* RelaySelectorWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorWrapperTests.swift; sourceTree = ""; }; 7A5869942B32E9C700640D27 /* LinkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkButton.swift; sourceTree = ""; }; 7A5869962B32EA4500640D27 /* AppButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppButton.swift; sourceTree = ""; }; 7A58699A2B482FE200640D27 /* UITableViewCell+Disable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Disable.swift"; sourceTree = ""; }; @@ -1987,6 +1991,9 @@ 7ABFB09D2BA316220074A49E /* RelayConstraintsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConstraintsTests.swift; sourceTree = ""; }; 7AC8A3AD2ABC6FBB00DC4939 /* SettingsHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsHeaderView.swift; sourceTree = ""; }; 7ACD79382C0DAADC00DBEE14 /* AddCustomListLocationsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCustomListLocationsPage.swift; sourceTree = ""; }; + 7ACE19102C1C349200260BB6 /* MultihopDecisionFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopDecisionFlow.swift; sourceTree = ""; }; + 7ACE19122C1C352100260BB6 /* RelayPickingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPickingTests.swift; sourceTree = ""; }; + 7ACE19142C1C429A00260BB6 /* MultihopDecisionFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopDecisionFlowTests.swift; sourceTree = ""; }; 7AD0AA192AD69B6E00119E10 /* PacketTunnelActorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelActorProtocol.swift; sourceTree = ""; }; 7AD0AA1B2AD6A63F00119E10 /* PacketTunnelActorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelActorStub.swift; sourceTree = ""; }; 7AD0AA1E2AD6C8B900119E10 /* URLRequestProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestProxyProtocol.swift; sourceTree = ""; }; @@ -1995,6 +2002,7 @@ 7ADCB2D92B6A730400C88F89 /* IPOverrideRepositoryStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideRepositoryStub.swift; sourceTree = ""; }; 7AE241482C20682B0076CE33 /* FormsheetPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormsheetPresentationController.swift; sourceTree = ""; }; 7AE90B672C2D726000375A60 /* NSParagraphStyle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSParagraphStyle+Extensions.swift"; sourceTree = ""; }; + 7AEBA5292C2179F20018BEC5 /* RelaySelectorWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelaySelectorWrapper.swift; sourceTree = ""; }; 7AEBA52B2C22C65B0018BEC5 /* TimeInterval+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Timeout.swift"; sourceTree = ""; }; 7AEF7F192AD00F52006FE45D /* AppMessageHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandler.swift; sourceTree = ""; }; 7AF10EB12ADE859200C090B9 /* AlertViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = ""; }; @@ -2514,8 +2522,11 @@ isa = PBXGroup; children = ( A9EC20E72A5D3A8C0040D56E /* CoordinatesTests.swift */, + 7ACE19142C1C429A00260BB6 /* MultihopDecisionFlowTests.swift */, A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */, + 7ACE19122C1C352100260BB6 /* RelayPickingTests.swift */, 584B26F3237434D00073B10E /* RelaySelectorTests.swift */, + 7A52F96B2C17450C00B133B9 /* RelaySelectorWrapperTests.swift */, ); path = Relay; sourceTree = ""; @@ -3803,7 +3814,6 @@ 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */, 58225D272A84F23B0083D7F1 /* PacketTunnelPathObserver.swift */, 58F3F3692AA08E3C00D3B0A4 /* PacketTunnelProvider.swift */, - 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */, 5864AF7C2A9F4DC9008BC928 /* SettingsReader.swift */, ); path = PacketTunnelProvider; @@ -4205,17 +4215,18 @@ F0DDE4272B220A15006B57A7 /* Haversine.swift */, 7A516C392B7111A700BBD33D /* IPOverrideWrapper.swift */, F0DDE4292B220A15006B57A7 /* Midpoint.swift */, + 7ACE19102C1C349200260BB6 /* MultihopDecisionFlow.swift */, F0F3161A2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift */, 5820675A26E6576800655B05 /* RelayCache.swift */, + 7A3AD5002C1068A800E9AD90 /* RelayPicking.swift */, F0DDE4282B220A15006B57A7 /* RelaySelector.swift */, F0B894F42BF7528700817A42 /* RelaySelector+Shadowsocks.swift */, F0B894F22BF7526700817A42 /* RelaySelector+Wireguard.swift */, 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */, F0F316182BF3572B0078DBCF /* RelaySelectorResult.swift */, - 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */, + 7AEBA5292C2179F20018BEC5 /* RelaySelectorWrapper.swift */, F0B894F02BF751E300817A42 /* RelayWithDistance.swift */, F0B894EE2BF751C500817A42 /* RelayWithLocation.swift */, - 7A3AD5002C1068A800E9AD90 /* RelaySelectorPicker.swift */, ); path = Relay; sourceTree = ""; @@ -5325,8 +5336,7 @@ F0164EBE2B4BFF940020268D /* ShadowsocksLoader.swift in Sources */, A9A1DE792AD5708E0073F689 /* TransportStrategy.swift in Sources */, A90763BF2B2857D50045ADF0 /* Socks5Handshake.swift in Sources */, - 7A3AD5012C1068A800E9AD90 /* RelaySelectorPicker.swift in Sources */, - 7A4D849D2C0F289400687980 /* RelaySelectorWrapper.swift in Sources */, + 7A3AD5012C1068A800E9AD90 /* RelayPicking.swift in Sources */, A90763C52B2858B40045ADF0 /* AnyIPEndpoint+Socks5.swift in Sources */, F06045EC2B2322A500B2D37A /* Jittered.swift in Sources */, F0DDE4152B220458006B57A7 /* ShadowsocksConfigurationCache.swift in Sources */, @@ -5348,10 +5358,12 @@ 7A516C3A2B7111A700BBD33D /* IPOverrideWrapper.swift in Sources */, F0DDE4142B220458006B57A7 /* ShadowSocksProxy.swift in Sources */, A90763B62B2857D50045ADF0 /* Socks5ConnectNegotiation.swift in Sources */, + 7ACE19112C1C349200260BB6 /* MultihopDecisionFlow.swift in Sources */, F06045E62B231EB700B2D37A /* URLSessionTransport.swift in Sources */, 06799AE628F98E4800ACD94E /* ServerRelaysResponse.swift in Sources */, F0DDE42B2B220A15006B57A7 /* RelaySelector.swift in Sources */, F0B894F32BF7526700817A42 /* RelaySelector+Wireguard.swift in Sources */, + 7AEBA52A2C2179F20018BEC5 /* RelaySelectorWrapper.swift in Sources */, F0B894EF2BF751C500817A42 /* RelayWithLocation.swift in Sources */, F0DDE42C2B220A15006B57A7 /* Midpoint.swift in Sources */, A90763C72B2858DC0045ADF0 /* CancellableChain.swift in Sources */, @@ -5503,12 +5515,14 @@ A9A5FA152ACB05160083449F /* RedeemVoucherOperation.swift in Sources */, A9A5FA162ACB05160083449F /* RotateKeyOperation.swift in Sources */, F072D3CF2C07122400906F64 /* MultihopUpdaterTests.swift in Sources */, + 7ACE19132C1C352100260BB6 /* RelayPickingTests.swift in Sources */, F09D04B52AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift in Sources */, 58BE4B9D2B18A85B007EA1D3 /* NSAttributedString+Extensions.swift in Sources */, A9A5FA172ACB05160083449F /* SendTunnelProviderMessageOperation.swift in Sources */, 7A83A0C62B29A750008B5CE7 /* APIAccessMethodsTests.swift in Sources */, A9A5FA182ACB05160083449F /* SetAccountOperation.swift in Sources */, A9A5FA192ACB05160083449F /* StartTunnelOperation.swift in Sources */, + 7ACE19152C1C429A00260BB6 /* MultihopDecisionFlowTests.swift in Sources */, A9A5FA1A2ACB05160083449F /* StopTunnelOperation.swift in Sources */, 7A9BE5A52B90760C00E2A7D0 /* CustomListsDataSourceTests.swift in Sources */, A9A5FA1B2ACB05160083449F /* Tunnel.swift in Sources */, @@ -5557,6 +5571,7 @@ A9A5FA332ACB05160083449F /* RelaySelectorTests.swift in Sources */, 58DFF7D32B02570000F864E0 /* MarkdownStylingOptions.swift in Sources */, A9A5FA342ACB05160083449F /* StringTests.swift in Sources */, + 7A52F96C2C17450C00B133B9 /* RelaySelectorWrapperTests.swift in Sources */, A9A5FA352ACB05160083449F /* WgKeyRotationTests.swift in Sources */, 7AB4CCB92B69097E006037F5 /* IPOverrideTests.swift in Sources */, A9A5FA362ACB05160083449F /* TunnelManagerTests.swift in Sources */, diff --git a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000000..929180afa433 --- /dev/null +++ b/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,22 @@ +{ + "pins" : [ + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "173f567a2dfec11d74588eea82cecea555bdc0bc", + "version" : "1.4.0" + } + }, + { + "identity" : "wireguard-apple", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mullvad/wireguard-apple.git", + "state" : { + "revision" : "15242e1698fc45261285d7417ed2cd5130d7332e" + } + } + ], + "version" : 2 +} diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 8ff9c3d6b3f5..f027b4dc59b4 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -93,8 +93,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD let relaySelector = RelaySelectorWrapper( relayCache: ipOverrideWrapper, - multihopUpdater: multihopUpdater, - multihopState: multihopState + multihopUpdater: multihopUpdater ) tunnelManager = createTunnelManager(application: application, relaySelector: relaySelector) @@ -124,8 +123,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD cache: shadowsocksCache, relaySelector: shadowsocksRelaySelector, constraintsUpdater: constraintsUpdater, - multihopUpdater: multihopUpdater, - multihopState: tunnelManager.settings.tunnelMultihopState + multihopUpdater: multihopUpdater ) configuredTransportProvider = ProxyConfigurationTransportProvider( diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift new file mode 100644 index 000000000000..d6d570ee9a73 --- /dev/null +++ b/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift @@ -0,0 +1,156 @@ +// +// MultihopDecisionFlowTests.swift +// MullvadVPNTests +// +// Created by Jon Petersson on 2024-06-14. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +@testable import MullvadREST +@testable import MullvadTypes +import XCTest + +class MultihopDecisionFlowTests: XCTestCase { + let sampleRelays = ServerRelaysResponseStubs.sampleRelays + + func testOneToOneCanHandle() throws { + let oneToOne = OneToOne(next: nil, relayPicker: picker) + + XCTAssertTrue(oneToOne.canHandle( + entryCandidates: [seSto6], + exitCandidates: [seSto2] + )) + + XCTAssertFalse(oneToOne.canHandle( + entryCandidates: [seSto2, seSto6], + exitCandidates: [seSto2] + )) + + XCTAssertFalse(oneToOne.canHandle( + entryCandidates: [seSto2, seSto6], + exitCandidates: [seSto2, seSto6] + )) + } + + func testOneToManyCanHandle() throws { + let oneToMany = OneToMany(next: nil, relayPicker: picker) + + XCTAssertTrue(oneToMany.canHandle( + entryCandidates: [seSto2, seSto6], + exitCandidates: [seSto2] + )) + + XCTAssertFalse(oneToMany.canHandle( + entryCandidates: [seSto6], + exitCandidates: [seSto2] + )) + + XCTAssertFalse(oneToMany.canHandle( + entryCandidates: [seSto2, seSto6], + exitCandidates: [seSto2, seSto6] + )) + } + + func testManyToManyCanHandle() throws { + let manyToMany = ManyToMany(next: nil, relayPicker: picker) + + XCTAssertTrue(manyToMany.canHandle( + entryCandidates: [seSto2, seSto6], + exitCandidates: [seSto2, seSto6] + )) + + XCTAssertFalse(manyToMany.canHandle( + entryCandidates: [seSto6], + exitCandidates: [seSto2] + )) + + XCTAssertFalse(manyToMany.canHandle( + entryCandidates: [seSto2, seSto6], + exitCandidates: [seSto2] + )) + } + + func testOneToOnePick() throws { + let oneToOne = OneToOne(next: nil, relayPicker: picker) + + let entryCandidates = [seSto2] + let exitCandidates = [seSto6] + + let selectedRelays = try oneToOne.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) + + XCTAssertEqual(selectedRelays.entry?.hostname, "se2-wireguard") + XCTAssertEqual(selectedRelays.exit.hostname, "se6-wireguard") + } + + func testOneToManyPick() throws { + let oneToMany = OneToMany(next: nil, relayPicker: picker) + + let entryCandidates = [seSto2, seSto6] + let exitCandidates = [seSto2] + + let selectedRelays = try oneToMany.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) + + XCTAssertEqual(selectedRelays.entry?.hostname, "se6-wireguard") + XCTAssertEqual(selectedRelays.exit.hostname, "se2-wireguard") + } + + func testManyToManyPick() throws { + let manyToMany = ManyToMany(next: nil, relayPicker: picker) + + let entryCandidates = [seSto2, seSto6] + let exitCandidates = [seSto2, seSto6] + + let selectedRelays = try manyToMany.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) + + if selectedRelays.exit.hostname == "se2-wireguard" { + XCTAssertEqual(selectedRelays.entry?.hostname, "se6-wireguard") + } else { + XCTAssertEqual(selectedRelays.entry?.hostname, "se2-wireguard") + } + } +} + +extension MultihopDecisionFlowTests { + var picker: MultihopPicker { + let constraints = RelayConstraints( + entryLocations: .only(UserSelectedRelays(locations: [.city("se", "sto")])), + exitLocations: .only(UserSelectedRelays(locations: [.city("se", "sto")])) + ) + + return MultihopPicker( + constraints: constraints, + relays: sampleRelays, + connectionAttemptCount: 0 + ) + } + + var seSto2: RelayWithLocation { + let relay = sampleRelays.wireguard.relays.first { $0.hostname == "se2-wireguard" }! + let serverLocation = sampleRelays.locations["se-sto"]! + let location = Location( + country: serverLocation.country, + countryCode: serverLocation.country, + city: serverLocation.city, + cityCode: "se-sto", + latitude: serverLocation.latitude, + longitude: serverLocation.longitude + ) + + return RelayWithLocation(relay: relay, serverLocation: location) + } + + var seSto6: RelayWithLocation { + let relay = sampleRelays.wireguard.relays.first { $0.hostname == "se6-wireguard" }! + let serverLocation = sampleRelays.locations["se-sto"]! + let location = Location( + country: serverLocation.country, + countryCode: serverLocation.country, + city: serverLocation.city, + cityCode: "se-sto", + latitude: serverLocation.latitude, + longitude: serverLocation.longitude + ) + + return RelayWithLocation(relay: relay, serverLocation: location) + } +} diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift new file mode 100644 index 000000000000..3c9acec44561 --- /dev/null +++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift @@ -0,0 +1,68 @@ +// +// RelayPickingTests.swift +// MullvadVPNTests +// +// Created by Jon Petersson on 2024-06-14. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +@testable import MullvadREST +@testable import MullvadTypes +import XCTest + +class RelayPickingTests: XCTestCase { + let sampleRelays = ServerRelaysResponseStubs.sampleRelays + + func testSinglehopPicker() throws { + let constraints = RelayConstraints( + entryLocations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se2-wireguard")])), + exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])) + ) + + let picker = SinglehopPicker( + constraints: constraints, + relays: sampleRelays, + connectionAttemptCount: 0 + ) + + let selectedRelays = try picker.pick() + + XCTAssertNil(selectedRelays.entry) + XCTAssertEqual(selectedRelays.exit.hostname, "se10-wireguard") + } + + func testMultihopPicker() throws { + let constraints = RelayConstraints( + entryLocations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se2-wireguard")])), + exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])) + ) + + let picker = MultihopPicker( + constraints: constraints, + relays: sampleRelays, + connectionAttemptCount: 0 + ) + + let selectedRelays = try picker.pick() + + XCTAssertEqual(selectedRelays.entry?.hostname, "se2-wireguard") + XCTAssertEqual(selectedRelays.exit.hostname, "se10-wireguard") + } + + func testMultihopPickerWithSameEntryAndExit() throws { + let constraints = RelayConstraints( + entryLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])), + exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])) + ) + + let picker = MultihopPicker( + constraints: constraints, + relays: sampleRelays, + connectionAttemptCount: 0 + ) + + XCTAssertThrowsError(try picker.pick()) + } +} diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift new file mode 100644 index 000000000000..a1ecb02fdfc2 --- /dev/null +++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift @@ -0,0 +1,55 @@ +// +// RelaySelectorWrapperTests.swift +// MullvadVPNTests +// +// Created by Jon Petersson on 2024-06-10. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +@testable import MullvadREST +@testable import MullvadSettings +@testable import MullvadTypes +import XCTest + +class RelaySelectorWrapperTests: XCTestCase { + let fileCache = MockFileCache( + initialState: .exists(CachedRelays( + relays: ServerRelaysResponseStubs.sampleRelays, + updatedAt: .distantPast + )) + ) + + var relayCache: RelayCache! + var multihopUpdater: MultihopUpdater! + var multihopStateListener: MultihopStateListener! + + override func setUp() { + relayCache = RelayCache(fileCache: fileCache) + multihopStateListener = MultihopStateListener() + multihopUpdater = MultihopUpdater(listener: multihopStateListener) + } + + func testSelectRelayWithMultihopOff() throws { + let wrapper = RelaySelectorWrapper( + relayCache: relayCache, + multihopUpdater: multihopUpdater + ) + + multihopStateListener.onNewMultihop?(.off) + + let selectedRelays = try wrapper.selectRelays(with: RelayConstraints(), connectionAttemptCount: 0) + XCTAssertNil(selectedRelays.entry) + } + + func testSelectRelayWithMultihopOn() throws { + let wrapper = RelaySelectorWrapper( + relayCache: relayCache, + multihopUpdater: multihopUpdater + ) + + multihopStateListener.onNewMultihop?(.on) + + let selectedRelays = try wrapper.selectRelays(with: RelayConstraints(), connectionAttemptCount: 0) + XCTAssertNotNil(selectedRelays.entry) + } +} diff --git a/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift b/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift index 6184756f4631..637e47c89ebe 100644 --- a/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift +++ b/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift @@ -9,7 +9,6 @@ import MullvadMockData import MullvadTypes @testable import PacketTunnelCore -@testable import PacketTunnelCoreTests import WireGuardKitTypes import XCTest diff --git a/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift b/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift deleted file mode 100644 index 8db65968a2b8..000000000000 --- a/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// RelaySelectorWrapper.swift -// PacketTunnel -// -// Created by pronebird on 08/08/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import MullvadREST -import MullvadSettings -import MullvadTypes -import PacketTunnelCore - -final class RelaySelectorWrapper: RelaySelectorProtocol { - let relayCache: RelayCacheProtocol - let multihopUpdater: MultihopUpdater - private var multihopState: MultihopState = .off - private var observer: MultihopObserverBlock! - - deinit { - self.multihopUpdater.removeObserver(observer) - } - - public init( - relayCache: RelayCacheProtocol, - multihopUpdater: MultihopUpdater - ) { - self.relayCache = relayCache - self.multihopUpdater = multihopUpdater - self.addObserver() - } - - private func addObserver() { - self.observer = MultihopObserverBlock(didUpdateMultihop: { [weak self] _, multihopState in - self?.multihopState = multihopState - }) - - multihopUpdater.addObserver(observer) - } - - func selectRelay( - with constraints: RelayConstraints, - connectionAttemptFailureCount: UInt - ) throws -> SelectedRelay { - switch multihopState { - case .off, .on: - let selectorResult = try RelaySelector.WireGuard.evaluate( - by: constraints, - in: relayCache.read().relays, - numberOfFailedAttempts: connectionAttemptFailureCount - ) - - return SelectedRelay( - endpoint: selectorResult.endpoint, - hostname: selectorResult.relay.hostname, - location: selectorResult.location, - retryAttempts: connectionAttemptFailureCount - ) - } - } -}