Skip to content

Commit

Permalink
Smart routing should select the closest relay with Daita
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Petersson committed Sep 27, 2024
1 parent faa8ffd commit 470857b
Show file tree
Hide file tree
Showing 13 changed files with 524 additions and 106 deletions.
119 changes: 99 additions & 20 deletions ios/MullvadREST/Relay/MultihopDecisionFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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],
smartRouting: Bool
) throws -> SelectedRelays
}

struct OneToOne: MultihopDecisionFlow {
Expand All @@ -23,20 +27,32 @@ struct OneToOne: MultihopDecisionFlow {
self.relayPicker = relayPicker
}

func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays {
func pick(
entryCandidates: [RelayCandidate],
exitCandidates: [RelayCandidate],
smartRouting: 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,
smartRouting: smartRouting
)
}

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: smartRouting ? exitMatch.location : nil
)

return SelectedRelays(entry: entryMatch, exit: exitMatch, retryAttempt: relayPicker.connectionAttemptCount)
}

Expand All @@ -54,7 +70,11 @@ struct OneToMany: MultihopDecisionFlow {
self.relayPicker = relayPicker
}

func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays {
func pick(
entryCandidates: [RelayCandidate],
exitCandidates: [RelayCandidate],
smartRouting: Bool
) throws -> SelectedRelays {
guard let multihopPicker = relayPicker as? MultihopPicker else {
fatalError("Could not cast picker to MultihopPicker")
}
Expand All @@ -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,
smartRouting: smartRouting
)
}

guard !smartRouting else {
return try ManyToOne(next: next, relayPicker: relayPicker)
.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates, smartRouting: 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],
smartRouting: 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,
smartRouting: smartRouting
)
}

let exitMatch = try multihopPicker.findBestMatch(from: exitCandidates)
let entryMatch = try multihopPicker.exclude(
relay: exitMatch,
from: entryCandidates,
closeTo: smartRouting ? 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
}
}

Expand All @@ -93,7 +159,11 @@ struct ManyToMany: MultihopDecisionFlow {
self.relayPicker = relayPicker
}

func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays {
func pick(
entryCandidates: [RelayCandidate],
exitCandidates: [RelayCandidate],
smartRouting: Bool
) throws -> SelectedRelays {
guard let multihopPicker = relayPicker as? MultihopPicker else {
fatalError("Could not cast picker to MultihopPicker")
}
Expand All @@ -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,
smartRouting: smartRouting
)
}

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: smartRouting ? exitMatch.location : nil
)

return SelectedRelays(entry: entryMatch, exit: exitMatch, retryAttempt: relayPicker.connectionAttemptCount)
}

Expand Down
116 changes: 76 additions & 40 deletions ios/MullvadREST/Relay/RelayPicking.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 and smart routing are 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
if daitaSettings.shouldDoSmartRouting {
var constraints = constraints
constraints.entryLocations = .any

return try MultihopPicker(
relays: relays,
constraints: constraints,
connectionAttemptCount: connectionAttemptCount,
daitaSettings: daitaSettings,
smartRouting: 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 smartRouting: 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,
Expand All @@ -96,33 +94,71 @@ 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
),
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,
smartRouting: smartRouting
)
} catch let error as NoRelaysSatisfyingConstraintsError where error.reason == .noDaitaRelaysFound {
// If DAITA and smart routing are 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.
if daitaSettings.shouldDoSmartRouting {
let entryCandidates = try RelaySelector.WireGuard.findCandidates(
by: .any,
in: relays,
filterConstraint: constraints.filter,
daitaEnabled: true
)

return try decisionFlow.pick(
entryCandidates: entryCandidates,
exitCandidates: exitCandidates,
smartRouting: 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)
}
}
Loading

0 comments on commit 470857b

Please sign in to comment.