Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Petersson committed Sep 26, 2024
1 parent 6c394e7 commit 557615e
Show file tree
Hide file tree
Showing 13 changed files with 252 additions and 137 deletions.
117 changes: 99 additions & 18 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,19 +83,67 @@ 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) ||
(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 {
Expand All @@ -93,7 +161,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 +174,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
132 changes: 66 additions & 66 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 preferClosest: Bool { get }
var daitaSettings: DAITASettings { get }
func pick() throws -> SelectedRelays
}

extension RelayPicking {
func findBestMatch(from candidates: [RelayWithLocation<REST.ServerRelay>]) throws -> SelectedRelay {
func findBestMatch(
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,
preferClosest: preferClosest
closeTo: location
)

return SelectedRelay(
Expand All @@ -36,87 +39,51 @@ extension RelayPicking {
}

struct SinglehopPicker: RelayPicking {
let constraints: RelayConstraints
let daitaSettings: DAITASettings
let relays: REST.ServerRelaysResponse
let constraints: RelayConstraints
let connectionAttemptCount: UInt
let preferClosest: Bool

init(
constraints: RelayConstraints,
daitaSettings: DAITASettings,
relays: REST.ServerRelaysResponse,
connectionAttemptCount: UInt,
preferClosest: Bool = false
) {
self.constraints = constraints
self.daitaSettings = daitaSettings
self.relays = relays
self.connectionAttemptCount = connectionAttemptCount
self.preferClosest = preferClosest
}
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,
preferClosest: true
).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 preferClosest: Bool

init(
constraints: RelayConstraints,
daitaSettings: DAITASettings,
relays: REST.ServerRelaysResponse,
connectionAttemptCount: UInt,
preferClosest: Bool = false
) {
self.constraints = constraints
self.daitaSettings = daitaSettings
self.relays = relays
self.connectionAttemptCount = connectionAttemptCount
self.preferClosest = preferClosest
}
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 @@ -143,17 +110,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,
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 557615e

Please sign in to comment.