summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2024-10-08 11:14:40 +0200
committerBug Magnet <marco.nikic@mullvad.net>2024-10-08 11:14:40 +0200
commita6edf5c790f6dad164548594d5e6a65300c267da (patch)
tree0ec62aabb15744ff964c770f0bc3d72a883bce27
parent431969fa3f9b95bfe040c275465cf9bb233035aa (diff)
parenta7b8c354fcce19d3486d137480d1d570474e6ce8 (diff)
downloadmullvadvpn-a6edf5c790f6dad164548594d5e6a65300c267da.tar.xz
mullvadvpn-a6edf5c790f6dad164548594d5e6a65300c267da.zip
Merge branch 'fix-smart-routing-algorithm-to-select-closest-relay-ios-830'
-rw-r--r--ios/MullvadREST/Relay/MultihopDecisionFlow.swift119
-rw-r--r--ios/MullvadREST/Relay/RelayPicking.swift114
-rw-r--r--ios/MullvadREST/Relay/RelaySelector+Wireguard.swift112
-rw-r--r--ios/MullvadREST/Relay/RelaySelectorWrapper.swift17
-rw-r--r--ios/MullvadSettings/DAITASettings.swift18
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelState.swift2
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift62
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift143
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift33
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift6
-rw-r--r--ios/MullvadVPNTests/MullvadSettings/DAITASettingsTests.swift21
12 files changed, 535 insertions, 116 deletions
diff --git a/ios/MullvadREST/Relay/MultihopDecisionFlow.swift b/ios/MullvadREST/Relay/MultihopDecisionFlow.swift
index 610cdbb5e3..a8706f39fa 100644
--- a/ios/MullvadREST/Relay/MultihopDecisionFlow.swift
+++ b/ios/MullvadREST/Relay/MultihopDecisionFlow.swift
@@ -12,7 +12,11 @@ protocol MultihopDecisionFlow {
typealias RelayCandidate = RelayWithLocation<REST.ServerRelay>
init(next: MultihopDecisionFlow?, relayPicker: RelayPicking)
func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool
- func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays
+ func pick(
+ entryCandidates: [RelayCandidate],
+ exitCandidates: [RelayCandidate],
+ automaticDaitaRouting: Bool
+ ) throws -> SelectedRelays
}
struct OneToOne: MultihopDecisionFlow {
@@ -23,20 +27,32 @@ struct OneToOne: MultihopDecisionFlow {
self.relayPicker = relayPicker
}
- func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays {
+ func pick(
+ entryCandidates: [RelayCandidate],
+ exitCandidates: [RelayCandidate],
+ automaticDaitaRouting: Bool
+ ) throws -> SelectedRelays {
guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else {
guard let next else {
throw NoRelaysSatisfyingConstraintsError(.multihopInvalidFlow)
}
- return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates)
+ return try next.pick(
+ entryCandidates: entryCandidates,
+ exitCandidates: exitCandidates,
+ automaticDaitaRouting: automaticDaitaRouting
+ )
}
guard entryCandidates.first != exitCandidates.first else {
throw NoRelaysSatisfyingConstraintsError(.entryEqualsExit)
}
- let entryMatch = try relayPicker.findBestMatch(from: entryCandidates)
let exitMatch = try relayPicker.findBestMatch(from: exitCandidates)
+ let entryMatch = try relayPicker.findBestMatch(
+ from: entryCandidates,
+ closeTo: automaticDaitaRouting ? exitMatch.location : nil
+ )
+
return SelectedRelays(entry: entryMatch, exit: exitMatch, retryAttempt: relayPicker.connectionAttemptCount)
}
@@ -54,7 +70,11 @@ struct OneToMany: MultihopDecisionFlow {
self.relayPicker = relayPicker
}
- func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays {
+ func pick(
+ entryCandidates: [RelayCandidate],
+ exitCandidates: [RelayCandidate],
+ automaticDaitaRouting: Bool
+ ) throws -> SelectedRelays {
guard let multihopPicker = relayPicker as? MultihopPicker else {
fatalError("Could not cast picker to MultihopPicker")
}
@@ -63,24 +83,70 @@ struct OneToMany: MultihopDecisionFlow {
guard let next else {
throw NoRelaysSatisfyingConstraintsError(.multihopInvalidFlow)
}
- return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates)
+ return try next.pick(
+ entryCandidates: entryCandidates,
+ exitCandidates: exitCandidates,
+ automaticDaitaRouting: automaticDaitaRouting
+ )
+ }
+
+ guard !automaticDaitaRouting else {
+ return try ManyToOne(next: next, relayPicker: relayPicker)
+ .pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates, automaticDaitaRouting: true)
+ }
+
+ let entryMatch = try multihopPicker.findBestMatch(from: entryCandidates)
+ let exitMatch = try multihopPicker.exclude(relay: entryMatch, from: exitCandidates)
+
+ return SelectedRelays(entry: entryMatch, exit: exitMatch, retryAttempt: relayPicker.connectionAttemptCount)
+ }
+
+ func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool {
+ entryCandidates.count == 1 && exitCandidates.count > 1
+ }
+}
+
+struct ManyToOne: 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],
+ automaticDaitaRouting: Bool
+ ) throws -> SelectedRelays {
+ guard let multihopPicker = relayPicker as? MultihopPicker else {
+ fatalError("Could not cast picker to MultihopPicker")
}
- 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, retryAttempt: relayPicker.connectionAttemptCount)
- default:
- let exitMatch = try multihopPicker.findBestMatch(from: exitCandidates)
- let entryMatch = try multihopPicker.exclude(relay: exitMatch, from: entryCandidates)
- return SelectedRelays(entry: entryMatch, exit: exitMatch, retryAttempt: relayPicker.connectionAttemptCount)
+ guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else {
+ guard let next else {
+ throw NoRelaysSatisfyingConstraintsError(.multihopInvalidFlow)
+ }
+ return try next.pick(
+ entryCandidates: entryCandidates,
+ exitCandidates: exitCandidates,
+ automaticDaitaRouting: automaticDaitaRouting
+ )
}
+
+ let exitMatch = try multihopPicker.findBestMatch(from: exitCandidates)
+ let entryMatch = try multihopPicker.exclude(
+ relay: exitMatch,
+ from: entryCandidates,
+ closeTo: automaticDaitaRouting ? exitMatch.location : nil
+ )
+
+ return SelectedRelays(entry: entryMatch, exit: exitMatch, retryAttempt: relayPicker.connectionAttemptCount)
}
func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool {
- (entryCandidates.count == 1 && exitCandidates.count > 1) ||
- (entryCandidates.count > 1 && exitCandidates.count == 1)
+ entryCandidates.count > 1 && exitCandidates.count == 1
}
}
@@ -93,7 +159,11 @@ struct ManyToMany: MultihopDecisionFlow {
self.relayPicker = relayPicker
}
- func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays {
+ func pick(
+ entryCandidates: [RelayCandidate],
+ exitCandidates: [RelayCandidate],
+ automaticDaitaRouting: Bool
+ ) throws -> SelectedRelays {
guard let multihopPicker = relayPicker as? MultihopPicker else {
fatalError("Could not cast picker to MultihopPicker")
}
@@ -102,11 +172,20 @@ struct ManyToMany: MultihopDecisionFlow {
guard let next else {
throw NoRelaysSatisfyingConstraintsError(.multihopInvalidFlow)
}
- return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates)
+ return try next.pick(
+ entryCandidates: entryCandidates,
+ exitCandidates: exitCandidates,
+ automaticDaitaRouting: automaticDaitaRouting
+ )
}
let exitMatch = try multihopPicker.findBestMatch(from: exitCandidates)
- let entryMatch = try multihopPicker.exclude(relay: exitMatch, from: entryCandidates)
+ let entryMatch = try multihopPicker.exclude(
+ relay: exitMatch,
+ from: entryCandidates,
+ closeTo: automaticDaitaRouting ? exitMatch.location : nil
+ )
+
return SelectedRelays(entry: entryMatch, exit: exitMatch, retryAttempt: relayPicker.connectionAttemptCount)
}
diff --git a/ios/MullvadREST/Relay/RelayPicking.swift b/ios/MullvadREST/Relay/RelayPicking.swift
index 4d752d3707..a3680e28f3 100644
--- a/ios/MullvadREST/Relay/RelayPicking.swift
+++ b/ios/MullvadREST/Relay/RelayPicking.swift
@@ -13,18 +13,21 @@ protocol RelayPicking {
var relays: REST.ServerRelaysResponse { get }
var constraints: RelayConstraints { get }
var connectionAttemptCount: UInt { get }
+ var daitaSettings: DAITASettings { get }
func pick() throws -> SelectedRelays
}
extension RelayPicking {
func findBestMatch(
- from candidates: [RelayWithLocation<REST.ServerRelay>]
+ from candidates: [RelayWithLocation<REST.ServerRelay>],
+ closeTo location: Location? = nil
) throws -> SelectedRelay {
let match = try RelaySelector.WireGuard.pickCandidate(
from: candidates,
relays: relays,
portConstraint: constraints.port,
- numberOfFailedAttempts: connectionAttemptCount
+ numberOfFailedAttempts: connectionAttemptCount,
+ closeTo: location
)
return SelectedRelay(
@@ -36,56 +39,51 @@ extension RelayPicking {
}
struct SinglehopPicker: RelayPicking {
- let constraints: RelayConstraints
- let daitaSettings: DAITASettings
let relays: REST.ServerRelaysResponse
+ let constraints: RelayConstraints
let connectionAttemptCount: UInt
+ let daitaSettings: DAITASettings
func pick() throws -> SelectedRelays {
- var exitCandidates = [RelayWithLocation<REST.ServerRelay>]()
-
do {
- exitCandidates = try RelaySelector.WireGuard.findCandidates(
+ let exitCandidates = try RelaySelector.WireGuard.findCandidates(
by: constraints.exitLocations,
in: relays,
filterConstraint: constraints.filter,
daitaEnabled: daitaSettings.daitaState.isEnabled
)
+
+ let match = try findBestMatch(from: exitCandidates)
+ return SelectedRelays(entry: nil, exit: match, retryAttempt: connectionAttemptCount)
} 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
+ // If DAITA is on and Direct only is off, 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
+ if daitaSettings.shouldDoAutomaticRouting {
+ var constraints = constraints
+ constraints.entryLocations = .any
- return try MultihopPicker(
- constraints: constraints,
- daitaSettings: daitaSettings,
- relays: relays,
- connectionAttemptCount: connectionAttemptCount
- ).pick()
- #endif
+ return try MultihopPicker(
+ relays: relays,
+ constraints: constraints,
+ connectionAttemptCount: connectionAttemptCount,
+ daitaSettings: daitaSettings,
+ automaticDaitaRouting: true
+ ).pick()
+ } else {
+ throw error
+ }
}
-
- 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 constraints: RelayConstraints
let connectionAttemptCount: UInt
+ let daitaSettings: DAITASettings
+ let automaticDaitaRouting: Bool
func pick() throws -> SelectedRelays {
- let entryCandidates = try RelaySelector.WireGuard.findCandidates(
- by: constraints.entryLocations,
- in: relays,
- filterConstraint: constraints.filter,
- daitaEnabled: daitaSettings.daitaState.isEnabled
- )
-
let exitCandidates = try RelaySelector.WireGuard.findCandidates(
by: constraints.exitLocations,
in: relays,
@@ -96,15 +94,20 @@ struct MultihopPicker: RelayPicking {
/*
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.
+ 2. Entry constraint matches only a single relay and the other multiple relays. The single relay
+ is selected and excluded from the list of multiple relays.
+ 3. Exit constraint matches multiple relays and the other a single relay. The single relay
+ is selected and excluded from the list of multiple relays.
+ 4. 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,
+ next: ManyToOne(
+ next: ManyToMany(
+ next: nil,
+ relayPicker: self
+ ),
relayPicker: self
),
relayPicker: self
@@ -112,17 +115,50 @@ struct MultihopPicker: RelayPicking {
relayPicker: self
)
- return try decisionFlow.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates)
+ do {
+ let entryCandidates = try RelaySelector.WireGuard.findCandidates(
+ by: constraints.entryLocations,
+ in: relays,
+ filterConstraint: constraints.filter,
+ daitaEnabled: daitaSettings.daitaState.isEnabled
+ )
+
+ return try decisionFlow.pick(
+ entryCandidates: entryCandidates,
+ exitCandidates: exitCandidates,
+ automaticDaitaRouting: automaticDaitaRouting
+ )
+ } catch let error as NoRelaysSatisfyingConstraintsError where error.reason == .noDaitaRelaysFound {
+ // If DAITA is on and Direct only is off, 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.
+ if daitaSettings.shouldDoAutomaticRouting {
+ let entryCandidates = try RelaySelector.WireGuard.findCandidates(
+ by: .any,
+ in: relays,
+ filterConstraint: constraints.filter,
+ daitaEnabled: true
+ )
+
+ return try decisionFlow.pick(
+ entryCandidates: entryCandidates,
+ exitCandidates: exitCandidates,
+ automaticDaitaRouting: true
+ )
+ } else {
+ throw error
+ }
+ }
}
func exclude(
relay: SelectedRelay,
- from candidates: [RelayWithLocation<REST.ServerRelay>]
+ from candidates: [RelayWithLocation<REST.ServerRelay>],
+ closeTo location: Location? = nil
) throws -> SelectedRelay {
let filteredCandidates = candidates.filter { relayWithLocation in
relayWithLocation.relay.hostname != relay.hostname
}
- return try findBestMatch(from: filteredCandidates)
+ return try findBestMatch(from: filteredCandidates, closeTo: location)
}
}
diff --git a/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift b/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift
index 4c8561f38b..0317788e23 100644
--- a/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift
+++ b/ios/MullvadREST/Relay/RelaySelector+Wireguard.swift
@@ -33,38 +33,106 @@ extension RelaySelector {
from relayWithLocations: [RelayWithLocation<REST.ServerRelay>],
relays: REST.ServerRelaysResponse,
portConstraint: RelayConstraint<UInt16>,
- numberOfFailedAttempts: UInt
+ numberOfFailedAttempts: UInt,
+ closeTo referenceLocation: Location? = nil
) throws -> RelaySelectorMatch {
- let port = applyPortConstraint(
- portConstraint,
- rawPortRanges: relays.wireguard.portRanges,
+ let port = try evaluatePort(
+ relays: relays,
+ portConstraint: portConstraint,
numberOfFailedAttempts: numberOfFailedAttempts
)
- guard let port else {
- throw NoRelaysSatisfyingConstraintsError(.invalidPort)
+ var relayWithLocation: RelayWithLocation<REST.ServerRelay>?
+ if let referenceLocation {
+ let relay = closestRelay(to: referenceLocation, using: relayWithLocations)
+ relayWithLocation = relayWithLocations.first(where: { $0.relay == relay })
}
- guard let relayWithLocation = pickRandomRelayByWeight(relays: relayWithLocations) else {
+ guard
+ let relayWithLocation = relayWithLocation ?? pickRandomRelayByWeight(relays: relayWithLocations)
+ else {
throw NoRelaysSatisfyingConstraintsError(.relayConstraintNotMatching)
}
- let endpoint = MullvadEndpoint(
- ipv4Relay: IPv4Endpoint(
- ip: relayWithLocation.relay.ipv4AddrIn,
- port: port
- ),
- ipv6Relay: nil,
- ipv4Gateway: relays.wireguard.ipv4Gateway,
- ipv6Gateway: relays.wireguard.ipv6Gateway,
- publicKey: relayWithLocation.relay.publicKey
- )
+ return createMatch(for: relayWithLocation, port: port, relays: relays)
+ }
- return RelaySelectorMatch(
- endpoint: endpoint,
- relay: relayWithLocation.relay,
- location: relayWithLocation.serverLocation
- )
+ public static func closestRelay(
+ to location: Location,
+ using relayWithLocations: [RelayWithLocation<REST.ServerRelay>]
+ ) -> REST.ServerRelay? {
+ let relaysWithDistance = relayWithLocations.map {
+ RelayWithDistance(
+ relay: $0.relay,
+ distance: Haversine.distance(
+ location.latitude,
+ location.longitude,
+ $0.serverLocation.latitude,
+ $0.serverLocation.longitude
+ )
+ )
+ }.sorted {
+ $0.distance < $1.distance
+ }.prefix(5)
+
+ let relaysGroupedByDistance = Dictionary(grouping: relaysWithDistance, by: { $0.distance })
+ guard let closetsRelayGroup = relaysGroupedByDistance.min(by: { $0.key < $1.key })?.value else {
+ return nil
+ }
+
+ var greatestDistance = 0.0
+ closetsRelayGroup.forEach {
+ if $0.distance > greatestDistance {
+ greatestDistance = $0.distance
+ }
+ }
+
+ let closestRelay = rouletteSelection(relays: closetsRelayGroup, weightFunction: { relay in
+ UInt64(1 + greatestDistance - relay.distance)
+ })
+
+ return closestRelay?.relay
}
}
+
+ private static func evaluatePort(
+ relays: REST.ServerRelaysResponse,
+ portConstraint: RelayConstraint<UInt16>,
+ numberOfFailedAttempts: UInt
+ ) throws -> UInt16 {
+ let port = applyPortConstraint(
+ portConstraint,
+ rawPortRanges: relays.wireguard.portRanges,
+ numberOfFailedAttempts: numberOfFailedAttempts
+ )
+
+ guard let port else {
+ throw NoRelaysSatisfyingConstraintsError(.invalidPort)
+ }
+
+ return port
+ }
+
+ private static func createMatch(
+ for relayWithLocation: RelayWithLocation<REST.ServerRelay>,
+ port: UInt16,
+ relays: REST.ServerRelaysResponse
+ ) -> RelaySelectorMatch {
+ let endpoint = MullvadEndpoint(
+ ipv4Relay: IPv4Endpoint(
+ ip: relayWithLocation.relay.ipv4AddrIn,
+ port: port
+ ),
+ ipv6Relay: nil,
+ ipv4Gateway: relays.wireguard.ipv4Gateway,
+ ipv6Gateway: relays.wireguard.ipv6Gateway,
+ publicKey: relayWithLocation.relay.publicKey
+ )
+
+ return RelaySelectorMatch(
+ endpoint: endpoint,
+ relay: relayWithLocation.relay,
+ location: relayWithLocation.serverLocation
+ )
+ }
}
diff --git a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift
index 32429d93cd..a3fe1c216f 100644
--- a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift
+++ b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift
@@ -22,20 +22,21 @@ public final class RelaySelectorWrapper: RelaySelectorProtocol {
) throws -> SelectedRelays {
let relays = try relayCache.read().relays
- switch tunnelSettings.tunnelMultihopState {
+ return switch tunnelSettings.tunnelMultihopState {
case .off:
- return try SinglehopPicker(
- constraints: tunnelSettings.relayConstraints,
- daitaSettings: tunnelSettings.daita,
+ try SinglehopPicker(
relays: relays,
- connectionAttemptCount: connectionAttemptCount
+ constraints: tunnelSettings.relayConstraints,
+ connectionAttemptCount: connectionAttemptCount,
+ daitaSettings: tunnelSettings.daita
).pick()
case .on:
- return try MultihopPicker(
+ try MultihopPicker(
+ relays: relays,
constraints: tunnelSettings.relayConstraints,
+ connectionAttemptCount: connectionAttemptCount,
daitaSettings: tunnelSettings.daita,
- relays: relays,
- connectionAttemptCount: connectionAttemptCount
+ automaticDaitaRouting: false
).pick()
}
}
diff --git a/ios/MullvadSettings/DAITASettings.swift b/ios/MullvadSettings/DAITASettings.swift
index 9e0a0f1fb7..344e56f166 100644
--- a/ios/MullvadSettings/DAITASettings.swift
+++ b/ios/MullvadSettings/DAITASettings.swift
@@ -8,7 +8,7 @@
import Foundation
-/// Whether DAITA is enabled
+/// Whether DAITA is enabled.
public enum DAITAState: Codable {
case on
case off
@@ -18,8 +18,8 @@ public enum DAITAState: Codable {
}
}
-/// Whether smart routing is enabled
-public enum SmartRoutingState: Codable {
+/// Whether "direct only" is enabled, meaning no automatic routing to DAITA relays.
+public enum DirectOnlyState: Codable {
case on
case off
@@ -38,11 +38,15 @@ public struct DAITASettings: Codable, Equatable {
public let state: DAITAState = .off
public let daitaState: DAITAState
- public let smartRoutingState: SmartRoutingState
+ public let directOnlyState: DirectOnlyState
- public init(daitaState: DAITAState = .off, smartRoutingState: SmartRoutingState = .off) {
+ public var shouldDoAutomaticRouting: Bool {
+ daitaState.isEnabled && !directOnlyState.isEnabled
+ }
+
+ public init(daitaState: DAITAState = .off, directOnlyState: DirectOnlyState = .off) {
self.daitaState = daitaState
- self.smartRoutingState = smartRoutingState
+ self.directOnlyState = directOnlyState
}
public init(from decoder: any Decoder) throws {
@@ -52,7 +56,7 @@ public struct DAITASettings: Codable, Equatable {
?? container.decodeIfPresent(DAITAState.self, forKey: .state)
?? .off
- smartRoutingState = try container.decodeIfPresent(SmartRoutingState.self, forKey: .smartRoutingState)
+ directOnlyState = try container.decodeIfPresent(DirectOnlyState.self, forKey: .directOnlyState)
?? .off
}
}
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 5942519486..8c5a8f19ac 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -574,6 +574,7 @@
7A9F29392CABFAFC005F2089 /* InfoHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9F29382CABFAEC005F2089 /* InfoHeaderView.swift */; };
7A9F293B2CAC4443005F2089 /* InfoHeaderConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9F293A2CAC4420005F2089 /* InfoHeaderConfig.swift */; };
7A9F293D2CAD2FD5005F2089 /* InfoModalConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9F293C2CAD2FCF005F2089 /* InfoModalConfig.swift */; };
+ 7A9F28FC2CA69D0C005F2089 /* DAITASettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9F28FB2CA69D04005F2089 /* DAITASettingsTests.swift */; };
7A9FA1422A2E3306000B728D /* CheckboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9FA1412A2E3306000B728D /* CheckboxView.swift */; };
7A9FA1442A2E3FE5000B728D /* CheckableSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9FA1432A2E3FE5000B728D /* CheckableSettingsCell.swift */; };
7AA513862BC91C6B00D081A4 /* LogRotationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA513852BC91C6B00D081A4 /* LogRotationTests.swift */; };
@@ -1891,6 +1892,7 @@
7A9F29382CABFAEC005F2089 /* InfoHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoHeaderView.swift; sourceTree = "<group>"; };
7A9F293A2CAC4420005F2089 /* InfoHeaderConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoHeaderConfig.swift; sourceTree = "<group>"; };
7A9F293C2CAD2FCF005F2089 /* InfoModalConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoModalConfig.swift; sourceTree = "<group>"; };
+ 7A9F28FB2CA69D04005F2089 /* DAITASettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAITASettingsTests.swift; sourceTree = "<group>"; };
7A9FA1412A2E3306000B728D /* CheckboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxView.swift; sourceTree = "<group>"; };
7A9FA1432A2E3FE5000B728D /* CheckableSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckableSettingsCell.swift; sourceTree = "<group>"; };
7AA513852BC91C6B00D081A4 /* LogRotationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogRotationTests.swift; sourceTree = "<group>"; };
@@ -2573,6 +2575,7 @@
children = (
7A83A0C52B29A750008B5CE7 /* APIAccessMethodsTests.swift */,
7A9BE5A82B90806800E2A7D0 /* CustomListsRepositoryStub.swift */,
+ 7A9F28FB2CA69D04005F2089 /* DAITASettingsTests.swift */,
A9B6AC192ADE8FBB00F7802A /* InMemorySettingsStore.swift */,
7ADCB2D92B6A730400C88F89 /* IPOverrideRepositoryStub.swift */,
7A5869C22B5820CE00640D27 /* IPOverrideRepositoryTests.swift */,
@@ -5413,6 +5416,7 @@
A9A5FA2C2ACB05160083449F /* DeviceCheckOperationTests.swift in Sources */,
A9A5FA2D2ACB05160083449F /* DurationTests.swift in Sources */,
A9A5FA2E2ACB05160083449F /* FileCacheTests.swift in Sources */,
+ 7A9F28FC2CA69D0C005F2089 /* DAITASettingsTests.swift in Sources */,
A9A5FA2F2ACB05160083449F /* FixedWidthIntegerArithmeticsTests.swift in Sources */,
7AA513862BC91C6B00D081A4 /* LogRotationTests.swift in Sources */,
F04413622BA45CE30018A6EE /* CustomListLocationNodeBuilder.swift in Sources */,
diff --git a/ios/MullvadVPN/TunnelManager/TunnelState.swift b/ios/MullvadVPN/TunnelManager/TunnelState.swift
index efcf9b1f41..ea55431d5a 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelState.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelState.swift
@@ -118,7 +118,7 @@ enum TunnelState: Equatable, CustomStringConvertible {
"""
negotiating key with exit relay: \(tunnelRelays.exit.hostname)\
\(tunnelRelays.entry.flatMap { " via \($0.hostname)" } ?? "")\
- "isPostQuantum: \(isPostQuantum), isDaita: \(isDaita)
+ , isPostQuantum: \(isPostQuantum), isDaita: \(isDaita)
"""
}
}
diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift
index 7f004ad5a9..b315b899d4 100644
--- a/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift
+++ b/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift
@@ -37,8 +37,8 @@ class MultihopDecisionFlowTests: XCTestCase {
let oneToMany = OneToMany(next: nil, relayPicker: picker)
XCTAssertTrue(oneToMany.canHandle(
- entryCandidates: [seSto2, seSto6],
- exitCandidates: [seSto2]
+ entryCandidates: [seSto2],
+ exitCandidates: [seSto2, seSto6]
))
XCTAssertFalse(oneToMany.canHandle(
@@ -52,6 +52,25 @@ class MultihopDecisionFlowTests: XCTestCase {
))
}
+ func testManyToOneCanHandle() throws {
+ let manyToOne = ManyToOne(next: nil, relayPicker: picker)
+
+ XCTAssertTrue(manyToOne.canHandle(
+ entryCandidates: [seSto2, seSto6],
+ exitCandidates: [seSto2]
+ ))
+
+ XCTAssertFalse(manyToOne.canHandle(
+ entryCandidates: [seSto6],
+ exitCandidates: [seSto2]
+ ))
+
+ XCTAssertFalse(manyToOne.canHandle(
+ entryCandidates: [seSto2, seSto6],
+ exitCandidates: [seSto2, seSto6]
+ ))
+ }
+
func testManyToManyCanHandle() throws {
let manyToMany = ManyToMany(next: nil, relayPicker: picker)
@@ -77,7 +96,11 @@ class MultihopDecisionFlowTests: XCTestCase {
let entryCandidates = [seSto2]
let exitCandidates = [seSto6]
- let selectedRelays = try oneToOne.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates)
+ let selectedRelays = try oneToOne.pick(
+ entryCandidates: entryCandidates,
+ exitCandidates: exitCandidates,
+ automaticDaitaRouting: false
+ )
XCTAssertEqual(selectedRelays.entry?.hostname, "se2-wireguard")
XCTAssertEqual(selectedRelays.exit.hostname, "se6-wireguard")
@@ -86,10 +109,30 @@ class MultihopDecisionFlowTests: XCTestCase {
func testOneToManyPick() throws {
let oneToMany = OneToMany(next: nil, relayPicker: picker)
+ let entryCandidates = [seSto2]
+ let exitCandidates = [seSto2, seSto6]
+
+ let selectedRelays = try oneToMany.pick(
+ entryCandidates: entryCandidates,
+ exitCandidates: exitCandidates,
+ automaticDaitaRouting: false
+ )
+
+ XCTAssertEqual(selectedRelays.entry?.hostname, "se2-wireguard")
+ XCTAssertEqual(selectedRelays.exit.hostname, "se6-wireguard")
+ }
+
+ func testManyToOnePick() throws {
+ let manyToOne = ManyToOne(next: nil, relayPicker: picker)
+
let entryCandidates = [seSto2, seSto6]
let exitCandidates = [seSto2]
- let selectedRelays = try oneToMany.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates)
+ let selectedRelays = try manyToOne.pick(
+ entryCandidates: entryCandidates,
+ exitCandidates: exitCandidates,
+ automaticDaitaRouting: false
+ )
XCTAssertEqual(selectedRelays.entry?.hostname, "se6-wireguard")
XCTAssertEqual(selectedRelays.exit.hostname, "se2-wireguard")
@@ -101,7 +144,11 @@ class MultihopDecisionFlowTests: XCTestCase {
let entryCandidates = [seSto2, seSto6]
let exitCandidates = [seSto2, seSto6]
- let selectedRelays = try manyToMany.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates)
+ let selectedRelays = try manyToMany.pick(
+ entryCandidates: entryCandidates,
+ exitCandidates: exitCandidates,
+ automaticDaitaRouting: false
+ )
if selectedRelays.exit.hostname == "se2-wireguard" {
XCTAssertEqual(selectedRelays.entry?.hostname, "se6-wireguard")
@@ -119,10 +166,11 @@ extension MultihopDecisionFlowTests {
)
return MultihopPicker(
+ relays: sampleRelays,
constraints: constraints,
+ connectionAttemptCount: 0,
daitaSettings: DAITASettings(daitaState: .off),
- relays: sampleRelays,
- connectionAttemptCount: 0
+ automaticDaitaRouting: false
)
}
diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift
index 73d538ab1b..fb42ba17f7 100644
--- a/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift
+++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift
@@ -23,10 +23,10 @@ class RelayPickingTests: XCTestCase {
)
let picker = SinglehopPicker(
- constraints: constraints,
- daitaSettings: DAITASettings(daitaState: .off),
relays: sampleRelays,
- connectionAttemptCount: 0
+ constraints: constraints,
+ connectionAttemptCount: 0,
+ daitaSettings: DAITASettings()
)
let selectedRelays = try picker.pick()
@@ -42,10 +42,11 @@ class RelayPickingTests: XCTestCase {
)
let picker = MultihopPicker(
- constraints: constraints,
- daitaSettings: DAITASettings(daitaState: .off),
relays: sampleRelays,
- connectionAttemptCount: 0
+ constraints: constraints,
+ connectionAttemptCount: 0,
+ daitaSettings: DAITASettings(),
+ automaticDaitaRouting: false
)
let selectedRelays = try picker.pick()
@@ -61,10 +62,11 @@ class RelayPickingTests: XCTestCase {
)
let picker = MultihopPicker(
- constraints: constraints,
- daitaSettings: DAITASettings(daitaState: .off),
relays: sampleRelays,
- connectionAttemptCount: 0
+ constraints: constraints,
+ connectionAttemptCount: 0,
+ daitaSettings: DAITASettings(),
+ automaticDaitaRouting: false
)
XCTAssertThrowsError(
@@ -74,4 +76,127 @@ class RelayPickingTests: XCTestCase {
XCTAssertEqual(error?.reason, .entryEqualsExit)
}
}
+
+ func testDirectOnlyOffDaitaOnForSinglehopWithoutDaitaRelay() throws {
+ let constraints = RelayConstraints(
+ exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")]))
+ )
+
+ let picker = SinglehopPicker(
+ relays: sampleRelays,
+ constraints: constraints,
+ connectionAttemptCount: 0,
+ daitaSettings: DAITASettings(daitaState: .on, directOnlyState: .off)
+ )
+
+ let selectedRelays = try picker.pick()
+
+ XCTAssertEqual(selectedRelays.entry?.hostname, "es1-wireguard") // Madrid relay is closest to exit relay.
+ XCTAssertEqual(selectedRelays.exit.hostname, "se10-wireguard")
+ }
+
+ func testDirectOnlyOnDaitaOnForSinglehopWithoutDaitaRelay() throws {
+ let constraints = RelayConstraints(
+ exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")]))
+ )
+
+ let picker = SinglehopPicker(
+ relays: sampleRelays,
+ constraints: constraints,
+ connectionAttemptCount: 0,
+ daitaSettings: DAITASettings(daitaState: .on, directOnlyState: .on)
+ )
+
+ XCTAssertThrowsError(try picker.pick())
+ }
+
+ func testDirectOnlyOffDaitaOnForSinglehopWithDaitaRelay() throws {
+ let constraints = RelayConstraints(
+ exitLocations: .only(UserSelectedRelays(locations: [.hostname("es", "mad", "es1-wireguard")]))
+ )
+
+ let picker = SinglehopPicker(
+ relays: sampleRelays,
+ constraints: constraints,
+ connectionAttemptCount: 0,
+ daitaSettings: DAITASettings(daitaState: .on, directOnlyState: .off)
+ )
+
+ let selectedRelays = try picker.pick()
+
+ XCTAssertNil(selectedRelays.entry?.hostname)
+ XCTAssertEqual(selectedRelays.exit.hostname, "es1-wireguard")
+ }
+
+ func testDirectOnlyOnDaitaOnForSinglehopWithDaitaRelay() throws {
+ let constraints = RelayConstraints(
+ exitLocations: .only(UserSelectedRelays(locations: [.hostname("es", "mad", "es1-wireguard")]))
+ )
+
+ let picker = SinglehopPicker(
+ relays: sampleRelays,
+ constraints: constraints,
+ connectionAttemptCount: 0,
+ daitaSettings: DAITASettings(daitaState: .on, directOnlyState: .on)
+ )
+
+ let selectedRelays = try picker.pick()
+
+ XCTAssertNil(selectedRelays.entry?.hostname)
+ XCTAssertEqual(selectedRelays.exit.hostname, "es1-wireguard")
+ }
+
+ func testDirectOnlyOffDaitaOnForMultihopWithDaitaRelay() throws {
+ let constraints = RelayConstraints(
+ entryLocations: .only(UserSelectedRelays(locations: [.hostname("us", "nyc", "us-nyc-wg-301")])),
+ exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")]))
+ )
+
+ let picker = SinglehopPicker(
+ relays: sampleRelays,
+ constraints: constraints,
+ connectionAttemptCount: 0,
+ daitaSettings: DAITASettings(daitaState: .on, directOnlyState: .off)
+ )
+
+ let selectedRelays = try picker.pick()
+
+ XCTAssertEqual(selectedRelays.entry?.hostname, "es1-wireguard") // Madrid relay is closest to exit relay.
+ XCTAssertEqual(selectedRelays.exit.hostname, "se10-wireguard")
+ }
+
+ func testDirectOnlyOffDaitaOnForMultihopWithoutDaitaRelay() throws {
+ let constraints = RelayConstraints(
+ entryLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])),
+ exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")]))
+ )
+
+ let picker = SinglehopPicker(
+ relays: sampleRelays,
+ constraints: constraints,
+ connectionAttemptCount: 0,
+ daitaSettings: DAITASettings(daitaState: .on, directOnlyState: .off)
+ )
+
+ let selectedRelays = try picker.pick()
+
+ XCTAssertEqual(selectedRelays.entry?.hostname, "es1-wireguard") // Madrid relay is closest to exit relay.
+ XCTAssertEqual(selectedRelays.exit.hostname, "se10-wireguard")
+ }
+
+ func testDirectOnlyOnDaitaOnForMultihopWithoutDaitaRelay() throws {
+ let constraints = RelayConstraints(
+ entryLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])),
+ exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")]))
+ )
+
+ let picker = SinglehopPicker(
+ relays: sampleRelays,
+ constraints: constraints,
+ connectionAttemptCount: 0,
+ daitaSettings: DAITASettings(daitaState: .on, directOnlyState: .on)
+ )
+
+ XCTAssertThrowsError(try picker.pick())
+ }
}
diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift
index 5b2a775a4f..d15d18aab3 100644
--- a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift
+++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift
@@ -136,6 +136,39 @@ class RelaySelectorTests: XCTestCase {
XCTAssertTrue(allPorts.contains(result.endpoint.ipv4Relay.port))
}
+ func testClosestRelay() throws {
+ let relayWithLocations = try sampleRelays.wireguard.relays.map {
+ let serverLocation = try XCTUnwrap(sampleRelays.locations[$0.location])
+ let location = Location(
+ country: serverLocation.country,
+ countryCode: serverLocation.country,
+ city: serverLocation.city,
+ cityCode: serverLocation.city,
+ latitude: serverLocation.latitude,
+ longitude: serverLocation.longitude
+ )
+
+ return RelayWithLocation(relay: $0, serverLocation: location)
+ }
+
+ let sampleLocation = try XCTUnwrap(sampleRelays.locations["se-got"])
+ let location = Location(
+ country: "Sweden",
+ countryCode: sampleLocation.country,
+ city: "Gothenburg",
+ cityCode: sampleLocation.city,
+ latitude: sampleLocation.latitude,
+ longitude: sampleLocation.longitude
+ )
+
+ let selectedRelay = RelaySelector.WireGuard.closestRelay(
+ to: location,
+ using: relayWithLocations
+ )
+
+ XCTAssertEqual(selectedRelay?.hostname, "se10-wireguard")
+ }
+
func testClosestShadowsocksRelay() throws {
let constraints = RelayConstraints(
exitLocations: .only(UserSelectedRelays(locations: [.city("se", "sto")]))
diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift
index 2bc33ca86e..b8ad4d2d46 100644
--- a/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift
+++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorWrapperTests.swift
@@ -80,13 +80,13 @@ class RelaySelectorWrapperTests: XCTestCase {
XCTAssertNoThrow(try wrapper.selectRelays(tunnelSettings: settings, connectionAttemptCount: 0))
}
- func testCannotSelectRelayWithMultihopOnAndDaitaOn() throws {
+ func testCannotSelectRelayWithMultihopOnDaitaOnDirectOnlyOn() throws {
let wrapper = RelaySelectorWrapper(relayCache: relayCache)
let settings = LatestTunnelSettings(
relayConstraints: multihopWithoutDaitaConstraints,
tunnelMultihopState: .on,
- daita: DAITASettings(daitaState: .on)
+ daita: DAITASettings(daitaState: .on, directOnlyState: .on)
)
XCTAssertThrowsError(try wrapper.selectRelays(tunnelSettings: settings, connectionAttemptCount: 0))
@@ -107,7 +107,7 @@ class RelaySelectorWrapperTests: XCTestCase {
// 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 {
+ func testCanSelectRelayWithMultihopOffDaitaOnThroughMultihop() throws {
let wrapper = RelaySelectorWrapper(relayCache: relayCache)
let settings = LatestTunnelSettings(
diff --git a/ios/MullvadVPNTests/MullvadSettings/DAITASettingsTests.swift b/ios/MullvadVPNTests/MullvadSettings/DAITASettingsTests.swift
new file mode 100644
index 0000000000..f153de9a55
--- /dev/null
+++ b/ios/MullvadVPNTests/MullvadSettings/DAITASettingsTests.swift
@@ -0,0 +1,21 @@
+//
+// DAITASettingsTests.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-09-27.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+@testable import MullvadSettings
+import XCTest
+
+final class DAITASettingsTests: XCTestCase {
+ func testShouldDoDirectOnly() throws {
+ let settings = DAITASettings()
+
+ XCTAssertEqual(
+ settings.shouldDoAutomaticRouting,
+ settings.daitaState == .on && settings.directOnlyState == .off
+ )
+ }
+}