summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@kvadrat.se>2024-06-12 09:48:19 +0200
committerEmīls <emils@mullvad.net>2024-07-11 16:53:24 +0200
commite6f0fccede55db61c86d9470e85ebe7693541368 (patch)
tree14561541caf232c260f3f2dc2228f22c91822d6f
parent5a0554fc315c31f2aa7955bf0ef9fdc9b1acff5e (diff)
downloadmullvadvpn-e6f0fccede55db61c86d9470e85ebe7693541368.tar.xz
mullvadvpn-e6f0fccede55db61c86d9470e85ebe7693541368.zip
Add RelaySelectorWrapper tests
-rw-r--r--ios/MullvadREST/Relay/MultihopDecisionFlow.swift116
-rw-r--r--ios/MullvadREST/Relay/RelayPicking.swift107
-rw-r--r--ios/MullvadREST/Relay/RelaySelectorPicker.swift200
-rw-r--r--ios/MullvadREST/Relay/RelaySelectorWrapper.swift4
-rw-r--r--ios/MullvadREST/Relay/RelayWithLocation.swift2
-rw-r--r--ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift4
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj33
-rw-r--r--ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved22
-rw-r--r--ios/MullvadVPN/AppDelegate.swift6
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift156
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift68
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift55
-rw-r--r--ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift1
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift62
14 files changed, 553 insertions, 283 deletions
diff --git a/ios/MullvadREST/Relay/MultihopDecisionFlow.swift b/ios/MullvadREST/Relay/MultihopDecisionFlow.swift
new file mode 100644
index 0000000000..fa8431ed99
--- /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<REST.ServerRelay>
+ 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 0000000000..eec1003a1c
--- /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<REST.ServerRelay>]
+ ) 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<REST.ServerRelay>]
+ ) 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 2ca41cb30e..0000000000
--- 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<REST.ServerRelay>]
- ) 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<REST.ServerRelay>]
- ) throws -> SelectedRelay {
- let filteredCandidates = candidates.filter { relayWithLocation in
- relayWithLocation.serverLocation != relay.location
- }
-
- return try findBestMatch(from: filteredCandidates)
- }
-}
-
-protocol MultihopDescionMaker {
- typealias RelayCandidate = RelayWithLocation<REST.ServerRelay>
- 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 eed453e163..3ee447d0ab 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 e6cdcac8af..0cba62661b 100644
--- a/ios/MullvadREST/Relay/RelayWithLocation.swift
+++ b/ios/MullvadREST/Relay/RelayWithLocation.swift
@@ -32,6 +32,6 @@ public struct RelayWithLocation<T: AnyRelay> {
extension RelayWithLocation: Equatable {
public static func == (lhs: RelayWithLocation<T>, rhs: RelayWithLocation<T>) -> 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 f919805665..2b46571bc8 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 1e25f62290..b039c4cb0f 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 = "<group>"; };
582403162A821FD700163DE8 /* TunnelDeviceInfoProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelDeviceInfoProtocol.swift; sourceTree = "<group>"; };
5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorProtocol.swift; sourceTree = "<group>"; };
- 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorWrapper.swift; sourceTree = "<group>"; };
5827B08F2B0CAA0500CCBBA1 /* EditAccessMethodCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessMethodCoordinator.swift; sourceTree = "<group>"; };
5827B0912B0CAB2800CCBBA1 /* MethodSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSettingsViewController.swift; sourceTree = "<group>"; };
5827B0952B0DB2C100CCBBA1 /* MethodSettingsItemIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSettingsItemIdentifier.swift; sourceTree = "<group>"; };
@@ -1893,14 +1896,15 @@
7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorVPNConnection.swift; sourceTree = "<group>"; };
7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelInfo.swift; sourceTree = "<group>"; };
7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationBlockObserverSupport.swift; sourceTree = "<group>"; };
+ 7A3AD5002C1068A800E9AD90 /* RelayPicking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPicking.swift; sourceTree = "<group>"; };
7A3EFAAA2BDFDAE800318736 /* RelaySelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelection.swift; sourceTree = "<group>"; };
- 7A3AD5002C1068A800E9AD90 /* RelaySelectorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorPicker.swift; sourceTree = "<group>"; };
7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandlerTests.swift; sourceTree = "<group>"; };
7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = "<group>"; };
7A45CFC22C05FF2F00D80B21 /* ScreenshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotTests.swift; sourceTree = "<group>"; };
7A516C2D2B6D357500BBD33D /* URL+Scoping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Scoping.swift"; sourceTree = "<group>"; };
7A516C392B7111A700BBD33D /* IPOverrideWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideWrapper.swift; sourceTree = "<group>"; };
7A516C3B2B712F0B00BBD33D /* IPOverrideWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideWrapperTests.swift; sourceTree = "<group>"; };
+ 7A52F96B2C17450C00B133B9 /* RelaySelectorWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorWrapperTests.swift; sourceTree = "<group>"; };
7A5869942B32E9C700640D27 /* LinkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkButton.swift; sourceTree = "<group>"; };
7A5869962B32EA4500640D27 /* AppButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppButton.swift; sourceTree = "<group>"; };
7A58699A2B482FE200640D27 /* UITableViewCell+Disable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Disable.swift"; sourceTree = "<group>"; };
@@ -1987,6 +1991,9 @@
7ABFB09D2BA316220074A49E /* RelayConstraintsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConstraintsTests.swift; sourceTree = "<group>"; };
7AC8A3AD2ABC6FBB00DC4939 /* SettingsHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsHeaderView.swift; sourceTree = "<group>"; };
7ACD79382C0DAADC00DBEE14 /* AddCustomListLocationsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCustomListLocationsPage.swift; sourceTree = "<group>"; };
+ 7ACE19102C1C349200260BB6 /* MultihopDecisionFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopDecisionFlow.swift; sourceTree = "<group>"; };
+ 7ACE19122C1C352100260BB6 /* RelayPickingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPickingTests.swift; sourceTree = "<group>"; };
+ 7ACE19142C1C429A00260BB6 /* MultihopDecisionFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopDecisionFlowTests.swift; sourceTree = "<group>"; };
7AD0AA192AD69B6E00119E10 /* PacketTunnelActorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelActorProtocol.swift; sourceTree = "<group>"; };
7AD0AA1B2AD6A63F00119E10 /* PacketTunnelActorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelActorStub.swift; sourceTree = "<group>"; };
7AD0AA1E2AD6C8B900119E10 /* URLRequestProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestProxyProtocol.swift; sourceTree = "<group>"; };
@@ -1995,6 +2002,7 @@
7ADCB2D92B6A730400C88F89 /* IPOverrideRepositoryStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideRepositoryStub.swift; sourceTree = "<group>"; };
7AE241482C20682B0076CE33 /* FormsheetPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormsheetPresentationController.swift; sourceTree = "<group>"; };
7AE90B672C2D726000375A60 /* NSParagraphStyle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSParagraphStyle+Extensions.swift"; sourceTree = "<group>"; };
+ 7AEBA5292C2179F20018BEC5 /* RelaySelectorWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelaySelectorWrapper.swift; sourceTree = "<group>"; };
7AEBA52B2C22C65B0018BEC5 /* TimeInterval+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Timeout.swift"; sourceTree = "<group>"; };
7AEF7F192AD00F52006FE45D /* AppMessageHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandler.swift; sourceTree = "<group>"; };
7AF10EB12ADE859200C090B9 /* AlertViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = "<group>"; };
@@ -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 = "<group>";
@@ -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 = "<group>";
@@ -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 0000000000..929180afa4
--- /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 8ff9c3d6b3..f027b4dc59 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 0000000000..d6d570ee9a
--- /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<REST.ServerRelay> {
+ 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<REST.ServerRelay> {
+ 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 0000000000..3c9acec445
--- /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 0000000000..a1ecb02fdf
--- /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 6184756f46..637e47c89e 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 8db65968a2..0000000000
--- 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
- )
- }
- }
-}