Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix smart routing algorithm to select closest relay #6871

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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],
automaticDaitaRouting: 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],
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)
}

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],
automaticDaitaRouting: 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,
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
}
}

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],
automaticDaitaRouting: 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,
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)
}

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 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

return try MultihopPicker(
constraints: constraints,
daitaSettings: daitaSettings,
relays: relays,
connectionAttemptCount: connectionAttemptCount
).pick()
#endif
if daitaSettings.shouldDoAutomaticRouting {
var constraints = constraints
constraints.entryLocations = .any

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,
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,
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)
}
}
Loading
Loading