Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow relay selector to filter DAITA enabled relays
Browse files Browse the repository at this point in the history
Jon Petersson committed Aug 12, 2024
1 parent 91e0194 commit c5fe10c
Showing 24 changed files with 419 additions and 90 deletions.
5 changes: 4 additions & 1 deletion ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift
Original file line number Diff line number Diff line change
@@ -34,6 +34,7 @@ extension REST {
public let ipv4AddrIn: IPv4Address
public let weight: UInt64
public let includeInCountry: Bool
public var daita: Bool? = nil

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
)
}
}
1 change: 1 addition & 0 deletions ios/MullvadREST/Relay/AnyRelay.swift
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 4 additions & 4 deletions ios/MullvadREST/Relay/MultihopDecisionFlow.swift
Original file line number Diff line number Diff line change
@@ -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(reason: .multihopOther)
}
return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates)
}

guard entryCandidates.first != exitCandidates.first else {
throw NoRelaysSatisfyingConstraintsError()
throw NoRelaysSatisfyingConstraintsError(reason: .multihopEntryEqualsExit)
}

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(reason: .multihopOther)
}
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(reason: .multihopOther)
}
return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates)
}
16 changes: 15 additions & 1 deletion ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift
Original file line number Diff line number Diff line change
@@ -8,10 +8,24 @@

import Foundation

public enum NoRelaysSatisfyingConstraintsReason {
case filterConstraintNotMatching
case invalidPort
case multihopEntryEqualsExit
case multihopOther
case noActiveRelaysFound
case noDaitaRelaysFound
case relayConstraintNotMatching
}

public struct NoRelaysSatisfyingConstraintsError: LocalizedError {
public init() {}
public let reason: NoRelaysSatisfyingConstraintsReason

public var errorDescription: String? {
"No relays satisfying constraints."
}

public init(reason: NoRelaysSatisfyingConstraintsReason) {
self.reason = reason
}
}
36 changes: 28 additions & 8 deletions ios/MullvadREST/Relay/RelayPicking.swift
Original file line number Diff line number Diff line change
@@ -37,38 +37,58 @@ extension RelayPicking {

struct SinglehopPicker: RelayPicking {
let constraints: RelayConstraints
let daita: Bool
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,
daita: daita
)
} catch let error as NoRelaysSatisfyingConstraintsError where error.reason == .noDaitaRelaysFound {
// 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,
daita: daita,
relays: relays,
connectionAttemptCount: connectionAttemptCount
).pick()
}

let match = try findBestMatch(from: exitCandidates)
return SelectedRelays(entry: nil, exit: match, retryAttempt: connectionAttemptCount)
}
}

struct MultihopPicker: RelayPicking {
let constraints: RelayConstraints
let daita: Bool
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
filterConstraint: constraints.filter,
daita: daita
)

let exitCandidates = try RelaySelector.WireGuard.findCandidates(
by: constraints.exitLocations,
in: relays,
filterConstraint: constraints.filter
filterConstraint: constraints.filter,
daita: false
)

