From 67e9e0ef08723cd991659a106398cbe0cb88493e Mon Sep 17 00:00:00 2001 From: Jon Petersson Date: Tue, 4 Jun 2024 12:18:44 +0200 Subject: [PATCH 1/3] Allow relay selector to select an entry peer --- .../MullvadREST}/RelaySelectorStub.swift | 24 ++- .../NoRelaysSatisfyingConstraintsError.swift | 2 + .../Relay/RelaySelector+Shadowsocks.swift | 1 - .../Relay/RelaySelector+Wireguard.swift | 47 ++-- ios/MullvadREST/Relay/RelaySelector.swift | 1 - .../Relay/RelaySelectorPicker.swift | 200 ++++++++++++++++++ .../Relay}/RelaySelectorProtocol.swift | 19 +- .../Relay/RelaySelectorWrapper.swift | 63 ++++++ ios/MullvadREST/Relay/RelayWithLocation.swift | 10 +- ios/MullvadVPN.xcodeproj/project.pbxproj | 23 +- .../xcshareddata/swiftpm/Package.resolved | 22 -- ios/MullvadVPN/AppDelegate.swift | 26 ++- .../SimulatorTunnelProviderHost.swift | 23 +- .../TunnelManager/TunnelInteractor.swift | 1 + .../TunnelManager/TunnelManager.swift | 21 +- .../TunnelManager/TunnelState.swift | 1 + .../Tunnel/TunnelControlView.swift | 1 + .../Relay/RelaySelectorTests.swift | 109 +++------- .../PacketTunnelActorReducerTests.swift | 26 +-- .../TunnelManager/MockTunnelInteractor.swift | 4 +- .../TunnelManager/TunnelManagerTests.swift | 12 +- .../PacketTunnelProvider.swift | 5 +- .../Actor/ObservedState.swift | 1 + .../Actor/PacketTunnelActor.swift | 7 +- ios/PacketTunnelCore/Actor/StartOptions.swift | 1 + ios/PacketTunnelCore/Actor/State.swift | 1 + .../IPC/PacketTunnelOptions.swift | 1 + .../AppMessageHandlerTests.swift | 20 +- .../Mocks/PacketTunnelActor+Mocks.swift | 2 + .../PacketTunnelActorTests.swift | 3 +- 30 files changed, 452 insertions(+), 225 deletions(-) rename ios/{PacketTunnelCoreTests/Mocks => MullvadMockData/MullvadREST}/RelaySelectorStub.swift (68%) create mode 100644 ios/MullvadREST/Relay/RelaySelectorPicker.swift rename ios/{PacketTunnelCore/Actor/Protocols => MullvadREST/Relay}/RelaySelectorProtocol.swift (63%) create mode 100644 ios/MullvadREST/Relay/RelaySelectorWrapper.swift delete mode 100644 ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/ios/PacketTunnelCoreTests/Mocks/RelaySelectorStub.swift b/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift similarity index 68% rename from ios/PacketTunnelCoreTests/Mocks/RelaySelectorStub.swift rename to ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift index 4922c080c584..2fe06dafe3dd 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 MullvadTypes -import PacketTunnelCore +import MullvadREST 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/NoRelaysSatisfyingConstraintsError.swift b/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift index 9435929db687..b43542893088 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/RelaySelector+Shadowsocks.swift b/ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift index 273b9afe03cc..1f678e6027db 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 4607838ac292..382479ee2067 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 and selects random relay for exit relay. + public static func findCandidates( + by relayConstraint: RelayConstraint, + in relays: REST.ServerRelaysResponse, + filterConstraint: RelayConstraint + ) throws -> [RelayWithLocation] { + 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( + // TODO: Add comment. + public static func pickCandidate( + from relayWithLocations: [RelayWithLocation], relays: REST.ServerRelaysResponse, - relayConstraint: RelayConstraint, portConstraint: RelayConstraint, - filterConstraint: RelayConstraint, 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 44062134ccc2..da4082a1b185 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( _ relayConstraint: RelayConstraint, - portConstraint: RelayConstraint, filterConstraint: RelayConstraint, relays: [RelayWithLocation] ) -> [RelayWithLocation] { diff --git a/ios/MullvadREST/Relay/RelaySelectorPicker.swift b/ios/MullvadREST/Relay/RelaySelectorPicker.swift new file mode 100644 index 000000000000..2ca41cb30e8c --- /dev/null +++ b/ios/MullvadREST/Relay/RelaySelectorPicker.swift @@ -0,0 +1,200 @@ +// +// RelaySelectorPicker.swift +// MullvadREST +// +// Created by Jon Petersson on 2024-06-05. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import MullvadSettings +import MullvadTypes + +protocol RelaySelectorPicker { + var relays: REST.ServerRelaysResponse { get } + var constraints: RelayConstraints { get } + var connectionAttemptCount: UInt { get } + func pick() throws -> SelectedRelays +} + +extension RelaySelectorPicker { + func findBestMatch( + from candidates: [RelayWithLocation] + ) throws -> SelectedRelay { + let match = try RelaySelector.WireGuard.pickCandidate( + from: candidates, + relays: relays, + portConstraint: constraints.port, + numberOfFailedAttempts: connectionAttemptCount + ) + + return SelectedRelay( + endpoint: match.endpoint, + hostname: match.relay.hostname, + location: match.location, + retryAttempts: connectionAttemptCount + ) + } +} + +struct SinglehopPicker: RelaySelectorPicker { + let constraints: RelayConstraints + let relays: REST.ServerRelaysResponse + let connectionAttemptCount: UInt + + func pick() throws -> SelectedRelays { + let candidates = try RelaySelector.WireGuard.findCandidates( + by: constraints.exitLocations, + in: relays, + filterConstraint: constraints.filter + ) + + let match = try findBestMatch(from: candidates) + + return SelectedRelays(entry: nil, exit: match) + } +} + +struct MultihopPicker: RelaySelectorPicker { + let constraints: RelayConstraints + let relays: REST.ServerRelaysResponse + let connectionAttemptCount: UInt + + func pick() throws -> SelectedRelays { + let entryCandidates = try RelaySelector.WireGuard.findCandidates( + by: constraints.entryLocations, + in: relays, + filterConstraint: constraints.filter + ) + + let exitCandidates = try RelaySelector.WireGuard.findCandidates( + by: constraints.exitLocations, + in: relays, + filterConstraint: constraints.filter + ) + + let decisionChain = OneToOne( + next: OneToMany(next: ManyToMany(next: nil, relaySelectorPicker: self), relaySelectorPicker: self), + relaySelectorPicker: self + ) + + return try decisionChain.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) + } + + func exclude( + relay: SelectedRelay, + from candidates: [RelayWithLocation] + ) throws -> SelectedRelay { + let filteredCandidates = candidates.filter { relayWithLocation in + relayWithLocation.serverLocation != relay.location + } + + return try findBestMatch(from: filteredCandidates) + } +} + +protocol MultihopDescionMaker { + typealias RelayCandidate = RelayWithLocation + init(next: MultihopDescionMaker?, relaySelectorPicker: RelaySelectorPicker) + func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool + func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays +} + +private struct OneToOne: MultihopDescionMaker { + let next: MultihopDescionMaker? + let relaySelectorPicker: RelaySelectorPicker + init(next: (any MultihopDescionMaker)?, relaySelectorPicker: RelaySelectorPicker) { + self.next = next + self.relaySelectorPicker = relaySelectorPicker + } + + func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays { + guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else { + guard let next else { + throw NoRelaysSatisfyingConstraintsError() + } + return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) + } + + guard entryCandidates.first != exitCandidates.first else { + throw NoRelaysSatisfyingConstraintsError() + } + + let entryMatch = try relaySelectorPicker.findBestMatch(from: entryCandidates) + let exitMatch = try relaySelectorPicker.findBestMatch(from: exitCandidates) + return SelectedRelays(entry: entryMatch, exit: exitMatch) + } + + func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { + entryCandidates.count == 1 && exitCandidates.count == 1 + } +} + +private struct OneToMany: MultihopDescionMaker { + let next: MultihopDescionMaker? + let relaySelectorPicker: RelaySelectorPicker + + init(next: (any MultihopDescionMaker)?, relaySelectorPicker: RelaySelectorPicker) { + self.next = next + self.relaySelectorPicker = relaySelectorPicker + } + + func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays { + guard let multihopPicker = relaySelectorPicker as? MultihopPicker else { + fatalError("Could not cast picker to MultihopPicker") + } + + guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else { + guard let next else { + throw NoRelaysSatisfyingConstraintsError() + } + return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) + } + + switch (entryCandidates.count, exitCandidates.count) { + case let (1, count) where count > 1: + let entryMatch = try multihopPicker.findBestMatch(from: entryCandidates) + let exitMatch = try multihopPicker.exclude(relay: entryMatch, from: exitCandidates) + return SelectedRelays(entry: entryMatch, exit: exitMatch) + default: + let exitMatch = try multihopPicker.findBestMatch(from: exitCandidates) + let entryMatch = try multihopPicker.exclude(relay: exitMatch, from: entryCandidates) + return SelectedRelays(entry: entryMatch, exit: exitMatch) + } + } + + func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { + (entryCandidates.count == 1 && exitCandidates.count > 1) || + (entryCandidates.count > 1 && exitCandidates.count == 1) + } +} + +private struct ManyToMany: MultihopDescionMaker { + let next: MultihopDescionMaker? + let relaySelectorPicker: RelaySelectorPicker + + init(next: (any MultihopDescionMaker)?, relaySelectorPicker: RelaySelectorPicker) { + self.next = next + self.relaySelectorPicker = relaySelectorPicker + } + + func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays { + guard let multihopPicker = relaySelectorPicker as? MultihopPicker else { + fatalError("Could not cast picker to MultihopPicker") + } + + guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else { + guard let next else { + throw NoRelaysSatisfyingConstraintsError() + } + return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) + } + + let exitMatch = try multihopPicker.findBestMatch(from: exitCandidates) + let entryMatch = try multihopPicker.exclude(relay: exitMatch, from: entryCandidates) + return SelectedRelays(entry: entryMatch, exit: exitMatch) + } + + func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { + entryCandidates.count > 1 && exitCandidates.count > 1 + } +} diff --git a/ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift b/ios/MullvadREST/Relay/RelaySelectorProtocol.swift similarity index 63% rename from ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift rename to ios/MullvadREST/Relay/RelaySelectorProtocol.swift index a4408392e363..390757c3ddf0 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/MullvadREST/Relay/RelaySelectorWrapper.swift b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift new file mode 100644 index 000000000000..eed453e16363 --- /dev/null +++ b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift @@ -0,0 +1,63 @@ +// +// RelaySelectorWrapper.swift +// PacketTunnel +// +// Created by pronebird on 08/08/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import MullvadSettings +import MullvadTypes + +public final class RelaySelectorWrapper: RelaySelectorProtocol { + let relayCache: RelayCacheProtocol + let multihopUpdater: MultihopUpdater + private var multihopState: MultihopState = .off + private var observer: MultihopObserverBlock! + + deinit { + self.multihopUpdater.removeObserver(observer) + } + + public init( + relayCache: RelayCacheProtocol, + multihopUpdater: MultihopUpdater, + multihopState: MultihopState + ) { + self.relayCache = relayCache + self.multihopState = multihopState + 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 + }) + + multihopUpdater.addObserver(observer) + } +} diff --git a/ios/MullvadREST/Relay/RelayWithLocation.swift b/ios/MullvadREST/Relay/RelayWithLocation.swift index c80cc34a3a7d..e6cdcac8afe1 100644 --- a/ios/MullvadREST/Relay/RelayWithLocation.swift +++ b/ios/MullvadREST/Relay/RelayWithLocation.swift @@ -9,9 +9,9 @@ import Foundation import MullvadTypes -struct RelayWithLocation { +public struct RelayWithLocation { let relay: T - let serverLocation: Location + public let serverLocation: Location func matches(location: RelayLocation) -> Bool { return switch location { @@ -29,3 +29,9 @@ struct RelayWithLocation { } } } + +extension RelayWithLocation: Equatable { + public static func == (lhs: RelayWithLocation, rhs: RelayWithLocation) -> Bool { + lhs.serverLocation == rhs.serverLocation + } +} diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index d6b4e6a4a11a..1e25f622903d 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 */; }; @@ -490,15 +487,19 @@ 7A3353932AAA089000F0A71C /* SimulatorTunnelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */; }; 7A3353972AAA0F8600F0A71C /* OperationBlockObserverSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */; }; 7A3EFAAB2BDFDAE800318736 /* RelaySelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3EFAAA2BDFDAE800318736 /* RelaySelection.swift */; }; + 7A3AD5012C1068A800E9AD90 /* RelaySelectorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3AD5002C1068A800E9AD90 /* RelaySelectorPicker.swift */; }; 7A3FD1B52AD4465A0042BEA6 /* AppMessageHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */; }; 7A3FD1B72AD54ABD0042BEA6 /* AnyTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB982A98F4ED00F578F2 /* AnyTransport.swift */; }; 7A3FD1B82AD54AE60042BEA6 /* TimeServerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB9A2A98F58600F578F2 /* TimeServerProxy.swift */; }; 7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */; }; 7A45CFC62C05FF6A00D80B21 /* ScreenshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A45CFC22C05FF2F00D80B21 /* ScreenshotTests.swift */; }; 7A45CFC72C071DD400D80B21 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D0C79D23F1CEBA00FE9BA7 /* SnapshotHelper.swift */; }; + 7A4D849D2C0F289400687980 /* RelaySelectorWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */; }; + 7A4D849E2C0F289800687980 /* RelaySelectorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */; }; 7A516C2E2B6D357500BBD33D /* URL+Scoping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A516C2D2B6D357500BBD33D /* URL+Scoping.swift */; }; 7A516C3A2B7111A700BBD33D /* IPOverrideWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A516C392B7111A700BBD33D /* IPOverrideWrapper.swift */; }; 7A516C3C2B712F0B00BBD33D /* IPOverrideWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A516C3B2B712F0B00BBD33D /* IPOverrideWrapperTests.swift */; }; + 7A52F96A2C1735AE00B133B9 /* RelaySelectorStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */; }; 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 */; }; @@ -1893,6 +1894,7 @@ 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelInfo.swift; sourceTree = ""; }; 7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationBlockObserverSupport.swift; sourceTree = ""; }; 7A3EFAAA2BDFDAE800318736 /* RelaySelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelection.swift; sourceTree = ""; }; + 7A3AD5002C1068A800E9AD90 /* RelaySelectorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorPicker.swift; sourceTree = ""; }; 7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandlerTests.swift; sourceTree = ""; }; 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = ""; }; 7A45CFC22C05FF2F00D80B21 /* ScreenshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotTests.swift; sourceTree = ""; }; @@ -2502,8 +2504,8 @@ 440E9EF42BDA943B00B1FD11 /* ApiHandlers */ = { isa = PBXGroup; children = ( - F0ACE3342BE51745006D5333 /* ServerRelaysResponse+Stubs.swift */, A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */, + F0ACE3342BE51745006D5333 /* ServerRelaysResponse+Stubs.swift */, ); path = ApiHandlers; sourceTree = ""; @@ -3730,7 +3732,6 @@ isa = PBXGroup; children = ( 580D6B8B2AB3369300B2D6E0 /* BlockedStateErrorMapperProtocol.swift */, - 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */, 586E7A2C2A987689006DAB1B /* SettingsReaderProtocol.swift */, 5819ABC22A8CF02C007B59A6 /* TunnelAdapterProtocol.swift */, ); @@ -3746,7 +3747,6 @@ 58EC067B2A8D2A0B00BEB973 /* NetworkCounters.swift */, 58FE25EB2AA77638003D1918 /* TunnelMonitorStub.swift */, 58FE25ED2AA7764E003D1918 /* TunnelAdapterDummy.swift */, - 58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */, 58FE25F12AA77674003D1918 /* SettingsReaderStub.swift */, 58F7753C2AB8473200425B47 /* BlockedStateErrorMapperStub.swift */, 5838321A2AC1B18400EA2071 /* PacketTunnelActor+Mocks.swift */, @@ -4182,6 +4182,7 @@ A900E9BD2ACC654100C95F67 /* APIProxy+Stubs.swift */, A900E9BB2ACC609200C95F67 /* DevicesProxy+Stubs.swift */, F0ACE32E2BE4EA8B006D5333 /* MockProxyFactory.swift */, + 58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */, A900E9B92ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift */, ); path = MullvadREST; @@ -4209,9 +4210,12 @@ F0DDE4282B220A15006B57A7 /* RelaySelector.swift */, F0B894F42BF7528700817A42 /* RelaySelector+Shadowsocks.swift */, F0B894F22BF7526700817A42 /* RelaySelector+Wireguard.swift */, + 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */, F0F316182BF3572B0078DBCF /* RelaySelectorResult.swift */, + 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */, F0B894F02BF751E300817A42 /* RelayWithDistance.swift */, F0B894EE2BF751C500817A42 /* RelayWithLocation.swift */, + 7A3AD5002C1068A800E9AD90 /* RelaySelectorPicker.swift */, ); path = Relay; sourceTree = ""; @@ -5317,9 +5321,12 @@ 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 /* RelaySelectorPicker.swift in Sources */, + 7A4D849D2C0F289400687980 /* RelaySelectorWrapper.swift in Sources */, A90763C52B2858B40045ADF0 /* AnyIPEndpoint+Socks5.swift in Sources */, F06045EC2B2322A500B2D37A /* Jittered.swift in Sources */, F0DDE4152B220458006B57A7 /* ShadowsocksConfigurationCache.swift in Sources */, @@ -5603,7 +5610,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 +5676,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 +6080,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 +6290,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.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 929180afa433..000000000000 --- a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,22 +0,0 @@ -{ - "pins" : [ - { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log.git", - "state" : { - "revision" : "173f567a2dfec11d74588eea82cecea555bdc0bc", - "version" : "1.4.0" - } - }, - { - "identity" : "wireguard-apple", - "kind" : "remoteSourceControl", - "location" : "https://github.com/mullvad/wireguard-apple.git", - "state" : { - "revision" : "15242e1698fc45261285d7417ed2cd5130d7332e" - } - } - ], - "version" : 2 -} diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index fc6c746cd6fc..8ff9c3d6b3f5 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -85,14 +85,19 @@ 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, + multihopState: multihopState + ) + tunnelManager = createTunnelManager(application: application, relaySelector: relaySelector) + settingsObserver = TunnelBlockObserver(didLoadConfiguration: { tunnelManager in multihopListener.onNewMultihop?(tunnelManager.settings.tunnelMultihopState) constraintsUpdater.onNewConstraints?(tunnelManager.settings.relayConstraints) @@ -139,7 +144,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD transportStrategy: transportStrategy ) setUpTransportMonitor(transportProvider: transportProvider) - setUpSimulatorHost(transportProvider: transportProvider) + setUpSimulatorHost(transportProvider: transportProvider, relaySelector: relaySelector) registerBackgroundTasks() setupPaymentHandler() @@ -151,7 +156,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 +167,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD accountsProxy: accountsProxy, devicesProxy: devicesProxy, apiProxy: apiProxy, - accessTokenManager: proxyFactory.configuration.accessTokenManager + accessTokenManager: proxyFactory.configuration.accessTokenManager, + relaySelector: relaySelector ) } @@ -192,11 +201,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 e53c5a50b214..bc3ce91307aa 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift @@ -20,13 +20,13 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { private var observedState: ObservedState = .disconnected private var selectedRelay: SelectedRelay? 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 @@ -157,19 +157,12 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { } private func pickRelay() throws -> SelectedRelay { - let cachedRelays = try relayCacheTracker.getCachedRelays() 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 + ).exit // TODO: Multihop } private func setInternalStateConnected(with selectedRelay: SelectedRelay?) { diff --git a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift index 3ec3a9791bf8..ef76cd8b8509 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 diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 13a2d661ddcd..4e4d44cd4c05 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() 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, @@ -780,20 +783,12 @@ final class TunnelManager: StorePaymentObserver { } fileprivate func selectRelay() throws -> SelectedRelay { - let cachedRelays = try relayCacheTracker.getCachedRelays() 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 + ).exit // TODO: Multihop } fileprivate func prepareForVPNConfigurationDeletion() { diff --git a/ios/MullvadVPN/TunnelManager/TunnelState.swift b/ios/MullvadVPN/TunnelManager/TunnelState.swift index 76148bdbb889..43ae78e17d49 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 diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift index 584a3c7ff3e1..6ef103f93cd2 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 diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift index 9163613bbe5e..50df5635a0a5 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 - ) - - XCTAssertNil(result) + XCTAssertThrowsError(try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 0)) } +} - // MARK: - Multi-Hop tests +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 + ) + + return try RelaySelector.WireGuard.pickCandidate( + from: candidates, + relays: relays, + portConstraint: constraints.port, + numberOfFailedAttempts: failedAttemptCount + ) + } } diff --git a/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift b/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift index a57d78bd396e..b538b910d8d2 100644 --- a/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift +++ b/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift @@ -7,29 +7,19 @@ // import MullvadTypes +import MullvadMockData @testable import PacketTunnelCore +@testable import PacketTunnelCoreTests 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 selectedRelay = try! RelaySelectorStub + .nonFallible() + .selectRelays(with: RelayConstraints(), connectionAttemptCount: 0) + .exit // TODO: Multihop + func makeConnectionData(keyPolicy: State.KeyPolicy = .useCurrent) -> State.ConnectionData { State.ConnectionData( selectedRelay: selectedRelay, diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift index 49784143e8cc..622c5269ad2e 100644 --- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift +++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift @@ -7,8 +7,8 @@ // import Foundation +import MullvadREST import MullvadSettings -import PacketTunnelCore // this is still very minimal, and will be fleshed out as needed. class MockTunnelInteractor: TunnelInteractor { @@ -73,7 +73,7 @@ class MockTunnelInteractor: TunnelInteractor { struct NotImplementedError: Error {} - func selectRelay() throws -> PacketTunnelCore.SelectedRelay { + func selectRelay() throws -> SelectedRelay { throw NotImplementedError() } } diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift index e9843b5dab4f..3b9dff23d06d 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 ab464a22744e..ebefeee7868a 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift @@ -92,10 +92,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { protocolObfuscator: ProtocolObfuscator() ) - 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) diff --git a/ios/PacketTunnelCore/Actor/ObservedState.swift b/ios/PacketTunnelCore/Actor/ObservedState.swift index bdb85a8e51b4..6975191e0497 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 diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift index f497df07ae3e..f3bc9cdc5a35 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 @@ -464,10 +465,10 @@ extension PacketTunnelActor { } case .random: - return try relaySelector.selectRelay( + return try relaySelector.selectRelays( with: relayConstraints, - connectionAttemptFailureCount: connectionAttemptCount - ) + connectionAttemptCount: connectionAttemptCount + ).exit // TODO: Multihop case let .preSelected(selectedRelay): return selectedRelay diff --git a/ios/PacketTunnelCore/Actor/StartOptions.swift b/ios/PacketTunnelCore/Actor/StartOptions.swift index 9dd3ffeb6829..0e484ef58fdd 100644 --- a/ios/PacketTunnelCore/Actor/StartOptions.swift +++ b/ios/PacketTunnelCore/Actor/StartOptions.swift @@ -7,6 +7,7 @@ // import Foundation +import MullvadREST /// Packet tunnel start options parsed from dictionary passed to packet tunnel with a call to `startTunnel()`. public struct StartOptions { diff --git a/ios/PacketTunnelCore/Actor/State.swift b/ios/PacketTunnelCore/Actor/State.swift index 1afc4ca76804..0aae1ac602d1 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 diff --git a/ios/PacketTunnelCore/IPC/PacketTunnelOptions.swift b/ios/PacketTunnelCore/IPC/PacketTunnelOptions.swift index 742fb1f12fdd..ad632baa16f3 100644 --- a/ios/PacketTunnelCore/IPC/PacketTunnelOptions.swift +++ b/ios/PacketTunnelCore/IPC/PacketTunnelOptions.swift @@ -7,6 +7,7 @@ // import Foundation +import MullvadREST public struct PacketTunnelOptions { /// Keys for options dictionary diff --git a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift index 0adb3ec0a688..dc85adcaa71c 100644 --- a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift +++ b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift @@ -81,16 +81,24 @@ 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, + endpoint: match.endpoint, + hostname: match.relay.hostname, + location: match.location, retryAttempts: 0 ) diff --git a/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift b/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift index c33f20457d28..49705b5dc551 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/PacketTunnelActorTests.swift b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift index 4419f5b96194..b656230c70ab 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") From 5a0554fc315c31f2aa7955bf0ef9fdc9b1acff5e Mon Sep 17 00:00:00 2001 From: Jon Petersson Date: Mon, 10 Jun 2024 16:14:39 +0200 Subject: [PATCH 2/3] Add general support for multiple selected relays --- .../MullvadREST/RelaySelectorStub.swift | 2 +- .../Relay/RelaySelector+Wireguard.swift | 4 +- .../SimulatorTunnelProviderHost.swift | 32 +++--- .../MapConnectionStatusOperation.swift | 8 +- .../TunnelManager/StartTunnelOperation.swift | 8 +- .../TunnelManager/Tunnel+Messaging.swift | 6 +- .../TunnelManager/TunnelInteractor.swift | 2 +- .../TunnelManager/TunnelManager.swift | 8 +- .../TunnelManager/TunnelState+UI.swift | 12 +- .../TunnelManager/TunnelState.swift | 40 +++---- .../Tunnel/TunnelControlView.swift | 14 +-- .../Tunnel/TunnelControlViewModel.swift | 4 +- .../Tunnel/TunnelViewController.swift | 12 +- .../PacketTunnelActorReducerTests.swift | 13 +-- .../TunnelManager/MockTunnelInteractor.swift | 14 +-- .../PacketTunnelProvider.swift | 4 +- .../Actor/ObservedState.swift | 8 +- .../Actor/PacketTunnelActor+PostQuantum.swift | 14 +-- .../Actor/PacketTunnelActor+Public.swift | 6 +- .../Actor/PacketTunnelActor.swift | 106 +++++++++--------- .../Actor/PacketTunnelActorCommand.swift | 10 +- .../Actor/PacketTunnelActorProtocol.swift | 2 +- .../Actor/PacketTunnelActorReducer.swift | 14 +-- ios/PacketTunnelCore/Actor/StartOptions.swift | 12 +- .../Actor/State+Extensions.swift | 2 +- ios/PacketTunnelCore/Actor/State.swift | 14 +-- .../IPC/PacketTunnelOptions.swift | 12 +- .../IPC/TunnelProviderMessage.swift | 2 +- .../AppMessageHandlerTests.swift | 15 ++- .../EventChannelTests.swift | 2 +- .../Mocks/PacketTunnelActorStub.swift | 2 +- 31 files changed, 203 insertions(+), 201 deletions(-) diff --git a/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift b/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift index 2fe06dafe3dd..f2f8952df4f0 100644 --- a/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift +++ b/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift @@ -6,8 +6,8 @@ // Copyright © 2023 Mullvad VPN AB. All rights reserved. // -import MullvadTypes import MullvadREST +import MullvadTypes import WireGuardKitTypes /// Relay selector stub that accepts a block that can be used to provide custom implementation. diff --git a/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift b/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift index 382479ee2067..1e611569417c 100644 --- a/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift +++ b/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift @@ -10,7 +10,7 @@ import MullvadTypes extension RelaySelector { public enum WireGuard { - /// Filters relay list using given constraints and selects random relay for exit relay. + /// Filters relay list using given constraints. public static func findCandidates( by relayConstraint: RelayConstraint, in relays: REST.ServerRelaysResponse, @@ -25,7 +25,7 @@ extension RelaySelector { ) } - // TODO: Add comment. + /// Picks a random relay from a list. public static func pickCandidate( from relayWithLocations: [RelayWithLocation], relays: REST.ServerRelaysResponse, diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift index bc3ce91307aa..08b49d4b0585 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift @@ -18,7 +18,7 @@ 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 relaySelector: RelaySelectorProtocol @@ -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,28 +156,28 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { } } - private func pickRelay() throws -> SelectedRelay { + private func pickRelays() throws -> SelectedRelays { let tunnelSettings = try SettingsManager.readSettings() return try relaySelector.selectRelays( with: tunnelSettings.relayConstraints, connectionAttemptCount: 0 - ).exit // TODO: Multihop + ) } - 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 4957af7c52ec..b605b85b476d 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 cd9e8b7a88c5..bb125db5abd1 100644 --- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift @@ -72,12 +72,12 @@ class StartTunnelOperation: ResultOperation { } 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 { 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 5299a281cc8d..04a231c5c497 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 ) -> 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 ef76cd8b8509..3b6735bd396a 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift @@ -39,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 4e4d44cd4c05..d6d4be4d0616 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -782,13 +782,13 @@ final class TunnelManager: StorePaymentObserver { updateTunnelStatus(tunnel?.status ?? .disconnected) } - fileprivate func selectRelay() throws -> SelectedRelay { + fileprivate func selectRelays() throws -> SelectedRelays { let retryAttempts = tunnelStatus.observedState.connectionState?.connectionAttemptCount ?? 0 return try relaySelector.selectRelays( with: settings.relayConstraints, connectionAttemptCount: retryAttempts - ).exit // TODO: Multihop + ) } fileprivate func prepareForVPNConfigurationDeletion() { @@ -1260,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 eefb1db415d4..3422c8602d88 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 43ae78e17d49..2c5a6109b678 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelState.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelState.swift @@ -50,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) @@ -66,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) @@ -81,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 } } @@ -114,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 6ef103f93cd2..c627f85fa360 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift @@ -145,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) } } @@ -224,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 833583efd389..c0df319d2505 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 36f853504768..b5d0dfab64bf 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/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift b/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift index b538b910d8d2..6184756f4631 100644 --- a/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift +++ b/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Mullvad VPN AB. All rights reserved. // -import MullvadTypes import MullvadMockData +import MullvadTypes @testable import PacketTunnelCore @testable import PacketTunnelCoreTests import WireGuardKitTypes @@ -15,19 +15,18 @@ import XCTest final class PacketTunnelActorReducerTests: XCTestCase { // swiftlint:disable:next force_try - let selectedRelay = try! RelaySelectorStub + let selectedRelays = try! RelaySelectorStub .nonFallible() .selectRelays(with: RelayConstraints(), connectionAttemptCount: 0) - .exit // TODO: Multihop 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 @@ -55,13 +54,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 622c5269ad2e..3da521592243 100644 --- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift +++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift @@ -14,9 +14,9 @@ import MullvadSettings 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 -> SelectedRelay { + func selectRelays() throws -> SelectedRelays { throw NotImplementedError() } } diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift index ebefeee7868a..cf81a3b1b41a 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift @@ -207,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 6975191e0497..43d99fdfea6b 100644 --- a/ios/PacketTunnelCore/Actor/ObservedState.swift +++ b/ios/PacketTunnelCore/Actor/ObservedState.swift @@ -27,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 @@ -41,7 +41,7 @@ public struct ObservedConnectionState: Equatable, Codable { } public init( - selectedRelay: SelectedRelay, + selectedRelays: SelectedRelays, relayConstraints: RelayConstraints, networkReachability: NetworkReachability, connectionAttemptCount: UInt, @@ -50,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 @@ -95,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 d8a5a8772c50..30348546f173 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 5ba42a0bd203..160fd9bbe6a3 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 f3bc9cdc5a35..715cfd840dc3 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift @@ -110,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) @@ -168,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.") @@ -206,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. @@ -227,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 @@ -246,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) } } @@ -269,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) @@ -316,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? { @@ -338,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 ) } @@ -355,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): @@ -368,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 @@ -383,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 ) } @@ -409,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, @@ -439,28 +439,28 @@ 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 } @@ -468,10 +468,10 @@ extension PacketTunnelActor { return try relaySelector.selectRelays( with: relayConstraints, connectionAttemptCount: connectionAttemptCount - ).exit // TODO: Multihop + ) - 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 21e2aca702f3..fd731a32e150 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 df34f768cb80..02729d449c01 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 200da62fffab..6d4056b68338 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 0e484ef58fdd..4c8ad7587891 100644 --- a/ios/PacketTunnelCore/Actor/StartOptions.swift +++ b/ios/PacketTunnelCore/Actor/StartOptions.swift @@ -14,20 +14,20 @@ 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 be1f05d52dc6..69d6579a9db9 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 0aae1ac602d1..0ae4c79e3ea6 100644 --- a/ios/PacketTunnelCore/Actor/State.swift +++ b/ios/PacketTunnelCore/Actor/State.swift @@ -110,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. @@ -229,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 ad632baa16f3..79bc030706f8 100644 --- a/ios/PacketTunnelCore/IPC/PacketTunnelOptions.swift +++ b/ios/PacketTunnelCore/IPC/PacketTunnelOptions.swift @@ -14,7 +14,7 @@ public struct PacketTunnelOptions { 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. @@ -35,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 70626744379e..2b1126e8db54 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 dc85adcaa71c..680b438f8395 100644 --- a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift +++ b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift @@ -95,15 +95,18 @@ final class AppMessageHandlerTests: XCTestCase { numberOfFailedAttempts: 0 ) - let selectedRelay = SelectedRelay( - endpoint: match.endpoint, - hostname: match.relay.hostname, - location: match.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 3cdd0f7b0d15..59d798a8a262 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/PacketTunnelActorStub.swift b/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActorStub.swift index 1c3c1533ae6e..d526f1f168b9 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() } From e6f0fccede55db61c86d9470e85ebe7693541368 Mon Sep 17 00:00:00 2001 From: Jon Petersson Date: Wed, 12 Jun 2024 09:48:19 +0200 Subject: [PATCH 3/3] Add RelaySelectorWrapper tests --- .../Relay/MultihopDecisionFlow.swift | 116 ++++++++++ ios/MullvadREST/Relay/RelayPicking.swift | 107 ++++++++++ .../Relay/RelaySelectorPicker.swift | 200 ------------------ .../Relay/RelaySelectorWrapper.swift | 4 +- ios/MullvadREST/Relay/RelayWithLocation.swift | 2 +- .../Shadowsocks/ShadowsocksLoader.swift | 4 +- ios/MullvadVPN.xcodeproj/project.pbxproj | 33 ++- .../xcshareddata/swiftpm/Package.resolved | 22 ++ ios/MullvadVPN/AppDelegate.swift | 6 +- .../Relay/MultihopDecisionFlowTests.swift | 156 ++++++++++++++ .../MullvadREST/Relay/RelayPickingTests.swift | 68 ++++++ .../Relay/RelaySelectorWrapperTests.swift | 55 +++++ .../PacketTunnelActorReducerTests.swift | 1 - .../RelaySelectorWrapper.swift | 62 ------ 14 files changed, 553 insertions(+), 283 deletions(-) create mode 100644 ios/MullvadREST/Relay/MultihopDecisionFlow.swift create mode 100644 ios/MullvadREST/Relay/RelayPicking.swift delete mode 100644 ios/MullvadREST/Relay/RelaySelectorPicker.swift create mode 100644 ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift create mode 100644 ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift create mode 100644 ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift delete mode 100644 ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift diff --git a/ios/MullvadREST/Relay/MultihopDecisionFlow.swift b/ios/MullvadREST/Relay/MultihopDecisionFlow.swift new file mode 100644 index 000000000000..fa8431ed9901 --- /dev/null +++ b/ios/MullvadREST/Relay/MultihopDecisionFlow.swift @@ -0,0 +1,116 @@ +// +// MultihopDecisionFlow.swift +// MullvadREST +// +// Created by Jon Petersson on 2024-06-14. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +protocol MultihopDecisionFlow { + typealias RelayCandidate = RelayWithLocation + init(next: MultihopDecisionFlow?, relayPicker: RelayPicking) + func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool + func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays +} + +struct OneToOne: MultihopDecisionFlow { + let next: MultihopDecisionFlow? + let relayPicker: RelayPicking + init(next: (any MultihopDecisionFlow)?, relayPicker: RelayPicking) { + self.next = next + self.relayPicker = relayPicker + } + + func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays { + guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else { + guard let next else { + throw NoRelaysSatisfyingConstraintsError() + } + return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) + } + + guard entryCandidates.first != exitCandidates.first else { + throw NoRelaysSatisfyingConstraintsError() + } + + let entryMatch = try relayPicker.findBestMatch(from: entryCandidates) + let exitMatch = try relayPicker.findBestMatch(from: exitCandidates) + return SelectedRelays(entry: entryMatch, exit: exitMatch) + } + + func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { + entryCandidates.count == 1 && exitCandidates.count == 1 + } +} + +struct OneToMany: MultihopDecisionFlow { + let next: MultihopDecisionFlow? + let relayPicker: RelayPicking + + init(next: (any MultihopDecisionFlow)?, relayPicker: RelayPicking) { + self.next = next + self.relayPicker = relayPicker + } + + func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays { + guard let multihopPicker = relayPicker as? MultihopPicker else { + fatalError("Could not cast picker to MultihopPicker") + } + + guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else { + guard let next else { + throw NoRelaysSatisfyingConstraintsError() + } + return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) + } + + switch (entryCandidates.count, exitCandidates.count) { + case let (1, count) where count > 1: + let entryMatch = try multihopPicker.findBestMatch(from: entryCandidates) + let exitMatch = try multihopPicker.exclude(relay: entryMatch, from: exitCandidates) + return SelectedRelays(entry: entryMatch, exit: exitMatch) + default: + let exitMatch = try multihopPicker.findBestMatch(from: exitCandidates) + let entryMatch = try multihopPicker.exclude(relay: exitMatch, from: entryCandidates) + return SelectedRelays(entry: entryMatch, exit: exitMatch) + } + } + + func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { + (entryCandidates.count == 1 && exitCandidates.count > 1) || + (entryCandidates.count > 1 && exitCandidates.count == 1) + } +} + +struct ManyToMany: MultihopDecisionFlow { + let next: MultihopDecisionFlow? + let relayPicker: RelayPicking + + init(next: (any MultihopDecisionFlow)?, relayPicker: RelayPicking) { + self.next = next + self.relayPicker = relayPicker + } + + func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays { + guard let multihopPicker = relayPicker as? MultihopPicker else { + fatalError("Could not cast picker to MultihopPicker") + } + + guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else { + guard let next else { + throw NoRelaysSatisfyingConstraintsError() + } + return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) + } + + let exitMatch = try multihopPicker.findBestMatch(from: exitCandidates) + let entryMatch = try multihopPicker.exclude(relay: exitMatch, from: entryCandidates) + return SelectedRelays(entry: entryMatch, exit: exitMatch) + } + + func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { + entryCandidates.count > 1 && exitCandidates.count > 1 + } +} diff --git a/ios/MullvadREST/Relay/RelayPicking.swift b/ios/MullvadREST/Relay/RelayPicking.swift new file mode 100644 index 000000000000..eec1003a1ca5 --- /dev/null +++ b/ios/MullvadREST/Relay/RelayPicking.swift @@ -0,0 +1,107 @@ +// +// RelaySelectorPicker.swift +// MullvadREST +// +// Created by Jon Petersson on 2024-06-05. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import MullvadSettings +import MullvadTypes + +protocol RelayPicking { + var relays: REST.ServerRelaysResponse { get } + var constraints: RelayConstraints { get } + var connectionAttemptCount: UInt { get } + func pick() throws -> SelectedRelays +} + +extension RelayPicking { + func findBestMatch( + from candidates: [RelayWithLocation] + ) throws -> SelectedRelay { + let match = try RelaySelector.WireGuard.pickCandidate( + from: candidates, + relays: relays, + portConstraint: constraints.port, + numberOfFailedAttempts: connectionAttemptCount + ) + + return SelectedRelay( + endpoint: match.endpoint, + hostname: match.relay.hostname, + location: match.location, + retryAttempts: connectionAttemptCount + ) + } +} + +struct SinglehopPicker: RelayPicking { + let constraints: RelayConstraints + let relays: REST.ServerRelaysResponse + let connectionAttemptCount: UInt + + func pick() throws -> SelectedRelays { + let candidates = try RelaySelector.WireGuard.findCandidates( + by: constraints.exitLocations, + in: relays, + filterConstraint: constraints.filter + ) + + let match = try findBestMatch(from: candidates) + + return SelectedRelays(entry: nil, exit: match) + } +} + +struct MultihopPicker: RelayPicking { + let constraints: RelayConstraints + let relays: REST.ServerRelaysResponse + let connectionAttemptCount: UInt + + func pick() throws -> SelectedRelays { + let entryCandidates = try RelaySelector.WireGuard.findCandidates( + by: constraints.entryLocations, + in: relays, + filterConstraint: constraints.filter + ) + + let exitCandidates = try RelaySelector.WireGuard.findCandidates( + by: constraints.exitLocations, + in: relays, + filterConstraint: constraints.filter + ) + + /* + Relay selection is prioritised in the following order: + 1. Both entry and exit constraints match only a single relay. Both relays are selected. + 2. Either entry or exit constraint matches only a single relay and the other multiple relays. The single relays + is selected and excluded from the list of multiple relays. + 3. Both entry and exit constraints match multiple relays. Exit relay is picked first and then excluded from + the list of entry relays. + */ + let decisionFlow = OneToOne( + next: OneToMany( + next: ManyToMany( + next: nil, + relayPicker: self + ), + relayPicker: self + ), + relayPicker: self + ) + + return try decisionFlow.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) + } + + func exclude( + relay: SelectedRelay, + from candidates: [RelayWithLocation] + ) throws -> SelectedRelay { + let filteredCandidates = candidates.filter { relayWithLocation in + relayWithLocation.relay.hostname != relay.hostname + } + + return try findBestMatch(from: filteredCandidates) + } +} diff --git a/ios/MullvadREST/Relay/RelaySelectorPicker.swift b/ios/MullvadREST/Relay/RelaySelectorPicker.swift deleted file mode 100644 index 2ca41cb30e8c..000000000000 --- a/ios/MullvadREST/Relay/RelaySelectorPicker.swift +++ /dev/null @@ -1,200 +0,0 @@ -// -// RelaySelectorPicker.swift -// MullvadREST -// -// Created by Jon Petersson on 2024-06-05. -// Copyright © 2024 Mullvad VPN AB. All rights reserved. -// - -import MullvadSettings -import MullvadTypes - -protocol RelaySelectorPicker { - var relays: REST.ServerRelaysResponse { get } - var constraints: RelayConstraints { get } - var connectionAttemptCount: UInt { get } - func pick() throws -> SelectedRelays -} - -extension RelaySelectorPicker { - func findBestMatch( - from candidates: [RelayWithLocation] - ) throws -> SelectedRelay { - let match = try RelaySelector.WireGuard.pickCandidate( - from: candidates, - relays: relays, - portConstraint: constraints.port, - numberOfFailedAttempts: connectionAttemptCount - ) - - return SelectedRelay( - endpoint: match.endpoint, - hostname: match.relay.hostname, - location: match.location, - retryAttempts: connectionAttemptCount - ) - } -} - -struct SinglehopPicker: RelaySelectorPicker { - let constraints: RelayConstraints - let relays: REST.ServerRelaysResponse - let connectionAttemptCount: UInt - - func pick() throws -> SelectedRelays { - let candidates = try RelaySelector.WireGuard.findCandidates( - by: constraints.exitLocations, - in: relays, - filterConstraint: constraints.filter - ) - - let match = try findBestMatch(from: candidates) - - return SelectedRelays(entry: nil, exit: match) - } -} - -struct MultihopPicker: RelaySelectorPicker { - let constraints: RelayConstraints - let relays: REST.ServerRelaysResponse - let connectionAttemptCount: UInt - - func pick() throws -> SelectedRelays { - let entryCandidates = try RelaySelector.WireGuard.findCandidates( - by: constraints.entryLocations, - in: relays, - filterConstraint: constraints.filter - ) - - let exitCandidates = try RelaySelector.WireGuard.findCandidates( - by: constraints.exitLocations, - in: relays, - filterConstraint: constraints.filter - ) - - let decisionChain = OneToOne( - next: OneToMany(next: ManyToMany(next: nil, relaySelectorPicker: self), relaySelectorPicker: self), - relaySelectorPicker: self - ) - - return try decisionChain.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) - } - - func exclude( - relay: SelectedRelay, - from candidates: [RelayWithLocation] - ) throws -> SelectedRelay { - let filteredCandidates = candidates.filter { relayWithLocation in - relayWithLocation.serverLocation != relay.location - } - - return try findBestMatch(from: filteredCandidates) - } -} - -protocol MultihopDescionMaker { - typealias RelayCandidate = RelayWithLocation - init(next: MultihopDescionMaker?, relaySelectorPicker: RelaySelectorPicker) - func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool - func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays -} - -private struct OneToOne: MultihopDescionMaker { - let next: MultihopDescionMaker? - let relaySelectorPicker: RelaySelectorPicker - init(next: (any MultihopDescionMaker)?, relaySelectorPicker: RelaySelectorPicker) { - self.next = next - self.relaySelectorPicker = relaySelectorPicker - } - - func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays { - guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else { - guard let next else { - throw NoRelaysSatisfyingConstraintsError() - } - return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) - } - - guard entryCandidates.first != exitCandidates.first else { - throw NoRelaysSatisfyingConstraintsError() - } - - let entryMatch = try relaySelectorPicker.findBestMatch(from: entryCandidates) - let exitMatch = try relaySelectorPicker.findBestMatch(from: exitCandidates) - return SelectedRelays(entry: entryMatch, exit: exitMatch) - } - - func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { - entryCandidates.count == 1 && exitCandidates.count == 1 - } -} - -private struct OneToMany: MultihopDescionMaker { - let next: MultihopDescionMaker? - let relaySelectorPicker: RelaySelectorPicker - - init(next: (any MultihopDescionMaker)?, relaySelectorPicker: RelaySelectorPicker) { - self.next = next - self.relaySelectorPicker = relaySelectorPicker - } - - func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays { - guard let multihopPicker = relaySelectorPicker as? MultihopPicker else { - fatalError("Could not cast picker to MultihopPicker") - } - - guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else { - guard let next else { - throw NoRelaysSatisfyingConstraintsError() - } - return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) - } - - switch (entryCandidates.count, exitCandidates.count) { - case let (1, count) where count > 1: - let entryMatch = try multihopPicker.findBestMatch(from: entryCandidates) - let exitMatch = try multihopPicker.exclude(relay: entryMatch, from: exitCandidates) - return SelectedRelays(entry: entryMatch, exit: exitMatch) - default: - let exitMatch = try multihopPicker.findBestMatch(from: exitCandidates) - let entryMatch = try multihopPicker.exclude(relay: exitMatch, from: entryCandidates) - return SelectedRelays(entry: entryMatch, exit: exitMatch) - } - } - - func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { - (entryCandidates.count == 1 && exitCandidates.count > 1) || - (entryCandidates.count > 1 && exitCandidates.count == 1) - } -} - -private struct ManyToMany: MultihopDescionMaker { - let next: MultihopDescionMaker? - let relaySelectorPicker: RelaySelectorPicker - - init(next: (any MultihopDescionMaker)?, relaySelectorPicker: RelaySelectorPicker) { - self.next = next - self.relaySelectorPicker = relaySelectorPicker - } - - func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays { - guard let multihopPicker = relaySelectorPicker as? MultihopPicker else { - fatalError("Could not cast picker to MultihopPicker") - } - - guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else { - guard let next else { - throw NoRelaysSatisfyingConstraintsError() - } - return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) - } - - let exitMatch = try multihopPicker.findBestMatch(from: exitCandidates) - let entryMatch = try multihopPicker.exclude(relay: exitMatch, from: entryCandidates) - return SelectedRelays(entry: entryMatch, exit: exitMatch) - } - - func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { - entryCandidates.count > 1 && exitCandidates.count > 1 - } -} diff --git a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift index eed453e16363..3ee447d0ab59 100644 --- a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift +++ b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift @@ -21,11 +21,9 @@ public final class RelaySelectorWrapper: RelaySelectorProtocol { public init( relayCache: RelayCacheProtocol, - multihopUpdater: MultihopUpdater, - multihopState: MultihopState + multihopUpdater: MultihopUpdater ) { self.relayCache = relayCache - self.multihopState = multihopState self.multihopUpdater = multihopUpdater self.addObserver() diff --git a/ios/MullvadREST/Relay/RelayWithLocation.swift b/ios/MullvadREST/Relay/RelayWithLocation.swift index e6cdcac8afe1..0cba62661bc0 100644 --- a/ios/MullvadREST/Relay/RelayWithLocation.swift +++ b/ios/MullvadREST/Relay/RelayWithLocation.swift @@ -32,6 +32,6 @@ public struct RelayWithLocation { extension RelayWithLocation: Equatable { public static func == (lhs: RelayWithLocation, rhs: RelayWithLocation) -> Bool { - lhs.serverLocation == rhs.serverLocation + lhs.relay.hostname == rhs.relay.hostname } } diff --git a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift index f91980566520..2b46571bc820 100644 --- a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift +++ b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift @@ -33,14 +33,12 @@ public class ShadowsocksLoader: ShadowsocksLoaderProtocol { cache: ShadowsocksConfigurationCacheProtocol, relaySelector: ShadowsocksRelaySelectorProtocol, constraintsUpdater: RelayConstraintsUpdater, - multihopUpdater: MultihopUpdater, - multihopState: MultihopState = .off + multihopUpdater: MultihopUpdater ) { self.cache = cache self.relaySelector = relaySelector self.constraintsUpdater = constraintsUpdater self.multihopUpdater = multihopUpdater - self.multihopState = multihopState self.addObservers() } diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 1e25f622903d..b039c4cb0f24 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -486,20 +486,20 @@ 7A3353912AAA014400F0A71C /* SimulatorVPNConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */; }; 7A3353932AAA089000F0A71C /* SimulatorTunnelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */; }; 7A3353972AAA0F8600F0A71C /* OperationBlockObserverSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */; }; + 7A3AD5012C1068A800E9AD90 /* RelayPicking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3AD5002C1068A800E9AD90 /* RelayPicking.swift */; }; 7A3EFAAB2BDFDAE800318736 /* RelaySelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3EFAAA2BDFDAE800318736 /* RelaySelection.swift */; }; - 7A3AD5012C1068A800E9AD90 /* RelaySelectorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3AD5002C1068A800E9AD90 /* RelaySelectorPicker.swift */; }; 7A3FD1B52AD4465A0042BEA6 /* AppMessageHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */; }; 7A3FD1B72AD54ABD0042BEA6 /* AnyTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB982A98F4ED00F578F2 /* AnyTransport.swift */; }; 7A3FD1B82AD54AE60042BEA6 /* TimeServerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB9A2A98F58600F578F2 /* TimeServerProxy.swift */; }; 7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */; }; 7A45CFC62C05FF6A00D80B21 /* ScreenshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A45CFC22C05FF2F00D80B21 /* ScreenshotTests.swift */; }; 7A45CFC72C071DD400D80B21 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D0C79D23F1CEBA00FE9BA7 /* SnapshotHelper.swift */; }; - 7A4D849D2C0F289400687980 /* RelaySelectorWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */; }; 7A4D849E2C0F289800687980 /* RelaySelectorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */; }; 7A516C2E2B6D357500BBD33D /* URL+Scoping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A516C2D2B6D357500BBD33D /* URL+Scoping.swift */; }; 7A516C3A2B7111A700BBD33D /* IPOverrideWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A516C392B7111A700BBD33D /* IPOverrideWrapper.swift */; }; 7A516C3C2B712F0B00BBD33D /* IPOverrideWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A516C3B2B712F0B00BBD33D /* IPOverrideWrapperTests.swift */; }; 7A52F96A2C1735AE00B133B9 /* RelaySelectorStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */; }; + 7A52F96C2C17450C00B133B9 /* RelaySelectorWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A52F96B2C17450C00B133B9 /* RelaySelectorWrapperTests.swift */; }; 7A5869952B32E9C700640D27 /* LinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869942B32E9C700640D27 /* LinkButton.swift */; }; 7A5869972B32EA4500640D27 /* AppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869962B32EA4500640D27 /* AppButton.swift */; }; 7A58699B2B482FE200640D27 /* UITableViewCell+Disable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A58699A2B482FE200640D27 /* UITableViewCell+Disable.swift */; }; @@ -598,6 +598,9 @@ 7AC8A3AE2ABC6FBB00DC4939 /* SettingsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC8A3AD2ABC6FBB00DC4939 /* SettingsHeaderView.swift */; }; 7AC8A3AF2ABC71D600DC4939 /* TermsOfServiceCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9CCCA22A96302700DD6A34 /* TermsOfServiceCoordinator.swift */; }; 7ACD79392C0DAADD00DBEE14 /* AddCustomListLocationsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACD79382C0DAADC00DBEE14 /* AddCustomListLocationsPage.swift */; }; + 7ACE19112C1C349200260BB6 /* MultihopDecisionFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACE19102C1C349200260BB6 /* MultihopDecisionFlow.swift */; }; + 7ACE19132C1C352100260BB6 /* RelayPickingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACE19122C1C352100260BB6 /* RelayPickingTests.swift */; }; + 7ACE19152C1C429A00260BB6 /* MultihopDecisionFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACE19142C1C429A00260BB6 /* MultihopDecisionFlowTests.swift */; }; 7AD0AA1C2AD6A63F00119E10 /* PacketTunnelActorStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD0AA1B2AD6A63F00119E10 /* PacketTunnelActorStub.swift */; }; 7AD0AA1D2AD6A86700119E10 /* PacketTunnelActorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD0AA192AD69B6E00119E10 /* PacketTunnelActorProtocol.swift */; }; 7AD0AA1F2AD6C8B900119E10 /* URLRequestProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD0AA1E2AD6C8B900119E10 /* URLRequestProxyProtocol.swift */; }; @@ -607,6 +610,7 @@ 7AE044BB2A935726003915D8 /* Routing.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A88DCD02A8FABBE00D2FF0E /* Routing.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7AE2414A2C20682B0076CE33 /* FormsheetPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE241482C20682B0076CE33 /* FormsheetPresentationController.swift */; }; 7AE90B682C2D726000375A60 /* NSParagraphStyle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE90B672C2D726000375A60 /* NSParagraphStyle+Extensions.swift */; }; + 7AEBA52A2C2179F20018BEC5 /* RelaySelectorWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEBA5292C2179F20018BEC5 /* RelaySelectorWrapper.swift */; }; 7AEBA52C2C22C65B0018BEC5 /* TimeInterval+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEBA52B2C22C65B0018BEC5 /* TimeInterval+Timeout.swift */; }; 7AED35CC2BD13F60002A67D1 /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; 7AED35CD2BD13FC4002A67D1 /* ApplicationTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C76A072A33850E00100D75 /* ApplicationTarget.swift */; }; @@ -1529,7 +1533,6 @@ 5824030C2A811B0000163DE8 /* State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = State.swift; sourceTree = ""; }; 582403162A821FD700163DE8 /* TunnelDeviceInfoProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelDeviceInfoProtocol.swift; sourceTree = ""; }; 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorProtocol.swift; sourceTree = ""; }; - 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorWrapper.swift; sourceTree = ""; }; 5827B08F2B0CAA0500CCBBA1 /* EditAccessMethodCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessMethodCoordinator.swift; sourceTree = ""; }; 5827B0912B0CAB2800CCBBA1 /* MethodSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSettingsViewController.swift; sourceTree = ""; }; 5827B0952B0DB2C100CCBBA1 /* MethodSettingsItemIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodSettingsItemIdentifier.swift; sourceTree = ""; }; @@ -1893,14 +1896,15 @@ 7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorVPNConnection.swift; sourceTree = ""; }; 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelInfo.swift; sourceTree = ""; }; 7A3353962AAA0F8600F0A71C /* OperationBlockObserverSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationBlockObserverSupport.swift; sourceTree = ""; }; + 7A3AD5002C1068A800E9AD90 /* RelayPicking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPicking.swift; sourceTree = ""; }; 7A3EFAAA2BDFDAE800318736 /* RelaySelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelection.swift; sourceTree = ""; }; - 7A3AD5002C1068A800E9AD90 /* RelaySelectorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorPicker.swift; sourceTree = ""; }; 7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandlerTests.swift; sourceTree = ""; }; 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = ""; }; 7A45CFC22C05FF2F00D80B21 /* ScreenshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotTests.swift; sourceTree = ""; }; 7A516C2D2B6D357500BBD33D /* URL+Scoping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Scoping.swift"; sourceTree = ""; }; 7A516C392B7111A700BBD33D /* IPOverrideWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideWrapper.swift; sourceTree = ""; }; 7A516C3B2B712F0B00BBD33D /* IPOverrideWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideWrapperTests.swift; sourceTree = ""; }; + 7A52F96B2C17450C00B133B9 /* RelaySelectorWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorWrapperTests.swift; sourceTree = ""; }; 7A5869942B32E9C700640D27 /* LinkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkButton.swift; sourceTree = ""; }; 7A5869962B32EA4500640D27 /* AppButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppButton.swift; sourceTree = ""; }; 7A58699A2B482FE200640D27 /* UITableViewCell+Disable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Disable.swift"; sourceTree = ""; }; @@ -1987,6 +1991,9 @@ 7ABFB09D2BA316220074A49E /* RelayConstraintsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConstraintsTests.swift; sourceTree = ""; }; 7AC8A3AD2ABC6FBB00DC4939 /* SettingsHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsHeaderView.swift; sourceTree = ""; }; 7ACD79382C0DAADC00DBEE14 /* AddCustomListLocationsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCustomListLocationsPage.swift; sourceTree = ""; }; + 7ACE19102C1C349200260BB6 /* MultihopDecisionFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopDecisionFlow.swift; sourceTree = ""; }; + 7ACE19122C1C352100260BB6 /* RelayPickingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPickingTests.swift; sourceTree = ""; }; + 7ACE19142C1C429A00260BB6 /* MultihopDecisionFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopDecisionFlowTests.swift; sourceTree = ""; }; 7AD0AA192AD69B6E00119E10 /* PacketTunnelActorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelActorProtocol.swift; sourceTree = ""; }; 7AD0AA1B2AD6A63F00119E10 /* PacketTunnelActorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelActorStub.swift; sourceTree = ""; }; 7AD0AA1E2AD6C8B900119E10 /* URLRequestProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestProxyProtocol.swift; sourceTree = ""; }; @@ -1995,6 +2002,7 @@ 7ADCB2D92B6A730400C88F89 /* IPOverrideRepositoryStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideRepositoryStub.swift; sourceTree = ""; }; 7AE241482C20682B0076CE33 /* FormsheetPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormsheetPresentationController.swift; sourceTree = ""; }; 7AE90B672C2D726000375A60 /* NSParagraphStyle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSParagraphStyle+Extensions.swift"; sourceTree = ""; }; + 7AEBA5292C2179F20018BEC5 /* RelaySelectorWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelaySelectorWrapper.swift; sourceTree = ""; }; 7AEBA52B2C22C65B0018BEC5 /* TimeInterval+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Timeout.swift"; sourceTree = ""; }; 7AEF7F192AD00F52006FE45D /* AppMessageHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandler.swift; sourceTree = ""; }; 7AF10EB12ADE859200C090B9 /* AlertViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = ""; }; @@ -2514,8 +2522,11 @@ isa = PBXGroup; children = ( A9EC20E72A5D3A8C0040D56E /* CoordinatesTests.swift */, + 7ACE19142C1C429A00260BB6 /* MultihopDecisionFlowTests.swift */, A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */, + 7ACE19122C1C352100260BB6 /* RelayPickingTests.swift */, 584B26F3237434D00073B10E /* RelaySelectorTests.swift */, + 7A52F96B2C17450C00B133B9 /* RelaySelectorWrapperTests.swift */, ); path = Relay; sourceTree = ""; @@ -3803,7 +3814,6 @@ 58906DDF2445C7A5002F0673 /* NEProviderStopReason+Debug.swift */, 58225D272A84F23B0083D7F1 /* PacketTunnelPathObserver.swift */, 58F3F3692AA08E3C00D3B0A4 /* PacketTunnelProvider.swift */, - 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */, 5864AF7C2A9F4DC9008BC928 /* SettingsReader.swift */, ); path = PacketTunnelProvider; @@ -4205,17 +4215,18 @@ F0DDE4272B220A15006B57A7 /* Haversine.swift */, 7A516C392B7111A700BBD33D /* IPOverrideWrapper.swift */, F0DDE4292B220A15006B57A7 /* Midpoint.swift */, + 7ACE19102C1C349200260BB6 /* MultihopDecisionFlow.swift */, F0F3161A2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift */, 5820675A26E6576800655B05 /* RelayCache.swift */, + 7A3AD5002C1068A800E9AD90 /* RelayPicking.swift */, F0DDE4282B220A15006B57A7 /* RelaySelector.swift */, F0B894F42BF7528700817A42 /* RelaySelector+Shadowsocks.swift */, F0B894F22BF7526700817A42 /* RelaySelector+Wireguard.swift */, 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */, F0F316182BF3572B0078DBCF /* RelaySelectorResult.swift */, - 582403812A827E1500163DE8 /* RelaySelectorWrapper.swift */, + 7AEBA5292C2179F20018BEC5 /* RelaySelectorWrapper.swift */, F0B894F02BF751E300817A42 /* RelayWithDistance.swift */, F0B894EE2BF751C500817A42 /* RelayWithLocation.swift */, - 7A3AD5002C1068A800E9AD90 /* RelaySelectorPicker.swift */, ); path = Relay; sourceTree = ""; @@ -5325,8 +5336,7 @@ F0164EBE2B4BFF940020268D /* ShadowsocksLoader.swift in Sources */, A9A1DE792AD5708E0073F689 /* TransportStrategy.swift in Sources */, A90763BF2B2857D50045ADF0 /* Socks5Handshake.swift in Sources */, - 7A3AD5012C1068A800E9AD90 /* RelaySelectorPicker.swift in Sources */, - 7A4D849D2C0F289400687980 /* RelaySelectorWrapper.swift in Sources */, + 7A3AD5012C1068A800E9AD90 /* RelayPicking.swift in Sources */, A90763C52B2858B40045ADF0 /* AnyIPEndpoint+Socks5.swift in Sources */, F06045EC2B2322A500B2D37A /* Jittered.swift in Sources */, F0DDE4152B220458006B57A7 /* ShadowsocksConfigurationCache.swift in Sources */, @@ -5348,10 +5358,12 @@ 7A516C3A2B7111A700BBD33D /* IPOverrideWrapper.swift in Sources */, F0DDE4142B220458006B57A7 /* ShadowSocksProxy.swift in Sources */, A90763B62B2857D50045ADF0 /* Socks5ConnectNegotiation.swift in Sources */, + 7ACE19112C1C349200260BB6 /* MultihopDecisionFlow.swift in Sources */, F06045E62B231EB700B2D37A /* URLSessionTransport.swift in Sources */, 06799AE628F98E4800ACD94E /* ServerRelaysResponse.swift in Sources */, F0DDE42B2B220A15006B57A7 /* RelaySelector.swift in Sources */, F0B894F32BF7526700817A42 /* RelaySelector+Wireguard.swift in Sources */, + 7AEBA52A2C2179F20018BEC5 /* RelaySelectorWrapper.swift in Sources */, F0B894EF2BF751C500817A42 /* RelayWithLocation.swift in Sources */, F0DDE42C2B220A15006B57A7 /* Midpoint.swift in Sources */, A90763C72B2858DC0045ADF0 /* CancellableChain.swift in Sources */, @@ -5503,12 +5515,14 @@ A9A5FA152ACB05160083449F /* RedeemVoucherOperation.swift in Sources */, A9A5FA162ACB05160083449F /* RotateKeyOperation.swift in Sources */, F072D3CF2C07122400906F64 /* MultihopUpdaterTests.swift in Sources */, + 7ACE19132C1C352100260BB6 /* RelayPickingTests.swift in Sources */, F09D04B52AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift in Sources */, 58BE4B9D2B18A85B007EA1D3 /* NSAttributedString+Extensions.swift in Sources */, A9A5FA172ACB05160083449F /* SendTunnelProviderMessageOperation.swift in Sources */, 7A83A0C62B29A750008B5CE7 /* APIAccessMethodsTests.swift in Sources */, A9A5FA182ACB05160083449F /* SetAccountOperation.swift in Sources */, A9A5FA192ACB05160083449F /* StartTunnelOperation.swift in Sources */, + 7ACE19152C1C429A00260BB6 /* MultihopDecisionFlowTests.swift in Sources */, A9A5FA1A2ACB05160083449F /* StopTunnelOperation.swift in Sources */, 7A9BE5A52B90760C00E2A7D0 /* CustomListsDataSourceTests.swift in Sources */, A9A5FA1B2ACB05160083449F /* Tunnel.swift in Sources */, @@ -5557,6 +5571,7 @@ A9A5FA332ACB05160083449F /* RelaySelectorTests.swift in Sources */, 58DFF7D32B02570000F864E0 /* MarkdownStylingOptions.swift in Sources */, A9A5FA342ACB05160083449F /* StringTests.swift in Sources */, + 7A52F96C2C17450C00B133B9 /* RelaySelectorWrapperTests.swift in Sources */, A9A5FA352ACB05160083449F /* WgKeyRotationTests.swift in Sources */, 7AB4CCB92B69097E006037F5 /* IPOverrideTests.swift in Sources */, A9A5FA362ACB05160083449F /* TunnelManagerTests.swift in Sources */, diff --git a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000000..929180afa433 --- /dev/null +++ b/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,22 @@ +{ + "pins" : [ + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "173f567a2dfec11d74588eea82cecea555bdc0bc", + "version" : "1.4.0" + } + }, + { + "identity" : "wireguard-apple", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mullvad/wireguard-apple.git", + "state" : { + "revision" : "15242e1698fc45261285d7417ed2cd5130d7332e" + } + } + ], + "version" : 2 +} diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 8ff9c3d6b3f5..f027b4dc59b4 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -93,8 +93,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD let relaySelector = RelaySelectorWrapper( relayCache: ipOverrideWrapper, - multihopUpdater: multihopUpdater, - multihopState: multihopState + multihopUpdater: multihopUpdater ) tunnelManager = createTunnelManager(application: application, relaySelector: relaySelector) @@ -124,8 +123,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD cache: shadowsocksCache, relaySelector: shadowsocksRelaySelector, constraintsUpdater: constraintsUpdater, - multihopUpdater: multihopUpdater, - multihopState: tunnelManager.settings.tunnelMultihopState + multihopUpdater: multihopUpdater ) configuredTransportProvider = ProxyConfigurationTransportProvider( diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift new file mode 100644 index 000000000000..d6d570ee9a73 --- /dev/null +++ b/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift @@ -0,0 +1,156 @@ +// +// MultihopDecisionFlowTests.swift +// MullvadVPNTests +// +// Created by Jon Petersson on 2024-06-14. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +@testable import MullvadREST +@testable import MullvadTypes +import XCTest + +class MultihopDecisionFlowTests: XCTestCase { + let sampleRelays = ServerRelaysResponseStubs.sampleRelays + + func testOneToOneCanHandle() throws { + let oneToOne = OneToOne(next: nil, relayPicker: picker) + + XCTAssertTrue(oneToOne.canHandle( + entryCandidates: [seSto6], + exitCandidates: [seSto2] + )) + + XCTAssertFalse(oneToOne.canHandle( + entryCandidates: [seSto2, seSto6], + exitCandidates: [seSto2] + )) + + XCTAssertFalse(oneToOne.canHandle( + entryCandidates: [seSto2, seSto6], + exitCandidates: [seSto2, seSto6] + )) + } + + func testOneToManyCanHandle() throws { + let oneToMany = OneToMany(next: nil, relayPicker: picker) + + XCTAssertTrue(oneToMany.canHandle( + entryCandidates: [seSto2, seSto6], + exitCandidates: [seSto2] + )) + + XCTAssertFalse(oneToMany.canHandle( + entryCandidates: [seSto6], + exitCandidates: [seSto2] + )) + + XCTAssertFalse(oneToMany.canHandle( + entryCandidates: [seSto2, seSto6], + exitCandidates: [seSto2, seSto6] + )) + } + + func testManyToManyCanHandle() throws { + let manyToMany = ManyToMany(next: nil, relayPicker: picker) + + XCTAssertTrue(manyToMany.canHandle( + entryCandidates: [seSto2, seSto6], + exitCandidates: [seSto2, seSto6] + )) + + XCTAssertFalse(manyToMany.canHandle( + entryCandidates: [seSto6], + exitCandidates: [seSto2] + )) + + XCTAssertFalse(manyToMany.canHandle( + entryCandidates: [seSto2, seSto6], + exitCandidates: [seSto2] + )) + } + + func testOneToOnePick() throws { + let oneToOne = OneToOne(next: nil, relayPicker: picker) + + let entryCandidates = [seSto2] + let exitCandidates = [seSto6] + + let selectedRelays = try oneToOne.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) + + XCTAssertEqual(selectedRelays.entry?.hostname, "se2-wireguard") + XCTAssertEqual(selectedRelays.exit.hostname, "se6-wireguard") + } + + func testOneToManyPick() throws { + let oneToMany = OneToMany(next: nil, relayPicker: picker) + + let entryCandidates = [seSto2, seSto6] + let exitCandidates = [seSto2] + + let selectedRelays = try oneToMany.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) + + XCTAssertEqual(selectedRelays.entry?.hostname, "se6-wireguard") + XCTAssertEqual(selectedRelays.exit.hostname, "se2-wireguard") + } + + func testManyToManyPick() throws { + let manyToMany = ManyToMany(next: nil, relayPicker: picker) + + let entryCandidates = [seSto2, seSto6] + let exitCandidates = [seSto2, seSto6] + + let selectedRelays = try manyToMany.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates) + + if selectedRelays.exit.hostname == "se2-wireguard" { + XCTAssertEqual(selectedRelays.entry?.hostname, "se6-wireguard") + } else { + XCTAssertEqual(selectedRelays.entry?.hostname, "se2-wireguard") + } + } +} + +extension MultihopDecisionFlowTests { + var picker: MultihopPicker { + let constraints = RelayConstraints( + entryLocations: .only(UserSelectedRelays(locations: [.city("se", "sto")])), + exitLocations: .only(UserSelectedRelays(locations: [.city("se", "sto")])) + ) + + return MultihopPicker( + constraints: constraints, + relays: sampleRelays, + connectionAttemptCount: 0 + ) + } + + var seSto2: RelayWithLocation { + let relay = sampleRelays.wireguard.relays.first { $0.hostname == "se2-wireguard" }! + let serverLocation = sampleRelays.locations["se-sto"]! + let location = Location( + country: serverLocation.country, + countryCode: serverLocation.country, + city: serverLocation.city, + cityCode: "se-sto", + latitude: serverLocation.latitude, + longitude: serverLocation.longitude + ) + + return RelayWithLocation(relay: relay, serverLocation: location) + } + + var seSto6: RelayWithLocation { + let relay = sampleRelays.wireguard.relays.first { $0.hostname == "se6-wireguard" }! + let serverLocation = sampleRelays.locations["se-sto"]! + let location = Location( + country: serverLocation.country, + countryCode: serverLocation.country, + city: serverLocation.city, + cityCode: "se-sto", + latitude: serverLocation.latitude, + longitude: serverLocation.longitude + ) + + return RelayWithLocation(relay: relay, serverLocation: location) + } +} diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift new file mode 100644 index 000000000000..3c9acec44561 --- /dev/null +++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift @@ -0,0 +1,68 @@ +// +// RelayPickingTests.swift +// MullvadVPNTests +// +// Created by Jon Petersson on 2024-06-14. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +@testable import MullvadREST +@testable import MullvadTypes +import XCTest + +class RelayPickingTests: XCTestCase { + let sampleRelays = ServerRelaysResponseStubs.sampleRelays + + func testSinglehopPicker() throws { + let constraints = RelayConstraints( + entryLocations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se2-wireguard")])), + exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])) + ) + + let picker = SinglehopPicker( + constraints: constraints, + relays: sampleRelays, + connectionAttemptCount: 0 + ) + + let selectedRelays = try picker.pick() + + XCTAssertNil(selectedRelays.entry) + XCTAssertEqual(selectedRelays.exit.hostname, "se10-wireguard") + } + + func testMultihopPicker() throws { + let constraints = RelayConstraints( + entryLocations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se2-wireguard")])), + exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])) + ) + + let picker = MultihopPicker( + constraints: constraints, + relays: sampleRelays, + connectionAttemptCount: 0 + ) + + let selectedRelays = try picker.pick() + + XCTAssertEqual(selectedRelays.entry?.hostname, "se2-wireguard") + XCTAssertEqual(selectedRelays.exit.hostname, "se10-wireguard") + } + + func testMultihopPickerWithSameEntryAndExit() throws { + let constraints = RelayConstraints( + entryLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])), + exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])) + ) + + let picker = MultihopPicker( + constraints: constraints, + relays: sampleRelays, + connectionAttemptCount: 0 + ) + + XCTAssertThrowsError(try picker.pick()) + } +} diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift new file mode 100644 index 000000000000..a1ecb02fdfc2 --- /dev/null +++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift @@ -0,0 +1,55 @@ +// +// RelaySelectorWrapperTests.swift +// MullvadVPNTests +// +// Created by Jon Petersson on 2024-06-10. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +@testable import MullvadREST +@testable import MullvadSettings +@testable import MullvadTypes +import XCTest + +class RelaySelectorWrapperTests: XCTestCase { + let fileCache = MockFileCache( + initialState: .exists(CachedRelays( + relays: ServerRelaysResponseStubs.sampleRelays, + updatedAt: .distantPast + )) + ) + + var relayCache: RelayCache! + var multihopUpdater: MultihopUpdater! + var multihopStateListener: MultihopStateListener! + + override func setUp() { + relayCache = RelayCache(fileCache: fileCache) + multihopStateListener = MultihopStateListener() + multihopUpdater = MultihopUpdater(listener: multihopStateListener) + } + + func testSelectRelayWithMultihopOff() throws { + let wrapper = RelaySelectorWrapper( + relayCache: relayCache, + multihopUpdater: multihopUpdater + ) + + multihopStateListener.onNewMultihop?(.off) + + let selectedRelays = try wrapper.selectRelays(with: RelayConstraints(), connectionAttemptCount: 0) + XCTAssertNil(selectedRelays.entry) + } + + func testSelectRelayWithMultihopOn() throws { + let wrapper = RelaySelectorWrapper( + relayCache: relayCache, + multihopUpdater: multihopUpdater + ) + + multihopStateListener.onNewMultihop?(.on) + + let selectedRelays = try wrapper.selectRelays(with: RelayConstraints(), connectionAttemptCount: 0) + XCTAssertNotNil(selectedRelays.entry) + } +} diff --git a/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift b/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift index 6184756f4631..637e47c89ebe 100644 --- a/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift +++ b/ios/MullvadVPNTests/MullvadVPN/PacketTunnelCore/PacketTunnelActorReducerTests.swift @@ -9,7 +9,6 @@ import MullvadMockData import MullvadTypes @testable import PacketTunnelCore -@testable import PacketTunnelCoreTests import WireGuardKitTypes import XCTest diff --git a/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift b/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift deleted file mode 100644 index 8db65968a2b8..000000000000 --- a/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// RelaySelectorWrapper.swift -// PacketTunnel -// -// Created by pronebird on 08/08/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import MullvadREST -import MullvadSettings -import MullvadTypes -import PacketTunnelCore - -final class RelaySelectorWrapper: RelaySelectorProtocol { - let relayCache: RelayCacheProtocol - let multihopUpdater: MultihopUpdater - private var multihopState: MultihopState = .off - private var observer: MultihopObserverBlock! - - deinit { - self.multihopUpdater.removeObserver(observer) - } - - public init( - relayCache: RelayCacheProtocol, - multihopUpdater: MultihopUpdater - ) { - self.relayCache = relayCache - self.multihopUpdater = multihopUpdater - self.addObserver() - } - - private func addObserver() { - self.observer = MultihopObserverBlock(didUpdateMultihop: { [weak self] _, multihopState in - self?.multihopState = multihopState - }) - - multihopUpdater.addObserver(observer) - } - - func selectRelay( - with constraints: RelayConstraints, - connectionAttemptFailureCount: UInt - ) throws -> SelectedRelay { - switch multihopState { - case .off, .on: - let selectorResult = try RelaySelector.WireGuard.evaluate( - by: constraints, - in: relayCache.read().relays, - numberOfFailedAttempts: connectionAttemptFailureCount - ) - - return SelectedRelay( - endpoint: selectorResult.endpoint, - hostname: selectorResult.relay.hostname, - location: selectorResult.location, - retryAttempts: connectionAttemptFailureCount - ) - } - } -}