1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
//
// MultihopPicker.swift
// MullvadVPN
//
// Created by Jon Petersson on 2024-12-11.
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//
import MullvadSettings
import MullvadTypes
struct MultihopPicker: RelayPicking {
let obfuscation: RelayObfuscation
let tunnelSettings: LatestTunnelSettings
let connectionAttemptCount: UInt
func pick() throws -> SelectedRelays {
let constraints = tunnelSettings.relayConstraints
let daitaSettings = tunnelSettings.daita
// Guarantee that the entry relay supports selected obfuscation
let obfuscationBypass = UnsupportedObfuscationProvider(
relayConstraint: constraints.entryLocations,
relays: obfuscation.obfuscatedRelays,
filterConstraint: constraints.filter,
daitaEnabled: daitaSettings.daitaState.isEnabled
)
let supportedObfuscation = RelayObfuscator(
relays: obfuscation.allRelays,
tunnelSettings: tunnelSettings,
connectionAttemptCount: connectionAttemptCount,
obfuscationBypass: obfuscationBypass
).obfuscate()
let entryCandidates = try RelaySelector.WireGuard.findCandidates(
by: daitaSettings.isAutomaticRouting ? .any : constraints.entryLocations,
in: supportedObfuscation.obfuscatedRelays,
filterConstraint: constraints.filter,
daitaEnabled: daitaSettings.daitaState.isEnabled
)
let exitCandidates = try RelaySelector.WireGuard.findCandidates(
by: constraints.exitLocations,
in: supportedObfuscation.allRelays,
filterConstraint: constraints.filter,
daitaEnabled: false
)
// Create a new picker so that it can use the new obfuscation object.
let picker = MultihopPicker(
obfuscation: supportedObfuscation,
tunnelSettings: tunnelSettings,
connectionAttemptCount: connectionAttemptCount
)
/*
Relay selection is prioritised in the following order:
1. Both entry and exit constraints match only a single relay. Both relays are selected.
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: ManyToOne(
next: ManyToMany(
next: nil,
relayPicker: picker
),
relayPicker: picker
),
relayPicker: picker
),
relayPicker: picker
)
return try decisionFlow.pick(
entryCandidates: entryCandidates,
exitCandidates: exitCandidates,
daitaAutomaticRouting: daitaSettings.isAutomaticRouting
)
}
func exclude(
relay: SelectedRelay,
from candidates: [RelayWithLocation<REST.ServerRelay>],
closeTo location: Location? = nil,
applyObfuscatedIps: Bool
) throws -> SelectedRelay {
let filteredCandidates = candidates.filter { relayWithLocation in
relayWithLocation.relay.hostname != relay.hostname
}
return try findBestMatch(
from: filteredCandidates,
closeTo: location,
applyObfuscatedIps: applyObfuscatedIps
)
}
}
|