summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@kvadrat.se>2024-08-08 13:25:43 +0200
committerBug Magnet <marco.nikic@mullvad.net>2024-08-21 11:33:59 +0200
commitb5baa95057e3825bb7b182bd20b72376cebafaeb (patch)
tree1f7c354343510c4bc97639fe35158701776810a1
parent1ee3102aefe058267ff32644c45be22fb23fddd2 (diff)
downloadmullvadvpn-b5baa95057e3825bb7b182bd20b72376cebafaeb.tar.xz
mullvadvpn-b5baa95057e3825bb7b182bd20b72376cebafaeb.zip
Allow relay selector to filter DAITA enabled relays
-rw-r--r--ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift2
-rw-r--r--ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift5
-rw-r--r--ios/MullvadREST/Relay/AnyRelay.swift1
-rw-r--r--ios/MullvadREST/Relay/MultihopDecisionFlow.swift8
-rw-r--r--ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift33
-rw-r--r--ios/MullvadREST/Relay/RelayPicking.swift38
-rw-r--r--ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift5
-rw-r--r--ios/MullvadREST/Relay/RelaySelector+Wireguard.swift15
-rw-r--r--ios/MullvadREST/Relay/RelaySelector.swift147
-rw-r--r--ios/MullvadREST/Relay/RelaySelectorWrapper.swift2
-rw-r--r--ios/MullvadVPN/Coordinators/LocationCoordinator.swift2
-rw-r--r--ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift6
-rw-r--r--ios/MullvadVPNTests/MullvadREST/ApiHandlers/ServerRelaysResponse+Stubs.swift31
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift2
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift11
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift106
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift86
-rw-r--r--ios/MullvadVPNTests/MullvadSettings/IPOverrideWrapperTests.swift3
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift15
-rw-r--r--ios/PacketTunnelCore/Actor/State+Extensions.swift4
-rw-r--r--ios/PacketTunnelCore/Actor/State.swift11
-rw-r--r--ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift3
-rw-r--r--ios/PacketTunnelCoreTests/MultiHopPostQuantumKeyExchangingTests.swift6
-rw-r--r--ios/PacketTunnelCoreTests/PostQuantumKeyExchangingPipelineTests.swift6
-rw-r--r--ios/PacketTunnelCoreTests/SingleHopPostQuantumKeyExchangingTests.swift3
25 files changed, 459 insertions, 92 deletions
diff --git a/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift b/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift
index 7dd95ae976..8fc6ba9141 100644
--- a/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift
+++ b/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift
@@ -58,7 +58,7 @@ extension RelaySelectorStub {
/// Returns a relay selector that cannot satisfy constraints .
public static func unsatisfied() -> RelaySelectorStub {
return RelaySelectorStub { _ in
- throw NoRelaysSatisfyingConstraintsError()
+ throw NoRelaysSatisfyingConstraintsError(.relayConstraintNotMatching)
}
}
}
diff --git a/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift b/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift
index a575da03cc..e38ae3e23e 100644
--- a/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift
+++ b/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift
@@ -34,6 +34,7 @@ extension REST {
public let ipv4AddrIn: IPv4Address
public let weight: UInt64
public let includeInCountry: Bool
+ public var daita: Bool?
public func override(ipv4AddrIn: IPv4Address?) -> Self {
return BridgeRelay(
@@ -60,6 +61,7 @@ extension REST {
public let ipv6AddrIn: IPv6Address
public let publicKey: Data
public let includeInCountry: Bool
+ public let daita: Bool?
public func override(ipv4AddrIn: IPv4Address?, ipv6AddrIn: IPv6Address?) -> Self {
return ServerRelay(
@@ -72,7 +74,8 @@ extension REST {
ipv4AddrIn: ipv4AddrIn ?? self.ipv4AddrIn,
ipv6AddrIn: ipv6AddrIn ?? self.ipv6AddrIn,
publicKey: publicKey,
- includeInCountry: includeInCountry
+ includeInCountry: includeInCountry,
+ daita: daita
)
}
}
diff --git a/ios/MullvadREST/Relay/AnyRelay.swift b/ios/MullvadREST/Relay/AnyRelay.swift
index 6c3c49aa55..13f10029b2 100644
--- a/ios/MullvadREST/Relay/AnyRelay.swift
+++ b/ios/MullvadREST/Relay/AnyRelay.swift
@@ -17,6 +17,7 @@ public protocol AnyRelay {
var weight: UInt64 { get }
var active: Bool { get }
var includeInCountry: Bool { get }
+ var daita: Bool? { get }
func override(ipv4AddrIn: IPv4Address?, ipv6AddrIn: IPv6Address?) -> Self
}
diff --git a/ios/MullvadREST/Relay/MultihopDecisionFlow.swift b/ios/MullvadREST/Relay/MultihopDecisionFlow.swift
index 53d8d1a8bc..610cdbb5e3 100644
--- a/ios/MullvadREST/Relay/MultihopDecisionFlow.swift
+++ b/ios/MullvadREST/Relay/MultihopDecisionFlow.swift
@@ -26,13 +26,13 @@ struct OneToOne: MultihopDecisionFlow {
func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays {
guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else {
guard let next else {
- throw NoRelaysSatisfyingConstraintsError()
+ throw NoRelaysSatisfyingConstraintsError(.multihopInvalidFlow)
}
return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates)
}
guard entryCandidates.first != exitCandidates.first else {
- throw NoRelaysSatisfyingConstraintsError()
+ throw NoRelaysSatisfyingConstraintsError(.entryEqualsExit)
}
let entryMatch = try relayPicker.findBestMatch(from: entryCandidates)
@@ -61,7 +61,7 @@ struct OneToMany: MultihopDecisionFlow {
guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else {
guard let next else {
- throw NoRelaysSatisfyingConstraintsError()
+ throw NoRelaysSatisfyingConstraintsError(.multihopInvalidFlow)
}
return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates)
}
@@ -100,7 +100,7 @@ struct ManyToMany: MultihopDecisionFlow {
guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else {
guard let next else {
- throw NoRelaysSatisfyingConstraintsError()
+ throw NoRelaysSatisfyingConstraintsError(.multihopInvalidFlow)
}
return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates)
}
diff --git a/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift b/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift
index b435428930..bfed08a410 100644
--- a/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift
+++ b/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift
@@ -8,10 +8,39 @@
import Foundation
+public enum NoRelaysSatisfyingConstraintsReason {
+ case filterConstraintNotMatching
+ case invalidPort
+ case entryEqualsExit
+ case multihopInvalidFlow
+ case noActiveRelaysFound
+ case noDaitaRelaysFound
+ case relayConstraintNotMatching
+}
+
public struct NoRelaysSatisfyingConstraintsError: LocalizedError {
- public init() {}
+ public let reason: NoRelaysSatisfyingConstraintsReason
public var errorDescription: String? {
- "No relays satisfying constraints."
+ switch reason {
+ case .filterConstraintNotMatching:
+ "Filter yields no matching relays"
+ case .invalidPort:
+ "Invalid port selected by RelaySelector"
+ case .entryEqualsExit:
+ "Entry and exit relays are the same"
+ case .multihopInvalidFlow:
+ "Invalid multihop decision flow"
+ case .noActiveRelaysFound:
+ "No active relays found"
+ case .noDaitaRelaysFound:
+ "No DAITA relays found"
+ case .relayConstraintNotMatching:
+ "Invalid constraint created to pick a relay"
+ }
+ }
+
+ public init(_ reason: NoRelaysSatisfyingConstraintsReason) {
+ self.reason = reason
}
}
diff --git a/ios/MullvadREST/Relay/RelayPicking.swift b/ios/MullvadREST/Relay/RelayPicking.swift
index 0877d8908e..15ae2ee90c 100644
--- a/ios/MullvadREST/Relay/RelayPicking.swift
+++ b/ios/MullvadREST/Relay/RelayPicking.swift
@@ -37,24 +37,44 @@ extension RelayPicking {
struct SinglehopPicker: RelayPicking {
let constraints: RelayConstraints
+ let daitaSettings: DAITASettings
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
- )
+ var exitCandidates = [RelayWithLocation<REST.ServerRelay>]()
- let match = try findBestMatch(from: candidates)
+ do {
+ exitCandidates = try RelaySelector.WireGuard.findCandidates(
+ by: constraints.exitLocations,
+ in: relays,
+ filterConstraint: constraints.filter,
+ daitaEnabled: daitaSettings.state.isEnabled
+ )
+ } catch let error as NoRelaysSatisfyingConstraintsError where error.reason == .noDaitaRelaysFound {
+ #if DEBUG
+ // If DAITA is enabled and no supported relays are found, we should try to find the nearest
+ // available relay that supports DAITA and use it as entry in a multihop selection.
+ var constraints = constraints
+ constraints.entryLocations = .any
+
+ return try MultihopPicker(
+ constraints: constraints,
+ daitaSettings: daitaSettings,
+ relays: relays,
+ connectionAttemptCount: connectionAttemptCount
+ ).pick()
+ #endif
+ }
+ let match = try findBestMatch(from: exitCandidates)
return SelectedRelays(entry: nil, exit: match, retryAttempt: connectionAttemptCount)
}
}
struct MultihopPicker: RelayPicking {
let constraints: RelayConstraints
+ let daitaSettings: DAITASettings
let relays: REST.ServerRelaysResponse
let connectionAttemptCount: UInt
@@ -62,13 +82,15 @@ struct MultihopPicker: RelayPicking {
let entryCandidates = try RelaySelector.WireGuard.findCandidates(
by: constraints.entryLocations,
in: relays,
- filterConstraint: constraints.filter
+ filterConstraint: constraints.filter,
+ daitaEnabled: daitaSettings.state.isEnabled
)
let exitCandidates = try RelaySelector.WireGuard.findCandidates(
by: constraints.exitLocations,
in: relays,
- filterConstraint: constraints.filter
+ filterConstraint: constraints.filter,
+ daitaEnabled: false
)
/*
diff --git a/ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift b/ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift
index 1f678e6027..f529b9b924 100644
--- a/ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift
+++ b/ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift
@@ -43,11 +43,12 @@ extension RelaySelector {
in relaysResponse: REST.ServerRelaysResponse
) -> REST.BridgeRelay? {
let mappedBridges = mapRelays(relays: relaysResponse.bridge.relays, locations: relaysResponse.locations)
- let filteredRelays = applyConstraints(
+ let filteredRelays = (try? applyConstraints(
location,
filterConstraint: filter,
+ daitaEnabled: false,
relays: mappedBridges
- )
+ )) ?? []
guard filteredRelays.isEmpty == false else { return relay(from: relaysResponse) }
// Compute the midpoint location from all the filtered relays
diff --git a/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift b/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift
index 1e61156941..4c8561f38b 100644
--- a/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift
+++ b/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift
@@ -6,6 +6,7 @@
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//
+import MullvadSettings
import MullvadTypes
extension RelaySelector {
@@ -14,13 +15,15 @@ extension RelaySelector {
public static func findCandidates(
by relayConstraint: RelayConstraint<UserSelectedRelays>,
in relays: REST.ServerRelaysResponse,
- filterConstraint: RelayConstraint<RelayFilter>
+ filterConstraint: RelayConstraint<RelayFilter>,
+ daitaEnabled: Bool
) throws -> [RelayWithLocation<REST.ServerRelay>] {
let mappedRelays = mapRelays(relays: relays.wireguard.relays, locations: relays.locations)
- return applyConstraints(
+ return try applyConstraints(
relayConstraint,
filterConstraint: filterConstraint,
+ daitaEnabled: daitaEnabled,
relays: mappedRelays
)
}
@@ -38,8 +41,12 @@ extension RelaySelector {
numberOfFailedAttempts: numberOfFailedAttempts
)
- guard let port, let relayWithLocation = pickRandomRelayByWeight(relays: relayWithLocations) else {
- throw NoRelaysSatisfyingConstraintsError()
+ guard let port else {
+ throw NoRelaysSatisfyingConstraintsError(.invalidPort)
+ }
+
+ guard let relayWithLocation = pickRandomRelayByWeight(relays: relayWithLocations) else {
+ throw NoRelaysSatisfyingConstraintsError(.relayConstraintNotMatching)
}
let endpoint = MullvadEndpoint(
diff --git a/ios/MullvadREST/Relay/RelaySelector.swift b/ios/MullvadREST/Relay/RelaySelector.swift
index da4082a1b1..98279daca5 100644
--- a/ios/MullvadREST/Relay/RelaySelector.swift
+++ b/ios/MullvadREST/Relay/RelaySelector.swift
@@ -6,7 +6,7 @@
// Copyright © 2019 Mullvad VPN AB. All rights reserved.
//
-import Foundation
+import MullvadSettings
import MullvadTypes
private let defaultPort: UInt16 = 53
@@ -135,74 +135,135 @@ public enum RelaySelector {
static func applyConstraints<T: AnyRelay>(
_ relayConstraint: RelayConstraint<UserSelectedRelays>,
filterConstraint: RelayConstraint<RelayFilter>,
+ daitaEnabled: Bool,
relays: [RelayWithLocation<T>]
- ) -> [RelayWithLocation<T>] {
- // Filter on active status, filter, and location.
- let filteredRelays = relays.filter { relayWithLocation -> Bool in
- guard relayWithLocation.relay.active else {
- return false
- }
+ ) throws -> [RelayWithLocation<T>] {
+ // Filter on active status, daita support, filter constraint and relay constraint.
+ var filteredRelays = try filterByActive(relays: relays)
+ filteredRelays = try filterByFilterConstraint(relays: filteredRelays, constraint: filterConstraint)
+ filteredRelays = try filterByLocationConstraint(relays: filteredRelays, constraint: relayConstraint)
+ filteredRelays = try filterByDaita(relays: filteredRelays, daitaEnabled: daitaEnabled)
+ return filterByCountryInclusion(relays: filteredRelays, constraint: relayConstraint)
+ }
+
+ /// Produce a port that is either user provided or randomly selected, satisfying the given constraints.
+ static func applyPortConstraint(
+ _ portConstraint: RelayConstraint<UInt16>,
+ rawPortRanges: [[UInt16]],
+ numberOfFailedAttempts: UInt
+ ) -> UInt16? {
+ switch portConstraint {
+ case let .only(port):
+ return port
+
+ case .any:
+ // 1. First two attempts should pick a random port.
+ // 2. The next two should pick port 53.
+ // 3. Repeat steps 1 and 2.
+ let useDefaultPort = (numberOfFailedAttempts % 4 == 2) || (numberOfFailedAttempts % 4 == 3)
+
+ return useDefaultPort ? defaultPort : pickRandomPort(rawPortRanges: rawPortRanges)
+ }
+ }
+
+ private static func filterByActive<T: AnyRelay>(
+ relays: [RelayWithLocation<T>]
+ ) throws -> [RelayWithLocation<T>] {
+ let filteredRelays = relays.filter { relayWithLocation in
+ relayWithLocation.relay.active
+ }
+
+ return if filteredRelays.isEmpty {
+ throw NoRelaysSatisfyingConstraintsError(.noActiveRelaysFound)
+ } else {
+ filteredRelays
+ }
+ }
- switch filterConstraint {
+ private static func filterByDaita<T: AnyRelay>(
+ relays: [RelayWithLocation<T>],
+ daitaEnabled: Bool
+ ) throws -> [RelayWithLocation<T>] {
+ guard daitaEnabled else { return relays }
+
+ let filteredRelays = relays.filter { relayWithLocation in
+ relayWithLocation.relay.daita == true
+ }
+
+ return if filteredRelays.isEmpty {
+ throw NoRelaysSatisfyingConstraintsError(.noDaitaRelaysFound)
+ } else {
+ filteredRelays
+ }
+ }
+
+ private static func filterByFilterConstraint<T: AnyRelay>(
+ relays: [RelayWithLocation<T>],
+ constraint: RelayConstraint<RelayFilter>
+ ) throws -> [RelayWithLocation<T>] {
+ let filteredRelays = relays.filter { relayWithLocation in
+ switch constraint {
case .any:
- break
+ true
case let .only(filter):
- if !relayMatchesFilter(relayWithLocation.relay, filter: filter) {
- return false
- }
+ relayMatchesFilter(relayWithLocation.relay, filter: filter)
}
+ }
- return switch relayConstraint {
+ return if filteredRelays.isEmpty {
+ throw NoRelaysSatisfyingConstraintsError(.filterConstraintNotMatching)
+ } else {
+ filteredRelays
+ }
+ }
+
+ private static func filterByLocationConstraint<T: AnyRelay>(
+ relays: [RelayWithLocation<T>],
+ constraint: RelayConstraint<UserSelectedRelays>
+ ) throws -> [RelayWithLocation<T>] {
+ let filteredRelays = relays.filter { relayWithLocation in
+ switch constraint {
case .any:
true
- case let .only(relayConstraint):
+ case let .only(constraint):
// At least one location must match the relay under test.
- relayConstraint.locations.contains { location in
+ constraint.locations.contains { location in
relayWithLocation.matches(location: location)
}
}
}
- // Filter on country inclusion.
- let includeInCountryFilteredRelays = filteredRelays.filter { relayWithLocation in
- return switch relayConstraint {
+ return if filteredRelays.isEmpty {
+ throw NoRelaysSatisfyingConstraintsError(.relayConstraintNotMatching)
+ } else {
+ filteredRelays
+ }
+ }
+
+ private static func filterByCountryInclusion<T: AnyRelay>(
+ relays: [RelayWithLocation<T>],
+ constraint: RelayConstraint<UserSelectedRelays>
+ ) -> [RelayWithLocation<T>] {
+ let filteredRelays = relays.filter { relayWithLocation in
+ return switch constraint {
case .any:
true
case let .only(relayConstraint):
relayConstraint.locations.contains { location in
if case .country = location {
- return relayWithLocation.relay.includeInCountry
+ relayWithLocation.relay.includeInCountry
+ } else {
+ false
}
- return false
}
}
}
// If no relays should be included in the matched country, instead accept all.
- if includeInCountryFilteredRelays.isEmpty {
- return filteredRelays
+ return if filteredRelays.isEmpty {
+ relays
} else {
- return includeInCountryFilteredRelays
- }
- }
-
- /// Produce a port that is either user provided or randomly selected, satisfying the given constraints.
- static func applyPortConstraint(
- _ portConstraint: RelayConstraint<UInt16>,
- rawPortRanges: [[UInt16]],
- numberOfFailedAttempts: UInt
- ) -> UInt16? {
- switch portConstraint {
- case let .only(port):
- return port
-
- case .any:
- // 1. First two attempts should pick a random port.
- // 2. The next two should pick port 53.
- // 3. Repeat steps 1 and 2.
- let useDefaultPort = (numberOfFailedAttempts % 4 == 2) || (numberOfFailedAttempts % 4 == 3)
-
- return useDefaultPort ? defaultPort : pickRandomPort(rawPortRanges: rawPortRanges)
+ filteredRelays
}
}
}
diff --git a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift
index 839b19f84a..38dd42e720 100644
--- a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift
+++ b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift
@@ -39,12 +39,14 @@ public final class RelaySelectorWrapper: RelaySelectorProtocol {
case .off:
return try SinglehopPicker(
constraints: tunnelSettings.relayConstraints,
+ daitaSettings: tunnelSettings.daita,
relays: relays,
connectionAttemptCount: connectionAttemptCount
).pick()
case .on:
return try MultihopPicker(
constraints: tunnelSettings.relayConstraints,
+ daitaSettings: tunnelSettings.daita,
relays: relays,
connectionAttemptCount: connectionAttemptCount
).pick()
diff --git a/ios/MullvadVPN/Coordinators/LocationCoordinator.swift b/ios/MullvadVPN/Coordinators/LocationCoordinator.swift
index 08224e6e75..ad58896f51 100644
--- a/ios/MullvadVPN/Coordinators/LocationCoordinator.swift
+++ b/ios/MullvadVPN/Coordinators/LocationCoordinator.swift
@@ -57,7 +57,7 @@ class LocationCoordinator: Coordinator, Presentable, Presenting {
let locationViewControllerWrapper = LocationViewControllerWrapper(
customListRepository: customListRepository,
constraints: tunnelManager.settings.relayConstraints,
- multihopEnabled: tunnelManager.settings.tunnelMultihopState == .on
+ multihopEnabled: tunnelManager.settings.tunnelMultihopState.isEnabled
)
locationViewControllerWrapper.delegate = self
diff --git a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift
index 84b5e9828d..3e98da16c7 100644
--- a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift
+++ b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift
@@ -236,6 +236,12 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific
switch error {
case .outdatedSchema:
errorString = "Unable to start tunnel connection after update. Please disconnect and reconnect."
+ case .noRelaysSatisfyingFilterConstraints:
+ errorString = "No servers match your location filter. Try changing filter settings."
+ case .multihopEntryEqualsExit:
+ errorString = "The entry and exit servers cannot be the same. Try changing one to a new server or location."
+ case .noRelaysSatisfyingDaitaConstraints:
+ errorString = "No DAITA compatible servers match your location settings. Try changing location."
case .noRelaysSatisfyingConstraints:
errorString = "No servers match your settings, try changing server or other settings."
case .invalidAccount:
diff --git a/ios/MullvadVPNTests/MullvadREST/ApiHandlers/ServerRelaysResponse+Stubs.swift b/ios/MullvadVPNTests/MullvadREST/ApiHandlers/ServerRelaysResponse+Stubs.swift
index 13b0d443e3..be0b8088a4 100644
--- a/ios/MullvadVPNTests/MullvadREST/ApiHandlers/ServerRelaysResponse+Stubs.swift
+++ b/ios/MullvadVPNTests/MullvadREST/ApiHandlers/ServerRelaysResponse+Stubs.swift
@@ -85,7 +85,8 @@ enum ServerRelaysResponseStubs {
ipv4AddrIn: .loopback,
ipv6AddrIn: .loopback,
publicKey: PrivateKey().publicKey.rawValue,
- includeInCountry: true
+ includeInCountry: true,
+ daita: true
),
REST.ServerRelay(
hostname: "se10-wireguard",
@@ -97,7 +98,8 @@ enum ServerRelaysResponseStubs {
ipv4AddrIn: .loopback,
ipv6AddrIn: .loopback,
publicKey: PrivateKey().publicKey.rawValue,
- includeInCountry: true
+ includeInCountry: true,
+ daita: false
),
REST.ServerRelay(
hostname: "se2-wireguard",
@@ -109,7 +111,8 @@ enum ServerRelaysResponseStubs {
ipv4AddrIn: .loopback,
ipv6AddrIn: .loopback,
publicKey: PrivateKey().publicKey.rawValue,
- includeInCountry: true
+ includeInCountry: true,
+ daita: false
),
REST.ServerRelay(
hostname: "se6-wireguard",
@@ -121,7 +124,8 @@ enum ServerRelaysResponseStubs {
ipv4AddrIn: .loopback,
ipv6AddrIn: .loopback,
publicKey: PrivateKey().publicKey.rawValue,
- includeInCountry: true
+ includeInCountry: true,
+ daita: false
),
REST.ServerRelay(
hostname: "us-dal-wg-001",
@@ -133,7 +137,8 @@ enum ServerRelaysResponseStubs {
ipv4AddrIn: .loopback,
ipv6AddrIn: .loopback,
publicKey: PrivateKey().publicKey.rawValue,
- includeInCountry: true
+ includeInCountry: true,
+ daita: true
),
REST.ServerRelay(
hostname: "us-nyc-wg-301",
@@ -145,7 +150,21 @@ enum ServerRelaysResponseStubs {
ipv4AddrIn: .loopback,
ipv6AddrIn: .loopback,
publicKey: PrivateKey().publicKey.rawValue,
- includeInCountry: true
+ includeInCountry: true,
+ daita: true
+ ),
+ REST.ServerRelay(
+ hostname: "us-nyc-wg-302",
+ active: false,
+ owned: true,
+ location: "us-nyc",
+ provider: "",
+ weight: 100,
+ ipv4AddrIn: .loopback,
+ ipv6AddrIn: .loopback,
+ publicKey: PrivateKey().publicKey.rawValue,
+ includeInCountry: true,
+ daita: true
),
]
),
diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift
index d6d570ee9a..190593778f 100644
--- a/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift
+++ b/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift
@@ -7,6 +7,7 @@
//
@testable import MullvadREST
+@testable import MullvadSettings
@testable import MullvadTypes
import XCTest
@@ -119,6 +120,7 @@ extension MultihopDecisionFlowTests {
return MultihopPicker(
constraints: constraints,
+ daitaSettings: DAITASettings(state: .off),
relays: sampleRelays,
connectionAttemptCount: 0
)
diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift
index 3c9acec445..9229d4ab8f 100644
--- a/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift
+++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift
@@ -9,6 +9,7 @@
import Foundation
@testable import MullvadREST
+@testable import MullvadSettings
@testable import MullvadTypes
import XCTest
@@ -23,6 +24,7 @@ class RelayPickingTests: XCTestCase {
let picker = SinglehopPicker(
constraints: constraints,
+ daitaSettings: DAITASettings(state: .off),
relays: sampleRelays,
connectionAttemptCount: 0
)
@@ -41,6 +43,7 @@ class RelayPickingTests: XCTestCase {
let picker = MultihopPicker(
constraints: constraints,
+ daitaSettings: DAITASettings(state: .off),
relays: sampleRelays,
connectionAttemptCount: 0
)
@@ -59,10 +62,16 @@ class RelayPickingTests: XCTestCase {
let picker = MultihopPicker(
constraints: constraints,
+ daitaSettings: DAITASettings(state: .off),
relays: sampleRelays,
connectionAttemptCount: 0
)
- XCTAssertThrowsError(try picker.pick())
+ XCTAssertThrowsError(
+ try picker.pick()
+ ) { error in
+ let error = error as? NoRelaysSatisfyingConstraintsError
+ XCTAssertEqual(error?.reason, .entryEqualsExit)
+ }
}
}
diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift
index 50df5635a0..2622d883fd 100644
--- a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift
+++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift
@@ -7,8 +7,10 @@
//
@testable import MullvadREST
+@testable import MullvadSettings
import MullvadTypes
import Network
+@testable import WireGuardKitTypes
import XCTest
private let portRanges: [[UInt16]] = [[4000, 4001], [5000, 5001]]
@@ -17,8 +19,6 @@ private let defaultPort: UInt16 = 53
class RelaySelectorTests: XCTestCase {
let sampleRelays = ServerRelaysResponseStubs.sampleRelays
- // MARK: - single-Hop tests
-
func testCountryConstraint() throws {
let constraints = RelayConstraints(
exitLocations: .only(UserSelectedRelays(locations: [.country("es")]))
@@ -71,9 +71,10 @@ class RelaySelectorTests: XCTestCase {
)
}
- let constrainedLocations = RelaySelector.applyConstraints(
+ let constrainedLocations = try RelaySelector.applyConstraints(
constraints.exitLocations,
filterConstraint: constraints.filter,
+ daitaEnabled: false,
relays: relayWithLocations
)
@@ -90,6 +91,19 @@ class RelaySelectorTests: XCTestCase {
)
}
+ func testNoMatchingRelayConstraintError() throws {
+ let constraints = RelayConstraints(
+ exitLocations: .only(UserSelectedRelays(locations: [.country("-")]))
+ )
+
+ XCTAssertThrowsError(
+ try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 0)
+ ) { error in
+ let error = error as? NoRelaysSatisfyingConstraintsError
+ XCTAssertEqual(error?.reason, .relayConstraintNotMatching)
+ }
+ }
+
func testSpecificPortConstraint() throws {
let constraints = RelayConstraints(
exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "sto", "se6-wireguard")])),
@@ -172,7 +186,10 @@ class RelaySelectorTests: XCTestCase {
filter: .only(filter)
)
- XCTAssertThrowsError(try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 0))
+ XCTAssertThrowsError(try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 0)) { error in
+ let error = error as? NoRelaysSatisfyingConstraintsError
+ XCTAssertEqual(error?.reason, .filterConstraintNotMatching)
+ }
}
func testRelayFilterConstraintWithCorrectProvider() throws {
@@ -197,7 +214,44 @@ class RelaySelectorTests: XCTestCase {
filter: .only(filter)
)
- XCTAssertThrowsError(try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 0))
+ XCTAssertThrowsError(try pickRelay(by: constraints, in: sampleRelays, failedAttemptCount: 0)) { error in
+ let error = error as? NoRelaysSatisfyingConstraintsError
+ XCTAssertEqual(error?.reason, .filterConstraintNotMatching)
+ }
+ }
+
+ func testRelayWithDaita() throws {
+ let hasDaitaConstraints = RelayConstraints(
+ exitLocations: .only(UserSelectedRelays(locations: [.country("es")]))
+ )
+
+ let noDaitaConstraints = RelayConstraints(
+ exitLocations: .only(UserSelectedRelays(locations: [.country("se")]))
+ )
+
+ XCTAssertNoThrow(
+ try pickRelay(
+ by: hasDaitaConstraints,
+ in: sampleRelays,
+ failedAttemptCount: 0,
+ daitaEnabled: true
+ )
+ )
+ XCTAssertThrowsError(
+ try pickRelay(by: noDaitaConstraints, in: sampleRelays, failedAttemptCount: 0, daitaEnabled: true)
+ ) { error in
+ let error = error as? NoRelaysSatisfyingConstraintsError
+ XCTAssertEqual(error?.reason, .noDaitaRelaysFound)
+ }
+ }
+
+ func testNoActiveRelaysError() throws {
+ XCTAssertThrowsError(
+ try pickRelay(by: RelayConstraints(), in: sampleRelaysNoActive, failedAttemptCount: 0)
+ ) { error in
+ let error = error as? NoRelaysSatisfyingConstraintsError
+ XCTAssertEqual(error?.reason, .noActiveRelaysFound)
+ }
}
}
@@ -205,12 +259,14 @@ extension RelaySelectorTests {
private func pickRelay(
by constraints: RelayConstraints,
in relays: REST.ServerRelaysResponse,
- failedAttemptCount: UInt
+ failedAttemptCount: UInt,
+ daitaEnabled: Bool = false
) throws -> RelaySelectorMatch {
let candidates = try RelaySelector.WireGuard.findCandidates(
by: constraints.exitLocations,
in: relays,
- filterConstraint: constraints.filter
+ filterConstraint: constraints.filter,
+ daitaEnabled: daitaEnabled
)
return try RelaySelector.WireGuard.pickCandidate(
@@ -221,3 +277,39 @@ extension RelaySelectorTests {
)
}
}
+
+extension RelaySelectorTests {
+ var sampleRelaysNoActive: REST.ServerRelaysResponse {
+ REST.ServerRelaysResponse(
+ locations: [
+ "es-mad": REST.ServerLocation(
+ country: "Spain",
+ city: "Madrid",
+ latitude: 40.408566,
+ longitude: -3.69222
+ ),
+ ],
+ wireguard: REST.ServerWireguardTunnels(
+ ipv4Gateway: .loopback,
+ ipv6Gateway: .loopback,
+ portRanges: portRanges,
+ relays: [
+ REST.ServerRelay(
+ hostname: "es1-wireguard",
+ active: false,
+ owned: true,
+ location: "es-mad",
+ provider: "",
+ weight: 500,
+ ipv4AddrIn: .loopback,
+ ipv6AddrIn: .loopback,
+ publicKey: PrivateKey().publicKey.rawValue,
+ includeInCountry: true,
+ daita: true
+ ),
+ ]
+ ),
+ bridge: REST.ServerBridges(shadowsocks: [], relays: [])
+ )
+ }
+}
diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift
index 6131b23d08..95b33883e8 100644
--- a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift
+++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift
@@ -52,4 +52,90 @@ class RelaySelectorWrapperTests: XCTestCase {
let selectedRelays = try wrapper.selectRelays(connectionAttemptCount: 0)
XCTAssertNotNil(selectedRelays.entry)
}
+
+ func testCanSelectRelayWithMultihopOnAndDaitaOn() throws {
+ let wrapper = RelaySelectorWrapper(
+ relayCache: relayCache,
+ tunnelSettingsUpdater: settingsUpdater
+ )
+
+ let constraints = RelayConstraints(
+ entryLocations: .only(UserSelectedRelays(locations: [.country("es")])), // Relay with DAITA.
+ exitLocations: .only(UserSelectedRelays(locations: [.country("us")]))
+ )
+
+ let settings = LatestTunnelSettings(
+ relayConstraints: constraints,
+ tunnelMultihopState: .on,
+ daita: DAITASettings(state: .on)
+ )
+ settingsListener.onNewSettings?(settings)
+
+ XCTAssertNoThrow(try wrapper.selectRelays(connectionAttemptCount: 0))
+ }
+
+ func testCannotSelectRelayWithMultihopOnAndDaitaOn() throws {
+ let wrapper = RelaySelectorWrapper(
+ relayCache: relayCache,
+ tunnelSettingsUpdater: settingsUpdater
+ )
+
+ let constraints = RelayConstraints(
+ entryLocations: .only(UserSelectedRelays(locations: [.country("se")])), // Relay without DAITA.
+ exitLocations: .only(UserSelectedRelays(locations: [.country("us")]))
+ )
+
+ let settings = LatestTunnelSettings(
+ relayConstraints: constraints,
+ tunnelMultihopState: .on,
+ daita: DAITASettings(state: .on)
+ )
+ settingsListener.onNewSettings?(settings)
+
+ XCTAssertThrowsError(try wrapper.selectRelays(connectionAttemptCount: 0))
+ }
+
+ func testCanSelectRelayWithMultihopOffAndDaitaOn() throws {
+ let wrapper = RelaySelectorWrapper(
+ relayCache: relayCache,
+ tunnelSettingsUpdater: settingsUpdater
+ )
+
+ let constraints = RelayConstraints(
+ exitLocations: .only(UserSelectedRelays(locations: [.country("es")])) // Relay with DAITA.
+ )
+
+ let settings = LatestTunnelSettings(
+ relayConstraints: constraints,
+ tunnelMultihopState: .off,
+ daita: DAITASettings(state: .on)
+ )
+ settingsListener.onNewSettings?(settings)
+
+ let selectedRelays = try wrapper.selectRelays(connectionAttemptCount: 0)
+ XCTAssertNil(selectedRelays.entry)
+ }
+
+ // If DAITA is enabled and no supported relays are found, we should try to find the nearest
+ // available relay that supports DAITA and use it as entry in a multihop selection.
+ func testCanSelectRelayWithMultihopOffAndDaitaOnThroughMultihop() throws {
+ let wrapper = RelaySelectorWrapper(
+ relayCache: relayCache,
+ tunnelSettingsUpdater: settingsUpdater
+ )
+
+ let constraints = RelayConstraints(
+ exitLocations: .only(UserSelectedRelays(locations: [.country("se")])) // Relay without DAITA.
+ )
+
+ let settings = LatestTunnelSettings(
+ relayConstraints: constraints,
+ tunnelMultihopState: .off,
+ daita: DAITASettings(state: .on)
+ )
+ settingsListener.onNewSettings?(settings)
+
+ let selectedRelays = try wrapper.selectRelays(connectionAttemptCount: 0)
+ XCTAssertNotNil(selectedRelays.entry)
+ }
}
diff --git a/ios/MullvadVPNTests/MullvadSettings/IPOverrideWrapperTests.swift b/ios/MullvadVPNTests/MullvadSettings/IPOverrideWrapperTests.swift
index bbaba178ab..a5e0d742e6 100644
--- a/ios/MullvadVPNTests/MullvadSettings/IPOverrideWrapperTests.swift
+++ b/ios/MullvadVPNTests/MullvadSettings/IPOverrideWrapperTests.swift
@@ -83,7 +83,8 @@ extension IPOverrideWrapperTests {
ipv4AddrIn: .any,
ipv6AddrIn: .any,
publicKey: Data(),
- includeInCountry: true
+ includeInCountry: true,
+ daita: false
)
}
diff --git a/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift b/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift
index 5038839fa5..df709b53f5 100644
--- a/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift
@@ -47,9 +47,18 @@ public struct BlockedStateErrorMapper: BlockedStateErrorMapperProtocol {
return .readSettings
}
- case is NoRelaysSatisfyingConstraintsError:
- // Returned by relay selector when there are no relays satisfying the given constraint.
- return .noRelaysSatisfyingConstraints
+ case let error as NoRelaysSatisfyingConstraintsError:
+ // Returned by relay selector when there are no relays satisfying the given constraints.
+ return switch error.reason {
+ case .filterConstraintNotMatching:
+ .noRelaysSatisfyingFilterConstraints
+ case .entryEqualsExit:
+ .multihopEntryEqualsExit
+ case .noDaitaRelaysFound:
+ .noRelaysSatisfyingDaitaConstraints
+ default:
+ .noRelaysSatisfyingConstraints
+ }
case is WireGuardAdapterError:
// Any errors that originate from wireguard adapter including failure to set tunnel settings using
diff --git a/ios/PacketTunnelCore/Actor/State+Extensions.swift b/ios/PacketTunnelCore/Actor/State+Extensions.swift
index f7d8cbfae7..4399942ead 100644
--- a/ios/PacketTunnelCore/Actor/State+Extensions.swift
+++ b/ios/PacketTunnelCore/Actor/State+Extensions.swift
@@ -194,7 +194,9 @@ extension BlockedStateReason {
case .deviceLocked:
return true
- case .noRelaysSatisfyingConstraints, .readSettings, .invalidAccount, .accountExpired, .deviceRevoked,
+ case .noRelaysSatisfyingConstraints, .noRelaysSatisfyingFilterConstraints,
+ .multihopEntryEqualsExit,
+ .noRelaysSatisfyingDaitaConstraints, .readSettings, .invalidAccount, .accountExpired, .deviceRevoked,
.tunnelAdapter, .unknown, .deviceLoggedOut, .outdatedSchema, .invalidRelayPublicKey:
return false
}
diff --git a/ios/PacketTunnelCore/Actor/State.swift b/ios/PacketTunnelCore/Actor/State.swift
index ee7e00ded3..f3c1b012b2 100644
--- a/ios/PacketTunnelCore/Actor/State.swift
+++ b/ios/PacketTunnelCore/Actor/State.swift
@@ -192,9 +192,18 @@ public enum BlockedStateReason: String, Codable, Equatable {
/// Settings schema is outdated.
case outdatedSchema
- /// No relay satisfying constraints.
+ /// General error for no relays satisfying constraints.
case noRelaysSatisfyingConstraints
+ /// No relays satisfying filter constraints.
+ case noRelaysSatisfyingFilterConstraints
+
+ /// No relays satisfying multihop constraints.
+ case multihopEntryEqualsExit
+
+ /// No relays satisfying DAITA constraints.
+ case noRelaysSatisfyingDaitaConstraints
+
/// Any other failure when reading settings.
case readSettings
diff --git a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift
index 8e2a42fd88..b300befc5a 100644
--- a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift
+++ b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift
@@ -85,7 +85,8 @@ final class AppMessageHandlerTests: XCTestCase {
let candidates = try RelaySelector.WireGuard.findCandidates(
by: relayConstraints.exitLocations,
in: ServerRelaysResponseStubs.sampleRelays,
- filterConstraint: relayConstraints.filter
+ filterConstraint: relayConstraints.filter,
+ daitaEnabled: false
)
let match = try RelaySelector.WireGuard.pickCandidate(
diff --git a/ios/PacketTunnelCoreTests/MultiHopPostQuantumKeyExchangingTests.swift b/ios/PacketTunnelCoreTests/MultiHopPostQuantumKeyExchangingTests.swift
index fea8988aa8..5b8968b1ea 100644
--- a/ios/PacketTunnelCoreTests/MultiHopPostQuantumKeyExchangingTests.swift
+++ b/ios/PacketTunnelCoreTests/MultiHopPostQuantumKeyExchangingTests.swift
@@ -27,7 +27,8 @@ final class MultiHopPostQuantumKeyExchangingTests: XCTestCase {
from: try RelaySelector.WireGuard.findCandidates(
by: relayConstraints.exitLocations,
in: ServerRelaysResponseStubs.sampleRelays,
- filterConstraint: relayConstraints.filter
+ filterConstraint: relayConstraints.filter,
+ daitaEnabled: false
),
relays: ServerRelaysResponseStubs.sampleRelays,
portConstraint: relayConstraints.port,
@@ -38,7 +39,8 @@ final class MultiHopPostQuantumKeyExchangingTests: XCTestCase {
from: try RelaySelector.WireGuard.findCandidates(
by: relayConstraints.entryLocations,
in: ServerRelaysResponseStubs.sampleRelays,
- filterConstraint: relayConstraints.filter
+ filterConstraint: relayConstraints.filter,
+ daitaEnabled: false
),
relays: ServerRelaysResponseStubs.sampleRelays,
portConstraint: relayConstraints.port,
diff --git a/ios/PacketTunnelCoreTests/PostQuantumKeyExchangingPipelineTests.swift b/ios/PacketTunnelCoreTests/PostQuantumKeyExchangingPipelineTests.swift
index ba45034935..f0725be312 100644
--- a/ios/PacketTunnelCoreTests/PostQuantumKeyExchangingPipelineTests.swift
+++ b/ios/PacketTunnelCoreTests/PostQuantumKeyExchangingPipelineTests.swift
@@ -28,7 +28,8 @@ final class PostQuantumKeyExchangingPipelineTests: XCTestCase {
from: try RelaySelector.WireGuard.findCandidates(
by: relayConstraints.exitLocations,
in: ServerRelaysResponseStubs.sampleRelays,
- filterConstraint: relayConstraints.filter
+ filterConstraint: relayConstraints.filter,
+ daitaEnabled: false
),
relays: ServerRelaysResponseStubs.sampleRelays,
portConstraint: relayConstraints.port,
@@ -39,7 +40,8 @@ final class PostQuantumKeyExchangingPipelineTests: XCTestCase {
from: try RelaySelector.WireGuard.findCandidates(
by: relayConstraints.entryLocations,
in: ServerRelaysResponseStubs.sampleRelays,
- filterConstraint: relayConstraints.filter
+ filterConstraint: relayConstraints.filter,
+ daitaEnabled: false
),
relays: ServerRelaysResponseStubs.sampleRelays,
portConstraint: relayConstraints.port,
diff --git a/ios/PacketTunnelCoreTests/SingleHopPostQuantumKeyExchangingTests.swift b/ios/PacketTunnelCoreTests/SingleHopPostQuantumKeyExchangingTests.swift
index fbadc64a66..b052ad7762 100644
--- a/ios/PacketTunnelCoreTests/SingleHopPostQuantumKeyExchangingTests.swift
+++ b/ios/PacketTunnelCoreTests/SingleHopPostQuantumKeyExchangingTests.swift
@@ -24,7 +24,8 @@ final class SingleHopPostQuantumKeyExchangingTests: XCTestCase {
let candidates = try RelaySelector.WireGuard.findCandidates(
by: relayConstraints.exitLocations,
in: ServerRelaysResponseStubs.sampleRelays,
- filterConstraint: relayConstraints.filter
+ filterConstraint: relayConstraints.filter,
+ daitaEnabled: false
)
let match = try RelaySelector.WireGuard.pickCandidate(