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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
//
// RelaySelectorWrapper.swift
// PacketTunnel
//
// Created by pronebird on 08/08/2023.
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
//
import MullvadSettings
import MullvadTypes
public final class RelaySelectorWrapper: RelaySelectorProtocol, Sendable {
public let relayCache: RelayCacheProtocol
public init(relayCache: RelayCacheProtocol) {
self.relayCache = relayCache
}
public func selectRelays(
tunnelSettings: LatestTunnelSettings,
connectionAttemptCount: UInt
) throws -> SelectedRelays {
let relays = try relayCache.read().relays
try validateWireguardCustomPort(tunnelSettings, relays: relays)
// Filter for obfuscation
let obfuscation = RelayObfuscator(
relays: relays,
tunnelSettings: tunnelSettings,
connectionAttemptCount: connectionAttemptCount,
obfuscationBypass: IdentityObfuscationProvider()
).obfuscate()
return switch tunnelSettings.tunnelMultihopState {
case .off:
try SinglehopPicker(
obfuscation: obfuscation,
tunnelSettings: tunnelSettings,
connectionAttemptCount: connectionAttemptCount
).pick()
case .on:
try MultihopPicker(
obfuscation: obfuscation,
tunnelSettings: tunnelSettings,
connectionAttemptCount: connectionAttemptCount
).pick()
}
}
public func findCandidates(tunnelSettings: LatestTunnelSettings) throws -> RelayCandidates {
let relays = try relayCache.read().relays
let obfuscation = RelayObfuscator(
relays: relays,
tunnelSettings: tunnelSettings,
connectionAttemptCount: 0,
obfuscationBypass: IdentityObfuscationProvider()
).obfuscate()
let findCandidates:
(REST.ServerRelaysResponse, Bool) throws
-> [RelayWithLocation<REST.ServerRelay>] = { relays, daitaEnabled in
try RelaySelector.WireGuard.findCandidates(
by: .any,
in: relays,
filterConstraint: tunnelSettings.relayConstraints.filter,
daitaEnabled: daitaEnabled
)
}
return if tunnelSettings.daita.isAutomaticRouting {
// When "Direct only" is not enabled the user will pick from the exit relays and
// is then multihopped to a compatible server if necessary. We need to apply the
// obfuscated relays to exit selection too so that the user doesn't pick
// anything that isn't available for the entry server IF multihop DOESN'T kick in.
RelayCandidates(
entryRelays: try findCandidates(
obfuscation.obfuscatedRelays,
tunnelSettings.daita.daitaState.isEnabled
),
exitRelays: try findCandidates(
// If multihop is explicitly enabled as well, any exit should be viable.
tunnelSettings.tunnelMultihopState.isEnabled ? obfuscation.allRelays : obfuscation.obfuscatedRelays,
false
)
)
} else if tunnelSettings.tunnelMultihopState.isEnabled {
// Any exit is viable due to multihop. DAITA and obfuscation is applied on
// the entry only.
RelayCandidates(
entryRelays: try findCandidates(
obfuscation.obfuscatedRelays,
tunnelSettings.daita.daitaState.isEnabled
),
exitRelays: try findCandidates(
obfuscation.allRelays,
false
)
)
} else {
// Singlehop. Always apply DAITA and obfuscation.
RelayCandidates(
entryRelays: nil,
exitRelays: try findCandidates(
obfuscation.obfuscatedRelays,
tunnelSettings.daita.daitaState.isEnabled
)
)
}
}
private func validateWireguardCustomPort(
_ tunnelSettings: LatestTunnelSettings,
relays: REST.ServerRelaysResponse
) throws {
if [.automatic, .off].contains(tunnelSettings.wireGuardObfuscation.state) {
if case let .only(port) = tunnelSettings.relayConstraints.port {
let isPortWithinValidWireGuardRanges: Bool =
relays.wireguard.portRanges
.contains { range in
if let minPort = range.first, let maxPort = range.last {
return (minPort...maxPort).contains(port)
}
return false
}
guard isPortWithinValidWireGuardRanges else {
throw NoRelaysSatisfyingConstraintsError(.invalidPort)
}
}
}
}
}
|