/*
5 changes: 3 additions & 2 deletions ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift
Original file line number Diff line number Diff line change
@@ -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,
daita: false,
relays: mappedBridges
)
)) ?? []
guard filteredRelays.isEmpty == false else { return relay(from: relaysResponse) }

// Compute the midpoint location from all the filtered relays
14 changes: 10 additions & 4 deletions ios/MullvadREST/Relay/RelaySelector+Wireguard.swift
Original file line number Diff line number Diff line change
@@ -14,13 +14,15 @@ extension RelaySelector {
public static func findCandidates(
by relayConstraint: RelayConstraint<UserSelectedRelays>,
in relays: REST.ServerRelaysResponse,
filterConstraint: RelayConstraint<RelayFilter>
filterConstraint: RelayConstraint<RelayFilter>,
daita: Bool
) throws -> [RelayWithLocation<REST.ServerRelay>] {
let mappedRelays = mapRelays(relays: relays.wireguard.relays, locations: relays.locations)

return applyConstraints(
return try applyConstraints(
relayConstraint,
filterConstraint: filterConstraint,
daita: daita,
relays: mappedRelays
)
}
@@ -38,8 +40,12 @@ extension RelaySelector {
numberOfFailedAttempts: numberOfFailedAttempts
)

guard let port, let relayWithLocation = pickRandomRelayByWeight(relays: relayWithLocations) else {
throw NoRelaysSatisfyingConstraintsError()
guard let port else {
throw NoRelaysSatisfyingConstraintsError(reason: .invalidPort)
}

guard let relayWithLocation = pickRandomRelayByWeight(relays: relayWithLocations) else {
throw NoRelaysSatisfyingConstraintsError(reason: .relayConstraintNotMatching)
}

let endpoint = MullvadEndpoint(
145 changes: 103 additions & 42 deletions ios/MullvadREST/Relay/RelaySelector.swift
Original file line number Diff line number Diff line change
@@ -135,74 +135,135 @@ public enum RelaySelector {
static func applyConstraints<T: AnyRelay>(
_ relayConstraint: RelayConstraint<UserSelectedRelays>,
filterConstraint: RelayConstraint<RelayFilter>,
daita: 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, daita: daita)
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(reason: .noActiveRelaysFound)
} else {
filteredRelays
}
}

switch filterConstraint {
private static func filterByDaita<T: AnyRelay>(
relays: [RelayWithLocation<T>],
daita: Bool
) throws -> [RelayWithLocation<T>] {
guard daita else { return relays }

let filteredRelays = relays.filter { relayWithLocation in
relayWithLocation.relay.daita == true
}

return if filteredRelays.isEmpty {
throw NoRelaysSatisfyingConstraintsError(reason: .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(reason: .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(reason: .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
}
}
}
8 changes: 8 additions & 0 deletions ios/MullvadREST/Relay/RelaySelectorWrapper.swift
Original file line number Diff line number Diff line change
@@ -13,8 +13,14 @@ public final class RelaySelectorWrapper: RelaySelectorProtocol {
let relayCache: RelayCacheProtocol
let multihopUpdater: MultihopUpdater
private var multihopState: MultihopState = .off
private var daitaState: MultihopState = .off
private var observer: MultihopObserverBlock!

// TODO: Remove, Jon
func setDaita(state: MultihopState) {
daitaState = state
}

deinit {
self.multihopUpdater.removeObserver(observer)
}
@@ -39,12 +45,14 @@ public final class RelaySelectorWrapper: RelaySelectorProtocol {
case .off:
return try SinglehopPicker(
constraints: constraints,
daita: daitaState == .on,
relays: relays,
connectionAttemptCount: connectionAttemptCount
).pick()
case .on:
return try MultihopPicker(
constraints: constraints,
daita: daitaState == .on,
relays: relays,
connectionAttemptCount: connectionAttemptCount
).pick()
Original file line number Diff line number Diff line change
@@ -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 your filter settings."
case .noRelaysSatisfyingMultihopConstraints:
errorString = "Entry and exit locations are equal. Try changing one or both to a new location."
case .noRelaysSatisfyingDaitaConstraints:
errorString = "No DAITA enabled servers match your location settings."
case .noRelaysSatisfyingConstraints:
errorString = "No servers match your settings, try changing server or other settings."
case .invalidAccount:
Original file line number Diff line number Diff line change
@@ -171,8 +171,8 @@ final class SimulatorTunnelProviderManager: NSObject, VPNTunnelProviderManagerPr
}

override func isEqual(_ object: Any?) -> Bool {
guard let other = object as? Self else { return false }
return self.identifier == other.identifier
guard let multihopOther = object as? Self else { return false }
return self.identifier == multihopOther.identifier
}
}

Original file line number Diff line number Diff line change
@@ -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
),
]
),
Original file line number Diff line number Diff line change
@@ -119,6 +119,7 @@ extension MultihopDecisionFlowTests {

return MultihopPicker(
constraints: constraints,
daita: false,
relays: sampleRelays,
connectionAttemptCount: 0
)
10 changes: 9 additions & 1 deletion ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ class RelayPickingTests: XCTestCase {

let picker = SinglehopPicker(
constraints: constraints,
daita: false,
relays: sampleRelays,
connectionAttemptCount: 0
)
@@ -41,6 +42,7 @@ class RelayPickingTests: XCTestCase {

let picker = MultihopPicker(
constraints: constraints,
daita: false,
relays: sampleRelays,
connectionAttemptCount: 0
)
@@ -59,10 +61,16 @@ class RelayPickingTests: XCTestCase {

let picker = MultihopPicker(
constraints: constraints,
daita: false,
relays: sampleRelays,
connectionAttemptCount: 0
)

XCTAssertThrowsError(try picker.pick())
XCTAssertThrowsError(
try picker.pick()
) { error in
let error = error as? NoRelaysSatisfyingConstraintsError
XCTAssertEqual(error?.reason, .multihopEntryEqualsExit)
}
}
}
98 changes: 91 additions & 7 deletions ios/MullvadVPNTests/MullvadREST/Relay/RelaySelectorTests.swift
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@
@testable import MullvadREST
import MullvadTypes
import Network
@testable import WireGuardKitTypes
import XCTest

private let portRanges: [[UInt16]] = [[4000, 4001], [5000, 5001]]
@@ -17,8 +18,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 +70,10 @@ class RelaySelectorTests: XCTestCase {
)
}

let constrainedLocations = RelaySelector.applyConstraints(
let constrainedLocations = try RelaySelector.applyConstraints(
constraints.exitLocations,
filterConstraint: constraints.filter,
daita: false,
relays: relayWithLocations
)

@@ -90,6 +90,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 +185,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,20 +213,52 @@ 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, daita: true))
XCTAssertThrowsError(
try pickRelay(by: noDaitaConstraints, in: sampleRelays, failedAttemptCount: 0, daita: 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)
}
}
}

extension RelaySelectorTests {
private func pickRelay(
by constraints: RelayConstraints,
in relays: REST.ServerRelaysResponse,
failedAttemptCount: UInt
failedAttemptCount: UInt,
daita: Bool = false
) throws -> RelaySelectorMatch {
let candidates = try RelaySelector.WireGuard.findCandidates(
by: constraints.exitLocations,
in: relays,
filterConstraint: constraints.filter
filterConstraint: constraints.filter,
daita: daita
)

return try RelaySelector.WireGuard.pickCandidate(
@@ -221,3 +269,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: [])
)
}
}
Original file line number Diff line number Diff line change
@@ -52,4 +52,74 @@ class RelaySelectorWrapperTests: XCTestCase {
let selectedRelays = try wrapper.selectRelays(with: RelayConstraints(), connectionAttemptCount: 0)
XCTAssertNotNil(selectedRelays.entry)
}

func testCanSelectRelayWithMultihopOnAndDaitaOn() throws {
let wrapper = RelaySelectorWrapper(
relayCache: relayCache,
multihopUpdater: multihopUpdater
)

multihopStateListener.onNewMultihop?(.on)
wrapper.setDaita(state: .on)

let constraints = RelayConstraints(
entryLocations: .only(UserSelectedRelays(locations: [.country("es")])), // Relay with DAITA.
exitLocations: .only(UserSelectedRelays(locations: [.country("us")]))
)

XCTAssertNoThrow(try wrapper.selectRelays(with: constraints, connectionAttemptCount: 0))
}

func testCannotSelectRelayWithMultihopOnAndDaitaOn() throws {
let wrapper = RelaySelectorWrapper(
relayCache: relayCache,
multihopUpdater: multihopUpdater
)

multihopStateListener.onNewMultihop?(.on)
wrapper.setDaita(state: .on)

let constraints = RelayConstraints(
entryLocations: .only(UserSelectedRelays(locations: [.country("se")])), // Relay without DAITA.
exitLocations: .only(UserSelectedRelays(locations: [.country("us")]))
)

XCTAssertThrowsError(try wrapper.selectRelays(with: constraints, connectionAttemptCount: 0))
}

func testCanSelectRelayWithMultihopOffAndDaitaOn() throws {
let wrapper = RelaySelectorWrapper(
relayCache: relayCache,
multihopUpdater: multihopUpdater
)

multihopStateListener.onNewMultihop?(.off)
wrapper.setDaita(state: .on)

let constraints = RelayConstraints(
exitLocations: .only(UserSelectedRelays(locations: [.country("es")])) // Relay with DAITA.
)

let selectedRelays = try wrapper.selectRelays(with: constraints, 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,
multihopUpdater: multihopUpdater
)

multihopStateListener.onNewMultihop?(.off)
wrapper.setDaita(state: .on)

let constraints = RelayConstraints(
exitLocations: .only(UserSelectedRelays(locations: [.country("se")])) // Relay without DAITA.
)

let selectedRelays = try wrapper.selectRelays(with: constraints, connectionAttemptCount: 0)
XCTAssertNotNil(selectedRelays.entry)
}
}
Original file line number Diff line number Diff line change
@@ -83,7 +83,8 @@ extension IPOverrideWrapperTests {
ipv4AddrIn: .any,
ipv6AddrIn: .any,
publicKey: Data(),
includeInCountry: true
includeInCountry: true,
daita: false
)
}

Original file line number Diff line number Diff line change
@@ -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 .multihopEntryEqualsExit:
.noRelaysSatisfyingMultihopConstraints
case .noDaitaRelaysFound:
.noRelaysSatisfyingDaitaConstraints
default:
.noRelaysSatisfyingConstraints
}

case is WireGuardAdapterError:
// Any errors that originate from wireguard adapter including failure to set tunnel settings using
4 changes: 3 additions & 1 deletion ios/PacketTunnelCore/Actor/State+Extensions.swift
Original file line number Diff line number Diff line change
@@ -194,7 +194,9 @@ extension BlockedStateReason {
case .deviceLocked:
return true

case .noRelaysSatisfyingConstraints, .readSettings, .invalidAccount, .accountExpired, .deviceRevoked,
case .noRelaysSatisfyingConstraints, .noRelaysSatisfyingFilterConstraints,
.noRelaysSatisfyingMultihopConstraints,
.noRelaysSatisfyingDaitaConstraints, .readSettings, .invalidAccount, .accountExpired, .deviceRevoked,
.tunnelAdapter, .unknown, .deviceLoggedOut, .outdatedSchema, .invalidRelayPublicKey:
return false
}
11 changes: 10 additions & 1 deletion ios/PacketTunnelCore/Actor/State.swift
Original file line number Diff line number Diff line change
@@ -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 noRelaysSatisfyingMultihopConstraints

/// No relays satisfying DAITA constraints.
case noRelaysSatisfyingDaitaConstraints

/// Any other failure when reading settings.
case readSettings

3 changes: 2 additions & 1 deletion ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift
Original file line number Diff line number Diff line change
@@ -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,
daita: false
)

let match = try RelaySelector.WireGuard.pickCandidate(
Original file line number Diff line number Diff line change
@@ -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,
daita: 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,
daita: false
),
relays: ServerRelaysResponseStubs.sampleRelays,
portConstraint: relayConstraints.port,
Original file line number Diff line number Diff line change
@@ -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,
daita: 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,
daita: false
),
relays: ServerRelaysResponseStubs.sampleRelays,
portConstraint: relayConstraints.port,
Original file line number Diff line number Diff line change
@@ -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,
daita: false
)

let match = try RelaySelector.WireGuard.pickCandidate(

0 comments on commit c5fe10c

Please sign in to comment.