summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEmīls <emils@mullvad.net>2024-07-12 10:50:27 +0200
committerEmīls <emils@mullvad.net>2024-07-12 10:50:27 +0200
commit0c22831fcc5c31d3210d586cc2cd2f81bcf5d224 (patch)
tree14561541caf232c260f3f2dc2228f22c91822d6f
parent5bd43f95a4189aff24bc9ea120b2841b222c6921 (diff)
parente6f0fccede55db61c86d9470e85ebe7693541368 (diff)
downloadmullvadvpn-0c22831fcc5c31d3210d586cc2cd2f81bcf5d224.tar.xz
mullvadvpn-0c22831fcc5c31d3210d586cc2cd2f81bcf5d224.zip
Merge branch 'ios-598-allow-relay-selector-to-select-an-entry-peer'
-rw-r--r--ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift (renamed from ios/PacketTunnelCoreTests/Mocks/RelaySelectorStub.swift)24
-rw-r--r--ios/MullvadREST/Relay/MultihopDecisionFlow.swift116
-rw-r--r--ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift2
-rw-r--r--ios/MullvadREST/Relay/RelayPicking.swift107
-rw-r--r--ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift1
-rw-r--r--ios/MullvadREST/Relay/RelaySelector+Wireguard.swift47
-rw-r--r--ios/MullvadREST/Relay/RelaySelector.swift1
-rw-r--r--ios/MullvadREST/Relay/RelaySelectorProtocol.swift (renamed from ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift)19
-rw-r--r--ios/MullvadREST/Relay/RelaySelectorWrapper.swift (renamed from ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift)49
-rw-r--r--ios/MullvadREST/Relay/RelayWithLocation.swift10
-rw-r--r--ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift4
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj42
-rw-r--r--ios/MullvadVPN/AppDelegate.swift28
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift51
-rw-r--r--ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift8
-rw-r--r--ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift8
-rw-r--r--ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift6
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelInteractor.swift3
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift25
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelState+UI.swift12
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelState.swift41
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift15
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelControlViewModel.swift4
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift12
-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/RelaySelectorTests.swift107
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift55
-rw-r--r--ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift32
-rw-r--r--ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift16
-rw-r--r--ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift12
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift9
-rw-r--r--ios/PacketTunnelCore/Actor/ObservedState.swift9
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift14
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift6
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor.swift109
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift10
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActorProtocol.swift2
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift14
-rw-r--r--ios/PacketTunnelCore/Actor/StartOptions.swift13
-rw-r--r--ios/PacketTunnelCore/Actor/State+Extensions.swift2
-rw-r--r--ios/PacketTunnelCore/Actor/State.swift15
-rw-r--r--ios/PacketTunnelCore/IPC/PacketTunnelOptions.swift13
-rw-r--r--ios/PacketTunnelCore/IPC/TunnelProviderMessage.swift2
-rw-r--r--ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift29
-rw-r--r--ios/PacketTunnelCoreTests/EventChannelTests.swift2
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift2
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/PacketTunnelActorStub.swift2
-rw-r--r--ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift3
49 files changed, 918 insertions, 419 deletions
diff --git a/ios/PacketTunnelCoreTests/Mocks/RelaySelectorStub.swift b/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift
index 4922c080c5..f2f8952df4 100644
--- a/ios/PacketTunnelCoreTests/Mocks/RelaySelectorStub.swift
+++ b/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift
@@ -6,30 +6,29 @@
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
//
-import Foundation
+import MullvadREST
import MullvadTypes
-import PacketTunnelCore
import WireGuardKitTypes
/// Relay selector stub that accepts a block that can be used to provide custom implementation.
-struct RelaySelectorStub: RelaySelectorProtocol {
- let block: (RelayConstraints, UInt) throws -> SelectedRelay
+public struct RelaySelectorStub: RelaySelectorProtocol {
+ let block: (RelayConstraints, UInt) throws -> SelectedRelays
- func selectRelay(
+ public func selectRelays(
with constraints: RelayConstraints,
- connectionAttemptFailureCount: UInt
- ) throws -> SelectedRelay {
- return try block(constraints, connectionAttemptFailureCount)
+ connectionAttemptCount: UInt
+ ) throws -> SelectedRelays {
+ return try block(constraints, connectionAttemptCount)
}
}
extension RelaySelectorStub {
/// Returns a relay selector that never fails.
- static func nonFallible() -> RelaySelectorStub {
+ public static func nonFallible() -> RelaySelectorStub {
let publicKey = PrivateKey().publicKey.rawValue
return RelaySelectorStub { _, _ in
- return SelectedRelay(
+ let cityRelay = SelectedRelay(
endpoint: MullvadEndpoint(
ipv4Relay: IPv4Endpoint(ip: .loopback, port: 1300),
ipv4Gateway: .loopback,
@@ -46,6 +45,11 @@ extension RelaySelectorStub {
longitude: 0
), retryAttempts: 0
)
+
+ return SelectedRelays(
+ entry: cityRelay,
+ exit: cityRelay
+ )
}
}
}
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/NoRelaysSatisfyingConstraintsError.swift b/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift
index 9435929db6..b435428930 100644
--- a/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift
+++ b/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift
@@ -9,6 +9,8 @@
import Foundation
public struct NoRelaysSatisfyingConstraintsError: LocalizedError {
+ public init() {}
+
public var errorDescription: String? {
"No relays satisfying constraints."
}
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/RelaySelector+Shadowsocks.swift b/ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift
index 273b9afe03..1f678e6027 100644
--- a/ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift
+++ b/ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift
@@ -45,7 +45,6 @@ extension RelaySelector {
let mappedBridges = mapRelays(relays: relaysResponse.bridge.relays, locations: relaysResponse.locations)
let filteredRelays = applyConstraints(
location,
- portConstraint: port,
filterConstraint: filter,
relays: mappedBridges
)
diff --git a/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift b/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift
index 4607838ac2..1e61156941 100644
--- a/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift
+++ b/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift
@@ -6,54 +6,39 @@
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//
-import Foundation
import MullvadTypes
extension RelaySelector {
public enum WireGuard {
- /**
- Filters relay list using given constraints and selects random relay for exit relay.
- Throws an error if there are no relays satisfying the given constraints.
- */
- public static func evaluate(
- by constraints: RelayConstraints,
- in relaysResponse: REST.ServerRelaysResponse,
- numberOfFailedAttempts: UInt
- ) throws -> RelaySelectorResult {
- let exitCandidates = try findBestMatch(
- relays: relaysResponse,
- relayConstraint: constraints.exitLocations,
- portConstraint: constraints.port,
- filterConstraint: constraints.filter,
- numberOfFailedAttempts: numberOfFailedAttempts
- )
+ /// Filters relay list using given constraints.
+ public static func findCandidates(
+ by relayConstraint: RelayConstraint<UserSelectedRelays>,
+ in relays: REST.ServerRelaysResponse,
+ filterConstraint: RelayConstraint<RelayFilter>
+ ) throws -> [RelayWithLocation<REST.ServerRelay>] {
+ let mappedRelays = mapRelays(relays: relays.wireguard.relays, locations: relays.locations)
- return exitCandidates
+ return applyConstraints(
+ relayConstraint,
+ filterConstraint: filterConstraint,
+ relays: mappedRelays
+ )
}
- // MARK: - private functions
-
- private static func findBestMatch(
+ /// Picks a random relay from a list.
+ public static func pickCandidate(
+ from relayWithLocations: [RelayWithLocation<REST.ServerRelay>],
relays: REST.ServerRelaysResponse,
- relayConstraint: RelayConstraint<UserSelectedRelays>,
portConstraint: RelayConstraint<UInt16>,
- filterConstraint: RelayConstraint<RelayFilter>,
numberOfFailedAttempts: UInt
) throws -> RelaySelectorMatch {
- let mappedRelays = mapRelays(relays: relays.wireguard.relays, locations: relays.locations)
- let filteredRelays = applyConstraints(
- relayConstraint,
- portConstraint: portConstraint,
- filterConstraint: filterConstraint,
- relays: mappedRelays
- )
let port = applyPortConstraint(
portConstraint,
rawPortRanges: relays.wireguard.portRanges,
numberOfFailedAttempts: numberOfFailedAttempts
)
- guard let relayWithLocation = pickRandomRelayByWeight(relays: filteredRelays), let port else {
+ guard let port, let relayWithLocation = pickRandomRelayByWeight(relays: relayWithLocations) else {
throw NoRelaysSatisfyingConstraintsError()
}
diff --git a/ios/MullvadREST/Relay/RelaySelector.swift b/ios/MullvadREST/Relay/RelaySelector.swift
index 44062134cc..da4082a1b1 100644
--- a/ios/MullvadREST/Relay/RelaySelector.swift
+++ b/ios/MullvadREST/Relay/RelaySelector.swift
@@ -134,7 +134,6 @@ public enum RelaySelector {
/// Produce a list of `RelayWithLocation` items satisfying the given constraints
static func applyConstraints<T: AnyRelay>(
_ relayConstraint: RelayConstraint<UserSelectedRelays>,
- portConstraint: RelayConstraint<UInt16>,
filterConstraint: RelayConstraint<RelayFilter>,
relays: [RelayWithLocation<T>]
) -> [RelayWithLocation<T>] {
diff --git a/ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift b/ios/MullvadREST/Relay/RelaySelectorProtocol.swift
index a4408392e3..390757c3dd 100644
--- a/ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift
+++ b/ios/MullvadREST/Relay/RelaySelectorProtocol.swift
@@ -11,7 +11,7 @@ import MullvadTypes
/// Protocol describing a type that can select a relay.
public protocol RelaySelectorProtocol {
- func selectRelay(with constraints: RelayConstraints, connectionAttemptFailureCount: UInt) throws -> SelectedRelay
+ func selectRelays(with constraints: RelayConstraints, connectionAttemptCount: UInt) throws -> SelectedRelays
}
/// Struct describing the selected relay.
@@ -42,3 +42,20 @@ extension SelectedRelay: CustomDebugStringConvertible {
"\(hostname) -> \(endpoint.ipv4Relay.description)"
}
}
+
+public struct SelectedRelays: Equatable, Codable {
+ public let entry: SelectedRelay?
+ public let exit: SelectedRelay
+
+ public init(entry: SelectedRelay?, exit: SelectedRelay) {
+ self.entry = entry
+ self.exit = exit
+ }
+}
+
+extension SelectedRelays: CustomDebugStringConvertible {
+ public var debugDescription: String {
+ "Entry: \(entry?.hostname ?? "-") -> \(entry?.endpoint.ipv4Relay.description ?? "-"), " +
+ "Exit: \(exit.hostname) -> \(exit.endpoint.ipv4Relay.description)"
+ }
+}
diff --git a/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift
index 8db65968a2..3ee447d0ab 100644
--- a/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift
+++ b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift
@@ -6,13 +6,10 @@
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
//
-import Foundation
-import MullvadREST
import MullvadSettings
import MullvadTypes
-import PacketTunnelCore
-final class RelaySelectorWrapper: RelaySelectorProtocol {
+public final class RelaySelectorWrapper: RelaySelectorProtocol {
let relayCache: RelayCacheProtocol
let multihopUpdater: MultihopUpdater
private var multihopState: MultihopState = .off
@@ -28,9 +25,32 @@ final class RelaySelectorWrapper: RelaySelectorProtocol {
) {
self.relayCache = relayCache
self.multihopUpdater = multihopUpdater
+
self.addObserver()
}
+ public func selectRelays(
+ with constraints: RelayConstraints,
+ connectionAttemptCount: UInt
+ ) throws -> SelectedRelays {
+ let relays = try relayCache.read().relays
+
+ switch multihopState {
+ case .off:
+ return try SinglehopPicker(
+ constraints: constraints,
+ relays: relays,
+ connectionAttemptCount: connectionAttemptCount
+ ).pick()
+ case .on:
+ return try MultihopPicker(
+ constraints: constraints,
+ relays: relays,
+ connectionAttemptCount: connectionAttemptCount
+ ).pick()
+ }
+ }
+
private func addObserver() {
self.observer = MultihopObserverBlock(didUpdateMultihop: { [weak self] _, multihopState in
self?.multihopState = multihopState
@@ -38,25 +58,4 @@ final class RelaySelectorWrapper: RelaySelectorProtocol {
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
- )
- }
- }
}
diff --git a/ios/MullvadREST/Relay/RelayWithLocation.swift b/ios/MullvadREST/Relay/RelayWithLocation.swift
index c80cc34a3a..0cba62661b 100644
--- a/ios/MullvadREST/Relay/RelayWithLocation.swift
+++ b/ios/MullvadREST/Relay/RelayWithLocation.swift
@@ -9,9 +9,9 @@
import Foundation
import MullvadTypes
-struct RelayWithLocation<T: AnyRelay> {
+public struct RelayWithLocation<T: AnyRelay> {
let relay: T
- let serverLocation: Location
+ public let serverLocation: Location
func matches(location: RelayLocation) -> Bool {
return switch location {
@@ -29,3 +29,9 @@ struct RelayWithLocation<T: AnyRelay> {
}
}
}
+
+extension RelayWithLocation: Equatable {
+ public static func == (lhs: RelayWithLocation<T>, rhs: RelayWithLocation<T>) -> Bool {
+ 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 d6b4e6a4a1..b039c4cb0f 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -81,7 +81,6 @@
5820EDAB288FF0D2006BF4E4 /* DeviceRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5820EDAA288FF0D2006BF4E4 /* DeviceRowView.swift */; };
58238CB92AD57EC700768310 /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; };
5823FA5426CE49F700283BF8 /* TunnelObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5823FA5326CE49F600283BF8 /* TunnelObserver.swift */; };
- 582403822A827E1500163DE8 /* RelaySelectorWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */; };
5826B6CB2ABD83E200B1CA13 /* PacketTunnelOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587C575226D2615F005EF767 /* PacketTunnelOptions.swift */; };
5827B0902B0CAA0500CCBBA1 /* EditAccessMethodCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B08F2B0CAA0500CCBBA1 /* EditAccessMethodCoordinator.swift */; };
5827B0922B0CAB2800CCBBA1 /* MethodSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5827B0912B0CAB2800CCBBA1 /* MethodSettingsViewController.swift */; };
@@ -448,12 +447,10 @@
58FE25DA2AA72A8F003D1918 /* PacketTunnelActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E9C3852A4EF1CB00CFDEAC /* PacketTunnelActor.swift */; };
58FE25DB2AA72A8F003D1918 /* StartOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ED3A132A7C199C0085CE65 /* StartOptions.swift */; };
58FE25DC2AA72A8F003D1918 /* AnyTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEBA02A9CA14B00F578F2 /* AnyTask.swift */; };
- 58FE25DF2AA72A9B003D1918 /* RelaySelectorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */; };
58FE25E12AA72A9B003D1918 /* SettingsReaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586E7A2C2A987689006DAB1B /* SettingsReaderProtocol.swift */; };
58FE25E62AA738E8003D1918 /* TunnelAdapterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5819ABC22A8CF02C007B59A6 /* TunnelAdapterProtocol.swift */; };
58FE25EC2AA77639003D1918 /* TunnelMonitorStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25EB2AA77638003D1918 /* TunnelMonitorStub.swift */; };
58FE25EE2AA7764E003D1918 /* TunnelAdapterDummy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25ED2AA7764E003D1918 /* TunnelAdapterDummy.swift */; };
- 58FE25F02AA77664003D1918 /* RelaySelectorStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */; };
58FE25F22AA77674003D1918 /* SettingsReaderStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25F12AA77674003D1918 /* SettingsReaderStub.swift */; };
58FE25F42AA9D730003D1918 /* PacketTunnelActor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25F32AA9D730003D1918 /* PacketTunnelActor+Extensions.swift */; };
58FE65952AB1D90600E53CB5 /* MullvadTypes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223D5294C8E5E0029F5F8 /* MullvadTypes.framework */; };
@@ -489,6 +486,7 @@
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 */; };
7A3FD1B52AD4465A0042BEA6 /* AppMessageHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */; };
7A3FD1B72AD54ABD0042BEA6 /* AnyTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB982A98F4ED00F578F2 /* AnyTransport.swift */; };
@@ -496,9 +494,12 @@
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 */; };
+ 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 */; };
@@ -597,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 */; };
@@ -606,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 */; };
@@ -1528,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>"; };
@@ -1892,6 +1896,7 @@
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>"; };
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>"; };
@@ -1899,6 +1904,7 @@
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>"; };
@@ -1985,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>"; };
@@ -1993,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>"; };
@@ -2502,8 +2512,8 @@
440E9EF42BDA943B00B1FD11 /* ApiHandlers */ = {
isa = PBXGroup;
children = (
- F0ACE3342BE51745006D5333 /* ServerRelaysResponse+Stubs.swift */,
A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */,
+ F0ACE3342BE51745006D5333 /* ServerRelaysResponse+Stubs.swift */,
);
path = ApiHandlers;
sourceTree = "<group>";
@@ -2512,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>";
@@ -3730,7 +3743,6 @@
isa = PBXGroup;
children = (
580D6B8B2AB3369300B2D6E0 /* BlockedStateErrorMapperProtocol.swift */,
- 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */,
586E7A2C2A987689006DAB1B /* SettingsReaderProtocol.swift */,
5819ABC22A8CF02C007B59A6 /* TunnelAdapterProtocol.swift */,
);
@@ -3746,7 +3758,6 @@
58EC067B2A8D2A0B00BEB973 /* NetworkCounters.swift */,
58FE25EB2AA77638003D1918 /* TunnelMonitorStub.swift */,
58FE25ED2AA7764E003D1918 /* TunnelAdapterDummy.swift */,
- 58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */,
58FE25F12AA77674003D1918 /* SettingsReaderStub.swift */,
58F7753C2AB8473200425B47 /* BlockedStateErrorMapperStub.swift */,
5838321A2AC1B18400EA2071 /* PacketTunnelActor+Mocks.swift */,
@@ -3803,7 +3814,6 @@
58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */,
58225D272A84F23B0083D7F1 /* PacketTunnelPathObserver.swift */,
58F3F3692AA08E3C00D3B0A4 /* PacketTunnelProvider.swift */,
- 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */,
5864AF7C2A9F4DC9008BC928 /* SettingsReader.swift */,
);
path = PacketTunnelProvider;
@@ -4182,6 +4192,7 @@
A900E9BD2ACC654100C95F67 /* APIProxy+Stubs.swift */,
A900E9BB2ACC609200C95F67 /* DevicesProxy+Stubs.swift */,
F0ACE32E2BE4EA8B006D5333 /* MockProxyFactory.swift */,
+ 58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */,
A900E9B92ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift */,
);
path = MullvadREST;
@@ -4204,12 +4215,16 @@
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 */,
+ 7AEBA5292C2179F20018BEC5 /* RelaySelectorWrapper.swift */,
F0B894F02BF751E300817A42 /* RelayWithDistance.swift */,
F0B894EE2BF751C500817A42 /* RelayWithLocation.swift */,
);
@@ -5317,9 +5332,11 @@
F0DDE4162B220458006B57A7 /* TransportProvider.swift in Sources */,
06799AEF28F98E4800ACD94E /* RetryStrategy.swift in Sources */,
06799AE128F98E4800ACD94E /* SSLPinningURLSessionDelegate.swift in Sources */,
+ 7A4D849E2C0F289800687980 /* RelaySelectorProtocol.swift in Sources */,
F0164EBE2B4BFF940020268D /* ShadowsocksLoader.swift in Sources */,
A9A1DE792AD5708E0073F689 /* TransportStrategy.swift in Sources */,
A90763BF2B2857D50045ADF0 /* Socks5Handshake.swift in Sources */,
+ 7A3AD5012C1068A800E9AD90 /* RelayPicking.swift in Sources */,
A90763C52B2858B40045ADF0 /* AnyIPEndpoint+Socks5.swift in Sources */,
F06045EC2B2322A500B2D37A /* Jittered.swift in Sources */,
F0DDE4152B220458006B57A7 /* ShadowsocksConfigurationCache.swift in Sources */,
@@ -5341,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 */,
@@ -5496,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 */,
@@ -5550,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 */,
@@ -5603,7 +5625,6 @@
files = (
58FE25F42AA9D730003D1918 /* PacketTunnelActor+Extensions.swift in Sources */,
58DDA18F2ABC32380039C360 /* Timings.swift in Sources */,
- 58FE25DF2AA72A9B003D1918 /* RelaySelectorProtocol.swift in Sources */,
58C7A4522A863FB50060C66F /* Pinger.swift in Sources */,
580D6B8C2AB3369300B2D6E0 /* BlockedStateErrorMapperProtocol.swift in Sources */,
58C7AF172ABD84AA007EDD7A /* ProxyURLRequest.swift in Sources */,
@@ -5670,7 +5691,6 @@
5838321D2AC1C54600EA2071 /* TaskSleepTests.swift in Sources */,
58092E542A8B832E00C3CC72 /* TunnelMonitorTests.swift in Sources */,
7AD0AA212AD6CB0000119E10 /* URLRequestProxyStub.swift in Sources */,
- 58FE25F02AA77664003D1918 /* RelaySelectorStub.swift in Sources */,
581F23AF2A8CF94D00788AB6 /* PingerMock.swift in Sources */,
A97D25B42B0CB59300946B2D /* TunnelObfuscationStub.swift in Sources */,
A97D25B02B0BB5C400946B2D /* ProtocolObfuscationStub.swift in Sources */,
@@ -6075,7 +6095,6 @@
583FE02429C1ACB3006E85F9 /* RESTCreateApplePaymentResponse+Localization.swift in Sources */,
58CE38C728992C8700A6D6E5 /* WireGuardAdapterError+Localization.swift in Sources */,
58E511E828DDDF2400B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */,
- 582403822A827E1500163DE8 /* RelaySelectorWrapper.swift in Sources */,
58FDF2D92A0BA11A00C2B061 /* DeviceCheckOperation.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -6286,6 +6305,7 @@
F0ACE3222BE4E4F2006D5333 /* APIProxy+Stubs.swift in Sources */,
F0ACE3332BE516F1006D5333 /* RESTRequestExecutor+Stubs.swift in Sources */,
F0ACE32D2BE4E784006D5333 /* AccountMock.swift in Sources */,
+ 7A52F96A2C1735AE00B133B9 /* RelaySelectorStub.swift in Sources */,
F0ACE32F2BE4EA8B006D5333 /* MockProxyFactory.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index fc6c746cd6..f027b4dc59 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -85,14 +85,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
)
addressCacheTracker = AddressCacheTracker(application: application, apiProxy: apiProxy, store: addressCache)
-
tunnelStore = TunnelStore(application: application)
- tunnelManager = createTunnelManager(application: application)
let constraintsUpdater = RelayConstraintsUpdater()
let multihopListener = MultihopStateListener()
let multihopUpdater = MultihopUpdater(listener: multihopListener)
+ let relaySelector = RelaySelectorWrapper(
+ relayCache: ipOverrideWrapper,
+ multihopUpdater: multihopUpdater
+ )
+ tunnelManager = createTunnelManager(application: application, relaySelector: relaySelector)
+
settingsObserver = TunnelBlockObserver(didLoadConfiguration: { tunnelManager in
multihopListener.onNewMultihop?(tunnelManager.settings.tunnelMultihopState)
constraintsUpdater.onNewConstraints?(tunnelManager.settings.relayConstraints)
@@ -119,8 +123,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
cache: shadowsocksCache,
relaySelector: shadowsocksRelaySelector,
constraintsUpdater: constraintsUpdater,
- multihopUpdater: multihopUpdater,
- multihopState: tunnelManager.settings.tunnelMultihopState
+ multihopUpdater: multihopUpdater
)
configuredTransportProvider = ProxyConfigurationTransportProvider(
@@ -139,7 +142,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
transportStrategy: transportStrategy
)
setUpTransportMonitor(transportProvider: transportProvider)
- setUpSimulatorHost(transportProvider: transportProvider)
+ setUpSimulatorHost(transportProvider: transportProvider, relaySelector: relaySelector)
registerBackgroundTasks()
setupPaymentHandler()
@@ -151,7 +154,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
return true
}
- private func createTunnelManager(application: UIApplication) -> TunnelManager {
+ private func createTunnelManager(
+ application: UIApplication,
+ relaySelector: RelaySelectorProtocol
+ ) -> TunnelManager {
return TunnelManager(
application: application,
tunnelStore: tunnelStore,
@@ -159,7 +165,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
accountsProxy: accountsProxy,
devicesProxy: devicesProxy,
apiProxy: apiProxy,
- accessTokenManager: proxyFactory.configuration.accessTokenManager
+ accessTokenManager: proxyFactory.configuration.accessTokenManager,
+ relaySelector: relaySelector
)
}
@@ -192,11 +199,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
)
}
- private func setUpSimulatorHost(transportProvider: TransportProvider) {
+ private func setUpSimulatorHost(
+ transportProvider: TransportProvider,
+ relaySelector: RelaySelectorWrapper
+ ) {
#if targetEnvironment(simulator)
// Configure mock tunnel provider on simulator
simulatorTunnelProviderHost = SimulatorTunnelProviderHost(
- relayCacheTracker: relayCacheTracker,
+ relaySelector: relaySelector,
transportProvider: transportProvider
)
SimulatorTunnelProvider.shared.delegate = simulatorTunnelProviderHost
diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift
index e53c5a50b2..08b49d4b05 100644
--- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift
+++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift
@@ -18,15 +18,15 @@ import PacketTunnelCore
final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate {
private var observedState: ObservedState = .disconnected
- private var selectedRelay: SelectedRelay?
+ private var selectedRelays: SelectedRelays?
private let urlRequestProxy: URLRequestProxy
- private let relayCacheTracker: RelayCacheTracker
+ private let relaySelector: RelaySelectorProtocol
private let providerLogger = Logger(label: "SimulatorTunnelProviderHost")
private let dispatchQueue = DispatchQueue(label: "SimulatorTunnelProviderHostQueue")
- init(relayCacheTracker: RelayCacheTracker, transportProvider: TransportProvider) {
- self.relayCacheTracker = relayCacheTracker
+ init(relaySelector: RelaySelectorProtocol, transportProvider: TransportProvider) {
+ self.relaySelector = relaySelector
self.urlRequestProxy = URLRequestProxy(
dispatchQueue: dispatchQueue,
transportProvider: transportProvider
@@ -43,12 +43,12 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate {
return
}
- var selectedRelay: SelectedRelay?
+ var selectedRelays: SelectedRelays?
do {
let tunnelOptions = PacketTunnelOptions(rawOptions: options ?? [:])
- selectedRelay = try tunnelOptions.getSelectedRelay()
+ selectedRelays = try tunnelOptions.getSelectedRelays()
} catch {
providerLogger.error(
error: error,
@@ -60,7 +60,7 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate {
}
do {
- setInternalStateConnected(with: try selectedRelay ?? pickRelay())
+ setInternalStateConnected(with: try selectedRelays ?? pickRelays())
completionHandler(nil)
} catch {
providerLogger.error(
@@ -74,7 +74,7 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate {
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
dispatchQueue.async { [weak self] in
- self?.selectedRelay = nil
+ self?.selectedRelays = nil
self?.observedState = .disconnected
completionHandler()
@@ -117,17 +117,17 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate {
reasserting = true
switch nextRelay {
- case let .preSelected(selectedRelay):
- self.selectedRelay = selectedRelay
+ case let .preSelected(selectedRelays):
+ self.selectedRelays = selectedRelays
case .random:
- if let nextRelay = try? pickRelay() {
- self.selectedRelay = nextRelay
+ if let nextRelays = try? pickRelays() {
+ self.selectedRelays = nextRelays
}
case .current:
break
}
- setInternalStateConnected(with: selectedRelay)
+ setInternalStateConnected(with: selectedRelays)
reasserting = false
completionHandler?(nil)
@@ -156,35 +156,28 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate {
}
}
- private func pickRelay() throws -> SelectedRelay {
- let cachedRelays = try relayCacheTracker.getCachedRelays()
+ private func pickRelays() throws -> SelectedRelays {
let tunnelSettings = try SettingsManager.readSettings()
- let selectorResult = try RelaySelector.WireGuard.evaluate(
- by: tunnelSettings.relayConstraints,
- in: cachedRelays.relays,
- numberOfFailedAttempts: 0
- )
- return SelectedRelay(
- endpoint: selectorResult.endpoint,
- hostname: selectorResult.relay.hostname,
- location: selectorResult.location,
- retryAttempts: 0
+
+ return try relaySelector.selectRelays(
+ with: tunnelSettings.relayConstraints,
+ connectionAttemptCount: 0
)
}
- private func setInternalStateConnected(with selectedRelay: SelectedRelay?) {
- guard let selectedRelay = selectedRelay else { return }
+ private func setInternalStateConnected(with selectedRelays: SelectedRelays?) {
+ guard let selectedRelays = selectedRelays else { return }
do {
let settings = try SettingsManager.readSettings()
observedState = .connected(
ObservedConnectionState(
- selectedRelay: selectedRelay,
+ selectedRelays: selectedRelays,
relayConstraints: settings.relayConstraints,
networkReachability: .reachable,
connectionAttemptCount: 0,
transportLayer: .udp,
- remotePort: selectedRelay.endpoint.ipv4Relay.port,
+ remotePort: selectedRelays.exit.endpoint.ipv4Relay.port, // TODO: Multihop
isPostQuantum: settings.tunnelQuantumResistance.isEnabled
)
)
diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
index 4957af7c52..b605b85b47 100644
--- a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
@@ -51,19 +51,19 @@ class MapConnectionStatusOperation: AsyncOperation {
switch observedState {
case let .connected(connectionState):
return connectionState.isNetworkReachable
- ? .connected(connectionState.selectedRelay, isPostQuantum: connectionState.isPostQuantum)
+ ? .connected(connectionState.selectedRelays, isPostQuantum: connectionState.isPostQuantum)
: .waitingForConnectivity(.noConnection)
case let .connecting(connectionState):
return connectionState.isNetworkReachable
- ? .connecting(connectionState.selectedRelay, isPostQuantum: connectionState.isPostQuantum)
+ ? .connecting(connectionState.selectedRelays, isPostQuantum: connectionState.isPostQuantum)
: .waitingForConnectivity(.noConnection)
case let .negotiatingPostQuantumKey(connectionState, privateKey):
return connectionState.isNetworkReachable
- ? .negotiatingPostQuantumKey(connectionState.selectedRelay, privateKey)
+ ? .negotiatingPostQuantumKey(connectionState.selectedRelays, privateKey)
: .waitingForConnectivity(.noConnection)
case let .reconnecting(connectionState):
return connectionState.isNetworkReachable
- ? .reconnecting(connectionState.selectedRelay, isPostQuantum: connectionState.isPostQuantum)
+ ? .reconnecting(connectionState.selectedRelays, isPostQuantum: connectionState.isPostQuantum)
: .waitingForConnectivity(.noConnection)
case let .error(blockedState):
return .error(blockedState.reason)
diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
index cd9e8b7a88..bb125db5ab 100644
--- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
@@ -72,12 +72,12 @@ class StartTunnelOperation: ResultOperation<Void> {
}
private func startTunnel(tunnel: any TunnelProtocol) throws {
- let selectedRelay = try? interactor.selectRelay()
+ let selectedRelays = try? interactor.selectRelays()
var tunnelOptions = PacketTunnelOptions()
do {
- if let selectedRelay {
- try tunnelOptions.setSelectedRelay(selectedRelay)
+ if let selectedRelays {
+ try tunnelOptions.setSelectedRelays(selectedRelays)
}
} catch {
logger.error(
@@ -91,7 +91,7 @@ class StartTunnelOperation: ResultOperation<Void> {
interactor.updateTunnelStatus { tunnelStatus in
tunnelStatus = TunnelStatus()
tunnelStatus.state = .connecting(
- selectedRelay,
+ selectedRelays,
isPostQuantum: interactor.settings.tunnelQuantumResistance.isEnabled
)
}
diff --git a/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift b/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift
index 5299a281cc..04a231c5c4 100644
--- a/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift
+++ b/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift
@@ -22,16 +22,16 @@ private let dispatchQueue = DispatchQueue(label: "Tunnel.dispatchQueue")
private let proxyRequestTimeout = REST.defaultAPINetworkTimeout + 2
extension TunnelProtocol {
- /// Request packet tunnel process to reconnect the tunnel with the given relay.
+ /// Request packet tunnel process to reconnect the tunnel with the given relays.
func reconnectTunnel(
- to nextRelay: NextRelay,
+ to nextRelays: NextRelays,
completionHandler: @escaping (Result<Void, Error>) -> Void
) -> Cancellable {
let operation = SendTunnelProviderMessageOperation(
dispatchQueue: dispatchQueue,
application: .shared,
tunnel: self,
- message: .reconnectTunnel(nextRelay),
+ message: .reconnectTunnel(nextRelays),
completionHandler: completionHandler
)
diff --git a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift
index 3ec3a9791b..3b6735bd39 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import MullvadREST
import MullvadSettings
import PacketTunnelCore
@@ -38,5 +39,5 @@ protocol TunnelInteractor {
func startTunnel()
func prepareForVPNConfigurationDeletion()
- func selectRelay() throws -> SelectedRelay
+ func selectRelays() throws -> SelectedRelays
}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
index 13a2d661dd..d6d4be4d06 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
@@ -59,6 +59,7 @@ final class TunnelManager: StorePaymentObserver {
private var lastMapConnectionStatusOperation: Operation?
private let observerList = ObserverList<TunnelObserver>()
private var networkMonitor: NWPathMonitor?
+ private let relaySelector: RelaySelectorProtocol
private var privateKeyRotationTimer: DispatchSourceTimer?
public private(set) var isRunningPeriodicPrivateKeyRotation = false
@@ -86,7 +87,8 @@ final class TunnelManager: StorePaymentObserver {
accountsProxy: RESTAccountHandling,
devicesProxy: DeviceHandling,
apiProxy: APIQuerying,
- accessTokenManager: RESTAccessTokenManagement
+ accessTokenManager: RESTAccessTokenManagement,
+ relaySelector: RelaySelectorProtocol
) {
self.application = application
self.tunnelStore = tunnelStore
@@ -97,6 +99,7 @@ final class TunnelManager: StorePaymentObserver {
self.operationQueue.name = "TunnelManager.operationQueue"
self.operationQueue.underlyingQueue = internalQueue
self.accessTokenManager = accessTokenManager
+ self.relaySelector = relaySelector
NotificationCenter.default.addObserver(
self,
@@ -779,20 +782,12 @@ final class TunnelManager: StorePaymentObserver {
updateTunnelStatus(tunnel?.status ?? .disconnected)
}
- fileprivate func selectRelay() throws -> SelectedRelay {
- let cachedRelays = try relayCacheTracker.getCachedRelays()
+ fileprivate func selectRelays() throws -> SelectedRelays {
let retryAttempts = tunnelStatus.observedState.connectionState?.connectionAttemptCount ?? 0
- let selectorResult = try RelaySelector.WireGuard.evaluate(
- by: settings.relayConstraints,
- in: cachedRelays.relays,
- numberOfFailedAttempts: retryAttempts
- )
- return SelectedRelay(
- endpoint: selectorResult.endpoint,
- hostname: selectorResult.relay.hostname,
- location: selectorResult.location,
- retryAttempts: retryAttempts
+ return try relaySelector.selectRelays(
+ with: settings.relayConstraints,
+ connectionAttemptCount: retryAttempts
)
}
@@ -1265,8 +1260,8 @@ private struct TunnelInteractorProxy: TunnelInteractor {
tunnelManager.prepareForVPNConfigurationDeletion()
}
- func selectRelay() throws -> SelectedRelay {
- try tunnelManager.selectRelay()
+ func selectRelays() throws -> SelectedRelays {
+ try tunnelManager.selectRelays()
}
func handleRestError(_ error: Error) {
diff --git a/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift b/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift
index eefb1db415..3422c8602d 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift
@@ -187,8 +187,8 @@ extension TunnelState {
value: "Quantum secure connection. Connected to %@, %@",
comment: ""
),
- tunnelInfo.location.city,
- tunnelInfo.location.country
+ tunnelInfo.exit.location.city, // TODO: Multihop
+ tunnelInfo.exit.location.country // TODO: Multihop
)
} else {
String(
@@ -198,8 +198,8 @@ extension TunnelState {
value: "Secure connection. Connected to %@, %@",
comment: ""
),
- tunnelInfo.location.city,
- tunnelInfo.location.country
+ tunnelInfo.exit.location.city, // TODO: Multihop
+ tunnelInfo.exit.location.country // TODO: Multihop
)
}
@@ -219,8 +219,8 @@ extension TunnelState {
value: "Reconnecting to %@, %@",
comment: ""
),
- tunnelInfo.location.city,
- tunnelInfo.location.country
+ tunnelInfo.exit.location.city, // TODO: Multihop
+ tunnelInfo.exit.location.country // TODO: Multihop
)
case .waitingForConnectivity(.noConnection), .error:
diff --git a/ios/MullvadVPN/TunnelManager/TunnelState.swift b/ios/MullvadVPN/TunnelManager/TunnelState.swift
index 76148bdbb8..2c5a6109b6 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelState.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelState.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import MullvadREST
import MullvadTypes
import PacketTunnelCore
import WireGuardKitTypes
@@ -49,13 +50,13 @@ enum TunnelState: Equatable, CustomStringConvertible {
case pendingReconnect
/// Connecting the tunnel.
- case connecting(SelectedRelay?, isPostQuantum: Bool)
+ case connecting(SelectedRelays?, isPostQuantum: Bool)
/// Negotiating a key for post-quantum resistance
- case negotiatingPostQuantumKey(SelectedRelay, PrivateKey)
+ case negotiatingPostQuantumKey(SelectedRelays, PrivateKey)
/// Connected the tunnel
- case connected(SelectedRelay, isPostQuantum: Bool)
+ case connected(SelectedRelays, isPostQuantum: Bool)
/// Disconnecting the tunnel
case disconnecting(ActionAfterDisconnect)
@@ -65,10 +66,10 @@ enum TunnelState: Equatable, CustomStringConvertible {
/// Reconnecting the tunnel.
/// Transition to this state happens when:
- /// 1. Asking the running tunnel to reconnect to new relay via IPC.
- /// 2. Tunnel attempts to reconnect to new relay as the current relay appears to be
+ /// 1. Asking the running tunnel to reconnect to new relays via IPC.
+ /// 2. Tunnel attempts to reconnect to new relays as the current relays appear to be
/// dysfunctional.
- case reconnecting(SelectedRelay, isPostQuantum: Bool)
+ case reconnecting(SelectedRelays, isPostQuantum: Bool)
/// Waiting for connectivity to come back up.
case waitingForConnectivity(WaitingForConnectionReason)
@@ -80,26 +81,26 @@ enum TunnelState: Equatable, CustomStringConvertible {
switch self {
case .pendingReconnect:
"pending reconnect after disconnect"
- case let .connecting(tunnelRelay, isPostQuantum):
- if let tunnelRelay {
- "connecting \(isPostQuantum ? "(PQ) " : "")to \(tunnelRelay.hostname)"
+ case let .connecting(tunnelRelays, isPostQuantum):
+ if let tunnelRelays {
+ "connecting \(isPostQuantum ? "(PQ) " : "")to \(tunnelRelays.exit.hostname)" // TODO: Multihop
} else {
"connecting\(isPostQuantum ? " (PQ)" : ""), fetching relay"
}
- case let .connected(tunnelRelay, isPostQuantum):
- "connected \(isPostQuantum ? "(PQ) " : "")to \(tunnelRelay.hostname)"
+ case let .connected(tunnelRelays, isPostQuantum):
+ "connected \(isPostQuantum ? "(PQ) " : "")to \(tunnelRelays.exit.hostname)" // TODO: Multihop
case let .disconnecting(actionAfterDisconnect):
"disconnecting and then \(actionAfterDisconnect)"
case .disconnected:
"disconnected"
- case let .reconnecting(tunnelRelay, isPostQuantum):
- "reconnecting \(isPostQuantum ? "(PQ) " : "")to \(tunnelRelay.hostname)"
+ case let .reconnecting(tunnelRelays, isPostQuantum):
+ "reconnecting \(isPostQuantum ? "(PQ) " : "")to \(tunnelRelays.exit.hostname)" // TODO: Multihop
case .waitingForConnectivity:
"waiting for connectivity"
case let .error(blockedStateReason):
"error state: \(blockedStateReason)"
- case let .negotiatingPostQuantumKey(tunnelRelay, _):
- "negotiating key with \(tunnelRelay.hostname)"
+ case let .negotiatingPostQuantumKey(tunnelRelays, _):
+ "negotiating key with \(tunnelRelays.exit.hostname)" // TODO: Multihop
}
}
@@ -113,12 +114,12 @@ enum TunnelState: Equatable, CustomStringConvertible {
}
}
- var relay: SelectedRelay? {
+ var relays: SelectedRelays? {
switch self {
- case let .connected(relay, _), let .reconnecting(relay, _), let .negotiatingPostQuantumKey(relay, _):
- relay
- case let .connecting(relay, _):
- relay
+ case let .connected(relays, _), let .reconnecting(relays, _), let .negotiatingPostQuantumKey(relays, _):
+ relays
+ case let .connecting(relays, _):
+ relays
case .disconnecting, .disconnected, .waitingForConnectivity, .pendingReconnect, .error:
nil
}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
index 584a3c7ff3..c627f85fa3 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
@@ -7,6 +7,7 @@
//
import MapKit
+import MullvadREST
import MullvadTypes
import PacketTunnelCore
import UIKit
@@ -144,9 +145,9 @@ final class TunnelControlView: UIView {
updateSecureLabel(tunnelState: tunnelState)
updateActionButtons(tunnelState: tunnelState)
if tunnelState.isSecured {
- updateTunnelRelay(tunnelRelay: tunnelState.relay)
+ updateTunnelRelays(tunnelRelays: tunnelState.relays)
} else {
- updateTunnelRelay(tunnelRelay: nil)
+ updateTunnelRelays(tunnelRelays: nil)
}
}
@@ -223,17 +224,17 @@ final class TunnelControlView: UIView {
connectButtonBlurView.isEnabled = shouldEnableButtons
}
- private func updateTunnelRelay(tunnelRelay: SelectedRelay?) {
- if let tunnelRelay {
+ private func updateTunnelRelays(tunnelRelays: SelectedRelays?) {
+ if let tunnelRelays {
cityLabel.attributedText = attributedStringForLocation(
- string: tunnelRelay.location.city
+ string: tunnelRelays.exit.location.city // TODO: Multihop
)
countryLabel.attributedText = attributedStringForLocation(
- string: tunnelRelay.location.country
+ string: tunnelRelays.exit.location.country // TODO: Multihop
)
connectionPanel.isHidden = false
- connectionPanel.connectedRelayName = tunnelRelay.hostname
+ connectionPanel.connectedRelayName = tunnelRelays.exit.hostname // TODO: Multihop
} else {
countryLabel.attributedText = attributedStringForLocation(string: " ")
cityLabel.attributedText = attributedStringForLocation(string: " ")
diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlViewModel.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlViewModel.swift
index 833583efd3..c0df319d25 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlViewModel.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlViewModel.swift
@@ -18,7 +18,7 @@ struct TunnelControlViewModel {
let outgoingConnectionInfo: OutgoingConnectionInfo?
var connectionPanel: ConnectionPanelData? {
- guard let tunnelRelay = tunnelStatus.state.relay else {
+ guard let tunnelRelays = tunnelStatus.state.relays else {
return nil
}
@@ -29,7 +29,7 @@ struct TunnelControlViewModel {
}
return ConnectionPanelData(
- inAddress: "\(tunnelRelay.endpoint.ipv4Relay.ip)\(portAndTransport)",
+ inAddress: "\(tunnelRelays.exit.endpoint.ipv4Relay.ip)\(portAndTransport)", // TODO: Multihop
outAddress: outgoingConnectionInfo?.outAddress
)
}
diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
index 36f8535047..b5d0dfab64 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
@@ -147,18 +147,18 @@ class TunnelViewController: UIViewController, RootContainment {
private func updateMap(animated: Bool) {
switch tunnelState {
- case let .connecting(tunnelRelay, _):
+ case let .connecting(tunnelRelays, _):
mapViewController.removeLocationMarker()
contentView.setAnimatingActivity(true)
- mapViewController.setCenter(tunnelRelay?.location.geoCoordinate, animated: animated)
+ mapViewController.setCenter(tunnelRelays?.exit.location.geoCoordinate, animated: animated) // TODO: Multihop
- case let .reconnecting(tunnelRelay, _), let .negotiatingPostQuantumKey(tunnelRelay, _):
+ case let .reconnecting(tunnelRelays, _), let .negotiatingPostQuantumKey(tunnelRelays, _):
mapViewController.removeLocationMarker()
contentView.setAnimatingActivity(true)
- mapViewController.setCenter(tunnelRelay.location.geoCoordinate, animated: animated)
+ mapViewController.setCenter(tunnelRelays.exit.location.geoCoordinate, animated: animated) // TODO: Multihop
- case let .connected(tunnelRelay, _):
- let center = tunnelRelay.location.geoCoordinate
+ case let .connected(tunnelRelays, _):
+ let center = tunnelRelays.exit.location.geoCoordinate // TODO: Multihop
mapViewController.setCenter(center, animated: animated) {
self.contentView.setAnimatingActivity(false)
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/RelaySelectorTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift
index 9163613bbe..50df5635a0 100644
--- a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift
+++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift
@@ -24,12 +24,7 @@ class RelaySelectorTests: XCTestCase {
exitLocations: .only(UserSelectedRelays(locations: [.country("es")]))
)
- let result = try RelaySelector.WireGuard.evaluate(
- by: constraints,
- in: sampleRelays,
- numberOfFailedAttempts: 0
- )
-
+ let result = try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 0)
XCTAssertEqual(result.relay.hostname, "es1-wireguard")
}
@@ -38,11 +33,7 @@ class RelaySelectorTests: XCTestCase {
exitLocations: .only(UserSelectedRelays(locations: [.city("se", "got")]))
)
- let result = try RelaySelector.WireGuard.evaluate(
- by: constraints,
- in: sampleRelays,
- numberOfFailedAttempts: 0
- )
+ let result = try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 0)
XCTAssertEqual(result.relay.hostname, "se10-wireguard")
}
@@ -51,12 +42,7 @@ class RelaySelectorTests: XCTestCase {
exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")]))
)
- let result = try RelaySelector.WireGuard.evaluate(
- by: constraints,
- in: sampleRelays,
- numberOfFailedAttempts: 0
- )
-
+ let result = try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 0)
XCTAssertEqual(result.relay.hostname, "se6-wireguard")
}
@@ -87,7 +73,6 @@ class RelaySelectorTests: XCTestCase {
let constrainedLocations = RelaySelector.applyConstraints(
constraints.exitLocations,
- portConstraint: constraints.port,
filterConstraint: constraints.filter,
relays: relayWithLocations
)
@@ -111,12 +96,7 @@ class RelaySelectorTests: XCTestCase {
port: .only(1)
)
- let result = try RelaySelector.WireGuard.evaluate(
- by: constraints,
- in: sampleRelays,
- numberOfFailedAttempts: 0
- )
-
+ let result = try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 0)
XCTAssertEqual(result.endpoint.ipv4Relay.port, 1)
}
@@ -126,39 +106,19 @@ class RelaySelectorTests: XCTestCase {
)
let allPorts = portRanges.flatMap { $0 }
- var result = try RelaySelector.WireGuard.evaluate(
- by: constraints,
- in: sampleRelays,
- numberOfFailedAttempts: 0
- )
+ var result = try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 0)
XCTAssertTrue(allPorts.contains(result.endpoint.ipv4Relay.port))
- result = try RelaySelector.WireGuard.evaluate(
- by: constraints,
- in: sampleRelays,
- numberOfFailedAttempts: 1
- )
+ result = try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 1)
XCTAssertTrue(allPorts.contains(result.endpoint.ipv4Relay.port))
- result = try RelaySelector.WireGuard.evaluate(
- by: constraints,
- in: sampleRelays,
- numberOfFailedAttempts: 2
- )
+ result = try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 2)
XCTAssertEqual(result.endpoint.ipv4Relay.port, defaultPort)
- result = try RelaySelector.WireGuard.evaluate(
- by: constraints,
- in: sampleRelays,
- numberOfFailedAttempts: 3
- )
+ result = try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 3)
XCTAssertEqual(result.endpoint.ipv4Relay.port, defaultPort)
- result = try RelaySelector.WireGuard.evaluate(
- by: constraints,
- in: sampleRelays,
- numberOfFailedAttempts: 4
- )
+ result = try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 4)
XCTAssertTrue(allPorts.contains(result.endpoint.ipv4Relay.port))
}
@@ -200,12 +160,7 @@ class RelaySelectorTests: XCTestCase {
filter: .only(filter)
)
- let result = try RelaySelector.WireGuard.evaluate(
- by: constraints,
- in: sampleRelays,
- numberOfFailedAttempts: 0
- )
-
+ let result = try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 0)
XCTAssertTrue(result.relay.owned)
}
@@ -217,13 +172,7 @@ class RelaySelectorTests: XCTestCase {
filter: .only(filter)
)
- let result = try? RelaySelector.WireGuard.evaluate(
- by: constraints,
- in: sampleRelays,
- numberOfFailedAttempts: 0
- )
-
- XCTAssertNil(result)
+ XCTAssertThrowsError(try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 0))
}
func testRelayFilterConstraintWithCorrectProvider() throws {
@@ -235,12 +184,7 @@ class RelaySelectorTests: XCTestCase {
filter: .only(filter)
)
- let result = try RelaySelector.WireGuard.evaluate(
- by: constraints,
- in: sampleRelays,
- numberOfFailedAttempts: 0
- )
-
+ let result = try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 0)
XCTAssertEqual(result.relay.provider, provider)
}
@@ -253,14 +197,27 @@ class RelaySelectorTests: XCTestCase {
filter: .only(filter)
)
- let result = try? RelaySelector.WireGuard.evaluate(
- by: constraints,
- in: sampleRelays,
- numberOfFailedAttempts: 0
+ XCTAssertThrowsError(try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 0))
+ }
+}
+
+extension RelaySelectorTests {
+ private func pickRelay(
+ by constraints: RelayConstraints,
+ in relays: REST.ServerRelaysResponse,
+ failedAttemptCount: UInt
+ ) throws -> RelaySelectorMatch {
+ let candidates = try RelaySelector.WireGuard.findCandidates(
+ by: constraints.exitLocations,
+ in: relays,
+ filterConstraint: constraints.filter
)
- XCTAssertNil(result)
+ return try RelaySelector.WireGuard.pickCandidate(
+ from: candidates,
+ relays: relays,
+ portConstraint: constraints.port,
+ numberOfFailedAttempts: failedAttemptCount
+ )
}
-
- // MARK: - Multi-Hop tests
}
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 a57d78bd39..637e47c89e 100644
--- a/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift
+++ b/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift
@@ -6,38 +6,26 @@
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//
+import MullvadMockData
import MullvadTypes
@testable import PacketTunnelCore
import WireGuardKitTypes
import XCTest
final class PacketTunnelActorReducerTests: XCTestCase {
- // test data
- let selectedRelay = SelectedRelay(
- endpoint: MullvadEndpoint(
- ipv4Relay: IPv4Endpoint(ip: .loopback, port: 1300),
- ipv4Gateway: .loopback,
- ipv6Gateway: .loopback,
- publicKey: PrivateKey().publicKey.rawValue
- ),
- hostname: "se-got",
- location: Location(
- country: "",
- countryCode: "se",
- city: "",
- cityCode: "got",
- latitude: 0,
- longitude: 0
- ), retryAttempts: 0
- )
+ // swiftlint:disable:next force_try
+ let selectedRelays = try! RelaySelectorStub
+ .nonFallible()
+ .selectRelays(with: RelayConstraints(), connectionAttemptCount: 0)
+
func makeConnectionData(keyPolicy: State.KeyPolicy = .useCurrent) -> State.ConnectionData {
State.ConnectionData(
- selectedRelay: selectedRelay,
+ selectedRelays: selectedRelays,
relayConstraints: RelayConstraints(),
keyPolicy: keyPolicy,
networkReachability: .reachable,
connectionAttemptCount: 0,
- connectedEndpoint: selectedRelay.endpoint,
+ connectedEndpoint: selectedRelays.exit.endpoint, // TODO: Multihop
transportLayer: .udp,
remotePort: 12345,
isPostQuantum: false
@@ -65,13 +53,13 @@ final class PacketTunnelActorReducerTests: XCTestCase {
// When
let effects = PacketTunnelActor.Reducer.reduce(
&state,
- .start(StartOptions(launchSource: .app, selectedRelay: selectedRelay))
+ .start(StartOptions(launchSource: .app, selectedRelays: selectedRelays))
)
// Then
XCTAssertEqual(effects, [
.startDefaultPathObserver,
.startTunnelMonitor,
- .startConnection(.preSelected(selectedRelay)),
+ .startConnection(.preSelected(selectedRelays)),
])
}
diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift
index 49784143e8..3da5215922 100644
--- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift
+++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift
@@ -7,16 +7,16 @@
//
import Foundation
+import MullvadREST
import MullvadSettings
-import PacketTunnelCore
// this is still very minimal, and will be fleshed out as needed.
class MockTunnelInteractor: TunnelInteractor {
var isConfigurationLoaded: Bool
- var settings: MullvadSettings.LatestTunnelSettings
+ var settings: LatestTunnelSettings
- var deviceState: MullvadSettings.DeviceState
+ var deviceState: DeviceState
var onUpdateTunnelStatus: ((TunnelStatus) -> Void)?
@@ -24,8 +24,8 @@ class MockTunnelInteractor: TunnelInteractor {
init(
isConfigurationLoaded: Bool,
- settings: MullvadSettings.LatestTunnelSettings,
- deviceState: MullvadSettings.DeviceState,
+ settings: LatestTunnelSettings,
+ deviceState: DeviceState,
onUpdateTunnelStatus: ((TunnelStatus) -> Void)? = nil
) {
self.isConfigurationLoaded = isConfigurationLoaded
@@ -59,9 +59,9 @@ class MockTunnelInteractor: TunnelInteractor {
func setConfigurationLoaded() {}
- func setSettings(_ settings: MullvadSettings.LatestTunnelSettings, persist: Bool) {}
+ func setSettings(_ settings: LatestTunnelSettings, persist: Bool) {}
- func setDeviceState(_ deviceState: MullvadSettings.DeviceState, persist: Bool) {}
+ func setDeviceState(_ deviceState: DeviceState, persist: Bool) {}
func removeLastUsedAccount() {}
@@ -73,7 +73,7 @@ class MockTunnelInteractor: TunnelInteractor {
struct NotImplementedError: Error {}
- func selectRelay() throws -> PacketTunnelCore.SelectedRelay {
+ func selectRelays() throws -> SelectedRelays {
throw NotImplementedError()
}
}
diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift
index e9843b5dab..3b9dff23d0 100644
--- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift
+++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift
@@ -33,6 +33,7 @@ final class TunnelManagerTests: XCTestCase {
let devicesProxy = DevicesProxyStub(deviceResult: .success(Device.mock(publicKey: PrivateKey().publicKey)))
let apiProxy = APIProxyStub()
let accessTokenManager = AccessTokenManagerStub()
+ let relaySelector = RelaySelectorStub.nonFallible()
let tunnelManager = TunnelManager(
application: application,
tunnelStore: tunnelStore,
@@ -40,7 +41,8 @@ final class TunnelManagerTests: XCTestCase {
accountsProxy: accountProxy,
devicesProxy: devicesProxy,
apiProxy: apiProxy,
- accessTokenManager: accessTokenManager
+ accessTokenManager: accessTokenManager,
+ relaySelector: relaySelector
)
XCTAssertNotNil(tunnelManager)
}
@@ -54,6 +56,7 @@ final class TunnelManagerTests: XCTestCase {
let apiProxy = APIProxyStub()
let accessTokenManager = AccessTokenManagerStub()
accountProxy.createAccountResult = .success(REST.NewAccountData.mockValue())
+ let relaySelector = RelaySelectorStub.nonFallible()
let tunnelManager = TunnelManager(
application: application,
tunnelStore: tunnelStore,
@@ -61,7 +64,8 @@ final class TunnelManagerTests: XCTestCase {
accountsProxy: accountProxy,
devicesProxy: devicesProxy,
apiProxy: apiProxy,
- accessTokenManager: accessTokenManager
+ accessTokenManager: accessTokenManager,
+ relaySelector: relaySelector
)
_ = try await tunnelManager.setNewAccount()
XCTAssertEqual(tunnelManager.isRunningPeriodicPrivateKeyRotation, true)
@@ -76,6 +80,7 @@ final class TunnelManagerTests: XCTestCase {
let apiProxy = APIProxyStub()
let accessTokenManager = AccessTokenManagerStub()
accountProxy.createAccountResult = .success(REST.NewAccountData.mockValue())
+ let relaySelector = RelaySelectorStub.nonFallible()
let tunnelManager = TunnelManager(
application: application,
tunnelStore: tunnelStore,
@@ -83,7 +88,8 @@ final class TunnelManagerTests: XCTestCase {
accountsProxy: accountProxy,
devicesProxy: devicesProxy,
apiProxy: apiProxy,
- accessTokenManager: accessTokenManager
+ accessTokenManager: accessTokenManager,
+ relaySelector: relaySelector
)
_ = try await tunnelManager.setNewAccount()
await tunnelManager.unsetAccount()
diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
index ab464a2274..cf81a3b1b4 100644
--- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
@@ -92,10 +92,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
protocolObfuscator: ProtocolObfuscator<UDPOverTCPObfuscator>()
)
- postQuantumActor = PostQuantumKeyExchangeActor(
- packetTunnel: self,
- onFailure: self.keyExchangeFailed
- )
+ postQuantumActor = PostQuantumKeyExchangeActor(packetTunnel: self, onFailure: self.keyExchangeFailed)
let urlRequestProxy = URLRequestProxy(dispatchQueue: internalQueue, transportProvider: transportProvider)
appMessageHandler = AppMessageHandler(packetTunnelActor: actor, urlRequestProxy: urlRequestProxy)
@@ -210,9 +207,9 @@ extension PacketTunnelProvider {
var parsedOptions = StartOptions(launchSource: tunnelOptions.isOnDemand() ? .onDemand : .app)
do {
- if let selectedRelay = try tunnelOptions.getSelectedRelay() {
+ if let selectedRelays = try tunnelOptions.getSelectedRelays() {
parsedOptions.launchSource = .app
- parsedOptions.selectedRelay = selectedRelay
+ parsedOptions.selectedRelays = selectedRelays
} else if !tunnelOptions.isOnDemand() {
parsedOptions.launchSource = .system
}
diff --git a/ios/PacketTunnelCore/Actor/ObservedState.swift b/ios/PacketTunnelCore/Actor/ObservedState.swift
index bdb85a8e51..43d99fdfea 100644
--- a/ios/PacketTunnelCore/Actor/ObservedState.swift
+++ b/ios/PacketTunnelCore/Actor/ObservedState.swift
@@ -8,6 +8,7 @@
import Combine
import Foundation
+import MullvadREST
import MullvadTypes
import Network
import WireGuardKitTypes
@@ -26,7 +27,7 @@ public enum ObservedState: Equatable, Codable {
/// A serializable representation of internal connection state.
public struct ObservedConnectionState: Equatable, Codable {
- public var selectedRelay: SelectedRelay
+ public var selectedRelays: SelectedRelays
public var relayConstraints: RelayConstraints
public var networkReachability: NetworkReachability
public var connectionAttemptCount: UInt
@@ -40,7 +41,7 @@ public struct ObservedConnectionState: Equatable, Codable {
}
public init(
- selectedRelay: SelectedRelay,
+ selectedRelays: SelectedRelays,
relayConstraints: RelayConstraints,
networkReachability: NetworkReachability,
connectionAttemptCount: UInt,
@@ -49,7 +50,7 @@ public struct ObservedConnectionState: Equatable, Codable {
lastKeyRotation: Date? = nil,
isPostQuantum: Bool
) {
- self.selectedRelay = selectedRelay
+ self.selectedRelays = selectedRelays
self.relayConstraints = relayConstraints
self.networkReachability = networkReachability
self.connectionAttemptCount = connectionAttemptCount
@@ -94,7 +95,7 @@ extension State.ConnectionData {
/// Map `State.ConnectionData` to `ObservedConnectionState`.
var observedConnectionState: ObservedConnectionState {
ObservedConnectionState(
- selectedRelay: selectedRelay,
+ selectedRelays: selectedRelays,
relayConstraints: relayConstraints,
networkReachability: networkReachability,
connectionAttemptCount: connectionAttemptCount,
diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift
index d8a5a8772c..30348546f1 100644
--- a/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift
+++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+PostQuantum.swift
@@ -16,10 +16,10 @@ extension PacketTunnelActor {
*/
internal func tryStartPostQuantumNegotiation(
withSettings settings: Settings,
- nextRelay: NextRelay,
+ nextRelays: NextRelays,
reason: ActorReconnectReason
) async throws {
- if let connectionState = try obfuscateConnection(nextRelay: nextRelay, settings: settings, reason: reason) {
+ if let connectionState = try obfuscateConnection(nextRelays: nextRelays, settings: settings, reason: reason) {
let selectedEndpoint = connectionState.connectedEndpoint
let activeKey = activeKey(from: connectionState, in: settings)
@@ -44,18 +44,18 @@ extension PacketTunnelActor {
internal func postQuantumConnect(with key: PreSharedKey, privateKey: PrivateKey) async {
guard
// It is important to select the same relay that was saved in the connection state as the key negotiation happened with this specific relay.
- let selectedRelay = state.connectionData?.selectedRelay,
+ let selectedRelays = state.connectionData?.selectedRelays,
let settings: Settings = try? settingsReader.read(),
let connectionState = try? obfuscateConnection(
- nextRelay: .preSelected(selectedRelay),
+ nextRelays: .preSelected(selectedRelays),
settings: settings,
reason: .userInitiated
)
else {
logger.error("Could not create connection state in PostQuantumConnect")
- let nextRelay: NextRelay = (state.connectionData?.selectedRelay).map { .preSelected($0) } ?? .current
- eventChannel.send(.reconnect(nextRelay))
+ let nextRelays: NextRelays = (state.connectionData?.selectedRelays).map { .preSelected($0) } ?? .current
+ eventChannel.send(.reconnect(nextRelays))
return
}
@@ -76,7 +76,7 @@ extension PacketTunnelActor {
try? await tunnelAdapter.start(configuration: configurationBuilder.makeConfiguration())
// Resume tunnel monitoring and use IPv4 gateway as a probe address.
- tunnelMonitor.start(probeAddress: connectionState.selectedRelay.endpoint.ipv4Gateway)
+ tunnelMonitor.start(probeAddress: connectionState.selectedRelays.exit.endpoint.ipv4Gateway) // TODO: Multihop
// Restart default path observer and notify the observer with the current path that might have changed while
// path observer was paused.
startDefaultPathObserver(notifyObserverWithCurrentPath: false)
diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift
index 5ba42a0bd2..160fd9bbe6 100644
--- a/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift
+++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+Public.swift
@@ -36,10 +36,10 @@ extension PacketTunnelActor {
/**
Tell actor to reconnect the tunnel.
- - Parameter nextRelay: next relay to connect to.
+ - Parameter nextRelays: next relays to connect to.
*/
- public nonisolated func reconnect(to nextRelay: NextRelay, reconnectReason: ActorReconnectReason) {
- eventChannel.send(.reconnect(nextRelay, reason: reconnectReason))
+ public nonisolated func reconnect(to nextRelays: NextRelays, reconnectReason: ActorReconnectReason) {
+ eventChannel.send(.reconnect(nextRelays, reason: reconnectReason))
}
/**
diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift
index f497df07ae..715cfd840d 100644
--- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift
+++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift
@@ -8,6 +8,7 @@
import Foundation
import MullvadLogging
+import MullvadREST
import MullvadTypes
import NetworkExtension
import TunnelObfuscation
@@ -109,16 +110,16 @@ public actor PacketTunnelActor {
tunnelMonitor.stop()
case let .updateTunnelMonitorPath(networkPath):
handleDefaultPathChange(networkPath)
- case let .startConnection(nextRelay):
+ case let .startConnection(nextRelays):
do {
- try await tryStart(nextRelay: nextRelay)
+ try await tryStart(nextRelays: nextRelays)
} catch {
logger.error(error: error, message: "Failed to start the tunnel.")
await setErrorStateInternal(with: error)
}
- case let .restartConnection(nextRelay, reason):
+ case let .restartConnection(nextRelays, reason):
do {
- try await tryStart(nextRelay: nextRelay, reason: reason)
+ try await tryStart(nextRelays: nextRelays, reason: reason)
} catch {
logger.error(error: error, message: "Failed to reconnect the tunnel.")
await setErrorStateInternal(with: error)
@@ -167,7 +168,7 @@ extension PacketTunnelActor {
setTunnelMonitorEventHandler()
do {
- try await tryStart(nextRelay: options.selectedRelay.map { .preSelected($0) } ?? .random)
+ try await tryStart(nextRelays: options.selectedRelays.map { .preSelected($0) } ?? .random)
} catch {
logger.error(error: error, message: "Failed to start the tunnel.")
@@ -205,13 +206,13 @@ extension PacketTunnelActor {
}
/**
- Reconnect tunnel to new relay. Enters error state on failure.
+ Reconnect tunnel to new relays. Enters error state on failure.
- Parameters:
- - nextRelay: next relay to connect to
+ - nextRelay: next relays to connect to
- reason: reason for reconnect
*/
- private func reconnect(to nextRelay: NextRelay, reason: ActorReconnectReason) async {
+ private func reconnect(to nextRelays: NextRelays, reason: ActorReconnectReason) async {
do {
switch state {
// There is no connection monitoring going on when exchanging keys.
@@ -226,7 +227,7 @@ extension PacketTunnelActor {
tunnelMonitor.stop()
}
- try await tryStart(nextRelay: nextRelay, reason: reason)
+ try await tryStart(nextRelays: nextRelays, reason: reason)
case .disconnected, .disconnecting, .initial:
break
@@ -245,15 +246,15 @@ extension PacketTunnelActor {
- Start either a direct connection or the post-quantum key negotiation process, depending on settings.
*/
private func tryStart(
- nextRelay: NextRelay,
+ nextRelays: NextRelays,
reason: ActorReconnectReason = .userInitiated
) async throws {
let settings: Settings = try settingsReader.read()
if settings.quantumResistance.isEnabled {
- try await tryStartPostQuantumNegotiation(withSettings: settings, nextRelay: nextRelay, reason: reason)
+ try await tryStartPostQuantumNegotiation(withSettings: settings, nextRelays: nextRelays, reason: reason)
} else {
- try await tryStartConnection(withSettings: settings, nextRelay: nextRelay, reason: reason)
+ try await tryStartConnection(withSettings: settings, nextRelays: nextRelays, reason: reason)
}
}
@@ -268,15 +269,15 @@ extension PacketTunnelActor {
- Reactivate default path observation (disabled when configuring tunnel adapter)
- Parameters:
- - nextRelay: which relay should be selected next.
+ - nextRelays: which relays should be selected next.
- reason: reason for reconnect
*/
private func tryStartConnection(
withSettings settings: Settings,
- nextRelay: NextRelay,
+ nextRelays: NextRelays,
reason: ActorReconnectReason
) async throws {
- guard let connectionState = try obfuscateConnection(nextRelay: nextRelay, settings: settings, reason: reason),
+ guard let connectionState = try obfuscateConnection(nextRelays: nextRelays, settings: settings, reason: reason),
let targetState = state.targetStateForReconnect else { return }
let activeKey = activeKey(from: connectionState, in: settings)
@@ -315,21 +316,21 @@ extension PacketTunnelActor {
try await tunnelAdapter.start(configuration: configurationBuilder.makeConfiguration())
// Resume tunnel monitoring and use IPv4 gateway as a probe address.
- tunnelMonitor.start(probeAddress: connectionState.selectedRelay.endpoint.ipv4Gateway)
+ tunnelMonitor.start(probeAddress: connectionState.selectedRelays.exit.endpoint.ipv4Gateway) // TODO: Multihop
}
/**
- Derive `ConnectionState` from current `state` updating it with new relay and settings.
+ Derive `ConnectionState` from current `state` updating it with new relays and settings.
- Parameters:
- - nextRelay: relay preference that should be used when selecting next relay.
+ - nextRelays: relay preference that should be used when selecting next relays.
- settings: current settings
- reason: reason for reconnect
- Returns: New connection state or `nil` if current state is at or past `.disconnecting` phase.
*/
internal func makeConnectionState(
- nextRelay: NextRelay,
+ nextRelays: NextRelays,
settings: Settings,
reason: ActorReconnectReason
) throws -> State.ConnectionData? {
@@ -337,11 +338,11 @@ extension PacketTunnelActor {
var networkReachability = defaultPathObserver.defaultPath?.networkReachability ?? .undetermined
var lastKeyRotation: Date?
- let callRelaySelector = { [self] maybeCurrentRelay, connectionCount in
- try self.selectRelay(
- nextRelay: nextRelay,
+ let callRelaySelector = { [self] maybeCurrentRelays, connectionCount in
+ try self.selectRelays(
+ nextRelays: nextRelays,
relayConstraints: settings.relayConstraints,
- currentRelay: maybeCurrentRelay,
+ currentRelays: maybeCurrentRelays,
connectionAttemptCount: connectionCount
)
}
@@ -354,11 +355,11 @@ extension PacketTunnelActor {
if reason == .connectionLoss {
connectionState.incrementAttemptCount()
}
- let selectedRelay = try callRelaySelector(
- connectionState.selectedRelay,
+ let selectedRelays = try callRelaySelector(
+ connectionState.selectedRelays,
connectionState.connectionAttemptCount
)
- connectionState.selectedRelay = selectedRelay
+ connectionState.selectedRelays = selectedRelays
connectionState.relayConstraints = settings.relayConstraints
return connectionState
case var .connecting(connectionState), var .reconnecting(connectionState):
@@ -367,11 +368,11 @@ extension PacketTunnelActor {
}
fallthrough
case var .connected(connectionState):
- let selectedRelay = try callRelaySelector(
- connectionState.selectedRelay,
+ let selectedRelays = try callRelaySelector(
+ connectionState.selectedRelays,
connectionState.connectionAttemptCount
)
- connectionState.selectedRelay = selectedRelay
+ connectionState.selectedRelays = selectedRelays
connectionState.relayConstraints = settings.relayConstraints
connectionState.currentKey = settings.privateKey
return connectionState
@@ -382,18 +383,18 @@ extension PacketTunnelActor {
case .disconnecting, .disconnected:
return nil
}
- let selectedRelay = try callRelaySelector(nil, 0)
+ let selectedRelays = try callRelaySelector(nil, 0)
return State.ConnectionData(
- selectedRelay: selectedRelay,
+ selectedRelays: selectedRelays,
relayConstraints: settings.relayConstraints,
currentKey: settings.privateKey,
keyPolicy: keyPolicy,
networkReachability: networkReachability,
connectionAttemptCount: 0,
lastKeyRotation: lastKeyRotation,
- connectedEndpoint: selectedRelay.endpoint,
+ connectedEndpoint: selectedRelays.exit.endpoint, // TODO: Multihop
transportLayer: .udp,
- remotePort: selectedRelay.endpoint.ipv4Relay.port,
+ remotePort: selectedRelays.exit.endpoint.ipv4Relay.port, // TODO: Multihop
isPostQuantum: settings.quantumResistance.isEnabled
)
}
@@ -408,22 +409,22 @@ extension PacketTunnelActor {
}
internal func obfuscateConnection(
- nextRelay: NextRelay,
+ nextRelays: NextRelays,
settings: Settings,
reason: ActorReconnectReason
) throws -> State.ConnectionData? {
- guard let connectionState = try makeConnectionState(nextRelay: nextRelay, settings: settings, reason: reason)
+ guard let connectionState = try makeConnectionState(nextRelays: nextRelays, settings: settings, reason: reason)
else { return nil }
let obfuscatedEndpoint = protocolObfuscator.obfuscate(
- connectionState.selectedRelay.endpoint,
+ connectionState.selectedRelays.exit.endpoint, // TODO: Multihop
settings: settings,
- retryAttempts: connectionState.selectedRelay.retryAttempts
+ retryAttempts: connectionState.selectedRelays.exit.retryAttempts // TODO: Multihop
)
let transportLayer = protocolObfuscator.transportLayer.map { $0 } ?? .udp
return State.ConnectionData(
- selectedRelay: connectionState.selectedRelay,
+ selectedRelays: connectionState.selectedRelays,
relayConstraints: connectionState.relayConstraints,
currentKey: settings.privateKey,
keyPolicy: connectionState.keyPolicy,
@@ -438,39 +439,39 @@ extension PacketTunnelActor {
}
/**
- Select next relay to connect to based on `NextRelay` and other input parameters.
+ Select next relay to connect to based on `NextRelays` and other input parameters.
- Parameters:
- - nextRelay: next relay to connect to.
+ - nextRelays: next relays to connect to.
- relayConstraints: relay constraints.
- - currentRelay: currently selected relay.
+ - currentRelays: currently selected relays.
- connectionAttemptCount: number of failed connection attempts so far.
- - Returns: selector result that contains the credentials of the next relay that the tunnel should connect to.
+ - Returns: selector result that contains the credentials of the next relays that the tunnel should connect to.
*/
- private func selectRelay(
- nextRelay: NextRelay,
+ private func selectRelays(
+ nextRelays: NextRelays,
relayConstraints: RelayConstraints,
- currentRelay: SelectedRelay?,
+ currentRelays: SelectedRelays?,
connectionAttemptCount: UInt
- ) throws -> SelectedRelay {
- switch nextRelay {
+ ) throws -> SelectedRelays {
+ switch nextRelays {
case .current:
- if let currentRelay {
- return currentRelay
+ if let currentRelays {
+ return currentRelays
} else {
- // Fallthrough to .random when current relay is not set.
+ // Fallthrough to .random when current relays are not set.
fallthrough
}
case .random:
- return try relaySelector.selectRelay(
+ return try relaySelector.selectRelays(
with: relayConstraints,
- connectionAttemptFailureCount: connectionAttemptCount
+ connectionAttemptCount: connectionAttemptCount
)
- case let .preSelected(selectedRelay):
- return selectedRelay
+ case let .preSelected(selectedRelays):
+ return selectedRelays
}
}
}
diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift
index 21e2aca702..fd731a32e1 100644
--- a/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift
+++ b/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift
@@ -19,7 +19,7 @@ extension PacketTunnelActor {
case stop
/// Reconnect tunnel.
- case reconnect(NextRelay, reason: ActorReconnectReason = .userInitiated)
+ case reconnect(NextRelays, reason: ActorReconnectReason = .userInitiated)
/// Enter blocked state.
case error(BlockedStateReason)
@@ -46,14 +46,14 @@ extension PacketTunnelActor {
return "start"
case .stop:
return "stop"
- case let .reconnect(nextRelay, stopTunnelMonitor):
- switch nextRelay {
+ case let .reconnect(nextRelays, stopTunnelMonitor):
+ switch nextRelays {
case .current:
return "reconnect(current, \(stopTunnelMonitor))"
case .random:
return "reconnect(random, \(stopTunnelMonitor))"
- case let .preSelected(selectedRelay):
- return "reconnect(\(selectedRelay.hostname), \(stopTunnelMonitor))"
+ case let .preSelected(selectedRelays):
+ return "reconnect(\(selectedRelays.exit.hostname), \(stopTunnelMonitor))" // TODO: Multihop
}
case let .error(reason):
return "error(\(reason))"
diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActorProtocol.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActorProtocol.swift
index df34f768cb..02729d449c 100644
--- a/ios/PacketTunnelCore/Actor/PacketTunnelActorProtocol.swift
+++ b/ios/PacketTunnelCore/Actor/PacketTunnelActorProtocol.swift
@@ -11,6 +11,6 @@ import Foundation
public protocol PacketTunnelActorProtocol {
var observedState: ObservedState { get async }
- func reconnect(to nextRelay: NextRelay, reconnectReason: ActorReconnectReason)
+ func reconnect(to nextRelays: NextRelays, reconnectReason: ActorReconnectReason)
func notifyKeyRotation(date: Date?)
}
diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift
index 200da62fff..6d4056b683 100644
--- a/ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift
+++ b/ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift
@@ -17,10 +17,10 @@ extension PacketTunnelActor {
case startTunnelMonitor
case stopTunnelMonitor
case updateTunnelMonitorPath(NetworkPath)
- case startConnection(NextRelay)
- case restartConnection(NextRelay, ActorReconnectReason)
+ case startConnection(NextRelays)
+ case restartConnection(NextRelays, ActorReconnectReason)
// trigger a reconnect, which becomes several effects depending on the state
- case reconnect(NextRelay)
+ case reconnect(NextRelays)
case stopTunnelAdapter
case configureForErrorState(BlockedStateReason)
case cacheActiveKey(Date?)
@@ -58,7 +58,7 @@ extension PacketTunnelActor {
return [
.startDefaultPathObserver,
.startTunnelMonitor,
- .startConnection(options.selectedRelay.map { .preSelected($0) } ?? .random),
+ .startConnection(options.selectedRelays.map { .preSelected($0) } ?? .random),
]
case .stop:
return subreducerForStop(&state)
@@ -124,7 +124,7 @@ extension PacketTunnelActor {
fileprivate static func subreducerForReconnect(
_ state: State,
_ reason: ActorReconnectReason,
- _ nextRelay: NextRelay
+ _ nextRelays: NextRelays
) -> [PacketTunnelActor.Effect] {
switch state {
case .disconnected, .disconnecting, .initial:
@@ -133,9 +133,9 @@ extension PacketTunnelActor {
return []
case .connecting, .connected, .reconnecting, .error, .negotiatingPostQuantumKey:
if reason == .userInitiated {
- return [.stopTunnelMonitor, .restartConnection(nextRelay, reason)]
+ return [.stopTunnelMonitor, .restartConnection(nextRelays, reason)]
} else {
- return [.restartConnection(nextRelay, reason)]
+ return [.restartConnection(nextRelays, reason)]
}
}
}
diff --git a/ios/PacketTunnelCore/Actor/StartOptions.swift b/ios/PacketTunnelCore/Actor/StartOptions.swift
index 9dd3ffeb68..4c8ad75878 100644
--- a/ios/PacketTunnelCore/Actor/StartOptions.swift
+++ b/ios/PacketTunnelCore/Actor/StartOptions.swift
@@ -7,26 +7,27 @@
//
import Foundation
+import MullvadREST
/// Packet tunnel start options parsed from dictionary passed to packet tunnel with a call to `startTunnel()`.
public struct StartOptions {
/// The system that triggered the launch of packet tunnel.
public var launchSource: LaunchSource
- /// Pre-selected relay received from UI when available.
- public var selectedRelay: SelectedRelay?
+ /// Pre-selected relays received from UI when available.
+ public var selectedRelays: SelectedRelays?
/// Designated initializer.
- public init(launchSource: LaunchSource, selectedRelay: SelectedRelay? = nil) {
+ public init(launchSource: LaunchSource, selectedRelays: SelectedRelays? = nil) {
self.launchSource = launchSource
- self.selectedRelay = selectedRelay
+ self.selectedRelays = selectedRelays
}
/// Returns a brief description suitable for output to tunnel provider log.
public func logFormat() -> String {
var s = "Start the tunnel via \(launchSource)"
- if let selectedRelay {
- s += ", connect to \(selectedRelay.hostname)"
+ if let selectedRelays {
+ s += ", connect to \(selectedRelays.exit.hostname)" // TODO: Multihop
}
s += "."
return s
diff --git a/ios/PacketTunnelCore/Actor/State+Extensions.swift b/ios/PacketTunnelCore/Actor/State+Extensions.swift
index be1f05d52d..69d6579a9d 100644
--- a/ios/PacketTunnelCore/Actor/State+Extensions.swift
+++ b/ios/PacketTunnelCore/Actor/State+Extensions.swift
@@ -47,7 +47,7 @@ extension State {
func logFormat() -> String {
switch self {
case let .connecting(connState), let .connected(connState), let .reconnecting(connState):
- let hostname = connState.selectedRelay.hostname
+ let hostname = connState.selectedRelays.exit.hostname // TODO: Multihop
return """
\(name) to \(hostname), \
diff --git a/ios/PacketTunnelCore/Actor/State.swift b/ios/PacketTunnelCore/Actor/State.swift
index 1afc4ca768..0ae4c79e3e 100644
--- a/ios/PacketTunnelCore/Actor/State.swift
+++ b/ios/PacketTunnelCore/Actor/State.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import MullvadREST
import MullvadTypes
import TunnelObfuscation
import WireGuardKitTypes
@@ -109,8 +110,8 @@ extension State {
/// Data associated with states that hold connection data.
struct ConnectionData: Equatable, StateAssociatedData {
- /// Current selected relay.
- public var selectedRelay: SelectedRelay
+ /// Current selected relays.
+ public var selectedRelays: SelectedRelays
/// Last relay constraints read from settings.
/// This is primarily used by packet tunnel for updating constraints in tunnel provider.
@@ -228,15 +229,15 @@ extension State.BlockingData {
}
/// Describes which relay the tunnel should connect to next.
-public enum NextRelay: Equatable, Codable {
- /// Select next relay randomly.
+public enum NextRelays: Equatable, Codable {
+ /// Select next relays randomly.
case random
- /// Use currently selected relay, fallback to random if not set.
+ /// Use currently selected relays, fallback to random if not set.
case current
- /// Use pre-selected relay.
- case preSelected(SelectedRelay)
+ /// Use pre-selected relays.
+ case preSelected(SelectedRelays)
}
/// Describes the reason for reconnection request.
diff --git a/ios/PacketTunnelCore/IPC/PacketTunnelOptions.swift b/ios/PacketTunnelCore/IPC/PacketTunnelOptions.swift
index 742fb1f12f..79bc030706 100644
--- a/ios/PacketTunnelCore/IPC/PacketTunnelOptions.swift
+++ b/ios/PacketTunnelCore/IPC/PacketTunnelOptions.swift
@@ -7,13 +7,14 @@
//
import Foundation
+import MullvadREST
public struct PacketTunnelOptions {
/// Keys for options dictionary
private enum Keys: String {
/// Option key that holds serialized `SelectedRelay` value encoded using `JSONEncoder`.
/// Used for passing the pre-selected relay in the GUI process to the Packet tunnel process.
- case selectedRelay = "selected-relay"
+ case selectedRelays = "selected-relays"
/// Option key that holds an `NSNumber` value, which is when set to `1` indicates that the tunnel was started by the system.
/// System automatically provides that flag to the tunnel.
@@ -34,14 +35,14 @@ public struct PacketTunnelOptions {
_rawOptions = rawOptions
}
- public func getSelectedRelay() throws -> SelectedRelay? {
- guard let data = _rawOptions[Keys.selectedRelay.rawValue] as? Data else { return nil }
+ public func getSelectedRelays() throws -> SelectedRelays? {
+ guard let data = _rawOptions[Keys.selectedRelays.rawValue] as? Data else { return nil }
- return try Self.decode(SelectedRelay.self, data)
+ return try Self.decode(SelectedRelays.self, data)
}
- public mutating func setSelectedRelay(_ value: SelectedRelay) throws {
- _rawOptions[Keys.selectedRelay.rawValue] = try Self.encode(value) as NSData
+ public mutating func setSelectedRelays(_ value: SelectedRelays) throws {
+ _rawOptions[Keys.selectedRelays.rawValue] = try Self.encode(value) as NSData
}
public func isOnDemand() -> Bool {
diff --git a/ios/PacketTunnelCore/IPC/TunnelProviderMessage.swift b/ios/PacketTunnelCore/IPC/TunnelProviderMessage.swift
index 7062674437..2b1126e8db 100644
--- a/ios/PacketTunnelCore/IPC/TunnelProviderMessage.swift
+++ b/ios/PacketTunnelCore/IPC/TunnelProviderMessage.swift
@@ -11,7 +11,7 @@ import Foundation
/// Enum describing supported app messages handled by packet tunnel provider.
public enum TunnelProviderMessage: Codable, CustomStringConvertible {
/// Request the tunnel to reconnect.
- case reconnectTunnel(NextRelay)
+ case reconnectTunnel(NextRelays)
/// Request the tunnel status.
case getTunnelStatus
diff --git a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift
index 0adb3ec0a6..680b438f83 100644
--- a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift
+++ b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift
@@ -81,21 +81,32 @@ final class AppMessageHandlerTests: XCTestCase {
let relayConstraints = RelayConstraints(
exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")]))
)
- let selectorResult = try XCTUnwrap(try? RelaySelector.WireGuard.evaluate(
- by: relayConstraints,
+
+ let candidates = try RelaySelector.WireGuard.findCandidates(
+ by: relayConstraints.exitLocations,
in: ServerRelaysResponseStubs.sampleRelays,
+ filterConstraint: relayConstraints.filter
+ )
+
+ let match = try RelaySelector.WireGuard.pickCandidate(
+ from: candidates,
+ relays: ServerRelaysResponseStubs.sampleRelays,
+ portConstraint: relayConstraints.port,
numberOfFailedAttempts: 0
- ))
+ )
- let selectedRelay = SelectedRelay(
- endpoint: selectorResult.endpoint,
- hostname: selectorResult.relay.hostname,
- location: selectorResult.location,
- retryAttempts: 0
+ let selectedRelays = SelectedRelays(
+ entry: nil,
+ exit: SelectedRelay(
+ endpoint: match.endpoint,
+ hostname: match.relay.hostname,
+ location: match.location,
+ retryAttempts: 0
+ )
)
_ = try? await appMessageHandler.handleAppMessage(
- TunnelProviderMessage.reconnectTunnel(.preSelected(selectedRelay)).encode()
+ TunnelProviderMessage.reconnectTunnel(.preSelected(selectedRelays)).encode()
)
await fulfillment(of: [reconnectExpectation], timeout: .UnitTest.timeout)
diff --git a/ios/PacketTunnelCoreTests/EventChannelTests.swift b/ios/PacketTunnelCoreTests/EventChannelTests.swift
index 3cdd0f7b0d..59d798a8a2 100644
--- a/ios/PacketTunnelCoreTests/EventChannelTests.swift
+++ b/ios/PacketTunnelCoreTests/EventChannelTests.swift
@@ -90,7 +90,7 @@ extension AsyncSequence {
/// Simplified version of `Event` that can be used in tests and easily compared against.
enum SimplifiedEvent: Equatable {
- case start, stop, reconnect(NextRelay), switchKey, other
+ case start, stop, reconnect(NextRelays), switchKey, other
}
extension PacketTunnelActor.Event {
diff --git a/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift b/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift
index c33f20457d..49705b5dc5 100644
--- a/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift
+++ b/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift
@@ -7,6 +7,8 @@
//
import Foundation
+import MullvadMockData
+import MullvadREST
import PacketTunnelCore
extension PacketTunnelActorTimings {
diff --git a/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActorStub.swift b/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActorStub.swift
index 1c3c1533ae..d526f1f168 100644
--- a/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActorStub.swift
+++ b/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActorStub.swift
@@ -23,7 +23,7 @@ struct PacketTunnelActorStub: PacketTunnelActorProtocol {
}
}
- func reconnect(to nextRelay: PacketTunnelCore.NextRelay, reconnectReason: ActorReconnectReason) {
+ func reconnect(to nextRelays: NextRelays, reconnectReason: ActorReconnectReason) {
reconnectExpectation?.fulfill()
}
diff --git a/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift
index 4419f5b961..b656230c70 100644
--- a/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift
+++ b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift
@@ -208,7 +208,8 @@ final class PacketTunnelActorTests: XCTestCase {
3. The issue goes away on the second attempt to read settings.
4. An actor should transition through `.connecting` towards`.connected` state.
*/
- func testLockedDeviceErrorOnBoot() async throws { // swiftlint:disable:this function_body_length
+ // swiftlint:disable:next function_body_length
+ func testLockedDeviceErrorOnBoot() async throws {
let initialStateExpectation = expectation(description: "Expect initial state")
let errorStateExpectation = expectation(description: "Expect error state")
let connectingStateExpectation = expectation(description: "Expect connecting state")