Skip to content

Commit 50415af

Browse files
author
Jon Petersson
committed
Allow relay selector to filter DAITA enabled relays
1 parent 91e0194 commit 50415af

24 files changed

+421
-90
lines changed

ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift

+4-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ extension REST {
3434
public let ipv4AddrIn: IPv4Address
3535
public let weight: UInt64
3636
public let includeInCountry: Bool
37+
public var daita: Bool? = nil
3738

3839
public func override(ipv4AddrIn: IPv4Address?) -> Self {
3940
return BridgeRelay(
@@ -60,6 +61,7 @@ extension REST {
6061
public let ipv6AddrIn: IPv6Address
6162
public let publicKey: Data
6263
public let includeInCountry: Bool
64+
public let daita: Bool?
6365

6466
public func override(ipv4AddrIn: IPv4Address?, ipv6AddrIn: IPv6Address?) -> Self {
6567
return ServerRelay(
@@ -72,7 +74,8 @@ extension REST {
7274
ipv4AddrIn: ipv4AddrIn ?? self.ipv4AddrIn,
7375
ipv6AddrIn: ipv6AddrIn ?? self.ipv6AddrIn,
7476
publicKey: publicKey,
75-
includeInCountry: includeInCountry
77+
includeInCountry: includeInCountry,
78+
daita: daita
7679
)
7780
}
7881
}

ios/MullvadREST/Relay/AnyRelay.swift

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public protocol AnyRelay {
1717
var weight: UInt64 { get }
1818
var active: Bool { get }
1919
var includeInCountry: Bool { get }
20+
var daita: Bool? { get }
2021

2122
func override(ipv4AddrIn: IPv4Address?, ipv6AddrIn: IPv6Address?) -> Self
2223
}

ios/MullvadREST/Relay/MultihopDecisionFlow.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ struct OneToOne: MultihopDecisionFlow {
2626
func pick(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) throws -> SelectedRelays {
2727
guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else {
2828
guard let next else {
29-
throw NoRelaysSatisfyingConstraintsError()
29+
throw NoRelaysSatisfyingConstraintsError(reason: .multihopOther)
3030
}
3131
return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates)
3232
}
3333

3434
guard entryCandidates.first != exitCandidates.first else {
35-
throw NoRelaysSatisfyingConstraintsError()
35+
throw NoRelaysSatisfyingConstraintsError(reason: .multihopEntryEqualsExit)
3636
}
3737

3838
let entryMatch = try relayPicker.findBestMatch(from: entryCandidates)
@@ -61,7 +61,7 @@ struct OneToMany: MultihopDecisionFlow {
6161

6262
guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else {
6363
guard let next else {
64-
throw NoRelaysSatisfyingConstraintsError()
64+
throw NoRelaysSatisfyingConstraintsError(reason: .multihopOther)
6565
}
6666
return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates)
6767
}
@@ -100,7 +100,7 @@ struct ManyToMany: MultihopDecisionFlow {
100100

101101
guard canHandle(entryCandidates: entryCandidates, exitCandidates: exitCandidates) else {
102102
guard let next else {
103-
throw NoRelaysSatisfyingConstraintsError()
103+
throw NoRelaysSatisfyingConstraintsError(reason: .multihopOther)
104104
}
105105
return try next.pick(entryCandidates: entryCandidates, exitCandidates: exitCandidates)
106106
}

ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift

+15-1
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,24 @@
88

99
import Foundation
1010

11+
public enum NoRelaysSatisfyingConstraintsReason {
12+
case filterConstraintNotMatching
13+
case invalidPort
14+
case multihopEntryEqualsExit
15+
case multihopOther
16+
case noActiveRelaysFound
17+
case noDaitaRelaysFound
18+
case relayConstraintNotMatching
19+
}
20+
1121
public struct NoRelaysSatisfyingConstraintsError: LocalizedError {
12-
public init() {}
22+
public let reason: NoRelaysSatisfyingConstraintsReason
1323

1424
public var errorDescription: String? {
1525
"No relays satisfying constraints."
1626
}
27+
28+
public init(reason: NoRelaysSatisfyingConstraintsReason) {
29+
self.reason = reason
30+
}
1731
}

ios/MullvadREST/Relay/RelayPicking.swift

+30-8
Original file line numberDiff line numberDiff line change
@@ -37,38 +37,60 @@ extension RelayPicking {
3737

3838
struct SinglehopPicker: RelayPicking {
3939
let constraints: RelayConstraints
40+
let daita: Bool
4041
let relays: REST.ServerRelaysResponse
4142
let connectionAttemptCount: UInt
4243

4344
func pick() throws -> SelectedRelays {
44-
let candidates = try RelaySelector.WireGuard.findCandidates(
45-
by: constraints.exitLocations,
46-
in: relays,
47-
filterConstraint: constraints.filter
48-
)
45+
var exitCandidates = [RelayWithLocation<REST.ServerRelay>]()
4946

50-
let match = try findBestMatch(from: candidates)
47+
do {
48+
exitCandidates = try RelaySelector.WireGuard.findCandidates(
49+
by: constraints.exitLocations,
50+
in: relays,
51+
filterConstraint: constraints.filter,
52+
daita: daita
53+
)
54+
} catch let error as NoRelaysSatisfyingConstraintsError where error.reason == .noDaitaRelaysFound {
55+
#if DEBUG
56+
// If DAITA is enabled and no supported relays are found, we should try to find the nearest
57+
// available relay that supports DAITA and use it as entry in a multihop selection.
58+
var constraints = constraints
59+
constraints.entryLocations = .any
60+
61+
return try MultihopPicker(
62+
constraints: constraints,
63+
daita: daita,
64+
relays: relays,
65+
connectionAttemptCount: connectionAttemptCount
66+
).pick()
67+
#endif
68+
}
5169

70+
let match = try findBestMatch(from: exitCandidates)
5271
return SelectedRelays(entry: nil, exit: match, retryAttempt: connectionAttemptCount)
5372
}
5473
}
5574

5675
struct MultihopPicker: RelayPicking {
5776
let constraints: RelayConstraints
77+
let daita: Bool
5878
let relays: REST.ServerRelaysResponse
5979
let connectionAttemptCount: UInt
6080

6181
func pick() throws -> SelectedRelays {
6282
let entryCandidates = try RelaySelector.WireGuard.findCandidates(
6383
by: constraints.entryLocations,
6484
in: relays,
65-
filterConstraint: constraints.filter
85+
filterConstraint: constraints.filter,
86+
daita: daita
6687
)
6788

6889
let exitCandidates = try RelaySelector.WireGuard.findCandidates(
6990
by: constraints.exitLocations,
7091
in: relays,
71-
filterConstraint: constraints.filter
92+
filterConstraint: constraints.filter,
93+
daita: false
7294
)
7395

7496
/*

ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,12 @@ extension RelaySelector {
4343
in relaysResponse: REST.ServerRelaysResponse
4444
) -> REST.BridgeRelay? {
4545
let mappedBridges = mapRelays(relays: relaysResponse.bridge.relays, locations: relaysResponse.locations)
46-
let filteredRelays = applyConstraints(
46+
let filteredRelays = (try? applyConstraints(
4747
location,
4848
filterConstraint: filter,
49+
daita: false,
4950
relays: mappedBridges
50-
)
51+
)) ?? []
5152
guard filteredRelays.isEmpty == false else { return relay(from: relaysResponse) }
5253

5354
// Compute the midpoint location from all the filtered relays

ios/MullvadREST/Relay/RelaySelector+Wireguard.swift

+10-4
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ extension RelaySelector {
1414
public static func findCandidates(
1515
by relayConstraint: RelayConstraint<UserSelectedRelays>,
1616
in relays: REST.ServerRelaysResponse,
17-
filterConstraint: RelayConstraint<RelayFilter>
17+
filterConstraint: RelayConstraint<RelayFilter>,
18+
daita: Bool
1819
) throws -> [RelayWithLocation<REST.ServerRelay>] {
1920
let mappedRelays = mapRelays(relays: relays.wireguard.relays, locations: relays.locations)
2021

21-
return applyConstraints(
22+
return try applyConstraints(
2223
relayConstraint,
2324
filterConstraint: filterConstraint,
25+
daita: daita,
2426
relays: mappedRelays
2527
)
2628
}
@@ -38,8 +40,12 @@ extension RelaySelector {
3840
numberOfFailedAttempts: numberOfFailedAttempts
3941
)
4042

41-
guard let port, let relayWithLocation = pickRandomRelayByWeight(relays: relayWithLocations) else {
42-
throw NoRelaysSatisfyingConstraintsError()
43+
guard let port else {
44+
throw NoRelaysSatisfyingConstraintsError(reason: .invalidPort)
45+
}
46+
47+
guard let relayWithLocation = pickRandomRelayByWeight(relays: relayWithLocations) else {
48+
throw NoRelaysSatisfyingConstraintsError(reason: .relayConstraintNotMatching)
4349
}
4450

4551
let endpoint = MullvadEndpoint(

ios/MullvadREST/Relay/RelaySelector.swift

+103-42
Original file line numberDiff line numberDiff line change
@@ -135,74 +135,135 @@ public enum RelaySelector {
135135
static func applyConstraints<T: AnyRelay>(
136136
_ relayConstraint: RelayConstraint<UserSelectedRelays>,
137137
filterConstraint: RelayConstraint<RelayFilter>,
138+
daita: Bool,
138139
relays: [RelayWithLocation<T>]
139-
) -> [RelayWithLocation<T>] {
140-
// Filter on active status, filter, and location.
141-
let filteredRelays = relays.filter { relayWithLocation -> Bool in
142-
guard relayWithLocation.relay.active else {
143-
return false
144-
}
140+
) throws -> [RelayWithLocation<T>] {
141+
// Filter on active status, daita support, filter constraint and relay constraint.
142+
var filteredRelays = try filterByActive(relays: relays)
143+
filteredRelays = try filterByFilterConstraint(relays: filteredRelays, constraint: filterConstraint)
144+
filteredRelays = try filterByLocationConstraint(relays: filteredRelays, constraint: relayConstraint)
145+
filteredRelays = try filterByDaita(relays: filteredRelays, daita: daita)
146+
return filterByCountryInclusion(relays: filteredRelays, constraint: relayConstraint)
147+
}
148+
149+
/// Produce a port that is either user provided or randomly selected, satisfying the given constraints.
150+
static func applyPortConstraint(
151+
_ portConstraint: RelayConstraint<UInt16>,
152+
rawPortRanges: [[UInt16]],
153+
numberOfFailedAttempts: UInt
154+
) -> UInt16? {
155+
switch portConstraint {
156+
case let .only(port):
157+
return port
158+
159+
case .any:
160+
// 1. First two attempts should pick a random port.
161+
// 2. The next two should pick port 53.
162+
// 3. Repeat steps 1 and 2.
163+
let useDefaultPort = (numberOfFailedAttempts % 4 == 2) || (numberOfFailedAttempts % 4 == 3)
164+
165+
return useDefaultPort ? defaultPort : pickRandomPort(rawPortRanges: rawPortRanges)
166+
}
167+
}
168+
169+
private static func filterByActive<T: AnyRelay>(
170+
relays: [RelayWithLocation<T>]
171+
) throws -> [RelayWithLocation<T>] {
172+
let filteredRelays = relays.filter { relayWithLocation in
173+
relayWithLocation.relay.active
174+
}
175+
176+
return if filteredRelays.isEmpty {
177+
throw NoRelaysSatisfyingConstraintsError(reason: .noActiveRelaysFound)
178+
} else {
179+
filteredRelays
180+
}
181+
}
145182

146-
switch filterConstraint {
183+
private static func filterByDaita<T: AnyRelay>(
184+
relays: [RelayWithLocation<T>],
185+
daita: Bool
186+
) throws -> [RelayWithLocation<T>] {
187+
guard daita else { return relays }
188+
189+
let filteredRelays = relays.filter { relayWithLocation in
190+
relayWithLocation.relay.daita == true
191+
}
192+
193+
return if filteredRelays.isEmpty {
194+
throw NoRelaysSatisfyingConstraintsError(reason: .noDaitaRelaysFound)
195+
} else {
196+
filteredRelays
197+
}
198+
}
199+
200+
private static func filterByFilterConstraint<T: AnyRelay>(
201+
relays: [RelayWithLocation<T>],
202+
constraint: RelayConstraint<RelayFilter>
203+
) throws -> [RelayWithLocation<T>] {
204+
let filteredRelays = relays.filter { relayWithLocation in
205+
switch constraint {
147206
case .any:
148-
break
207+
true
149208
case let .only(filter):
150-
if !relayMatchesFilter(relayWithLocation.relay, filter: filter) {
151-
return false
152-
}
209+
relayMatchesFilter(relayWithLocation.relay, filter: filter)
153210
}
211+
}
154212

155-
return switch relayConstraint {
213+
return if filteredRelays.isEmpty {
214+
throw NoRelaysSatisfyingConstraintsError(reason: .filterConstraintNotMatching)
215+
} else {
216+
filteredRelays
217+
}
218+
}
219+
220+
private static func filterByLocationConstraint<T: AnyRelay>(
221+
relays: [RelayWithLocation<T>],
222+
constraint: RelayConstraint<UserSelectedRelays>
223+
) throws -> [RelayWithLocation<T>] {
224+
let filteredRelays = relays.filter { relayWithLocation in
225+
switch constraint {
156226
case .any:
157227
true
158-
case let .only(relayConstraint):
228+
case let .only(constraint):
159229
// At least one location must match the relay under test.
160-
relayConstraint.locations.contains { location in
230+
constraint.locations.contains { location in
161231
relayWithLocation.matches(location: location)
162232
}
163233
}
164234
}
165235

166-
// Filter on country inclusion.
167-
let includeInCountryFilteredRelays = filteredRelays.filter { relayWithLocation in
168-
return switch relayConstraint {
236+
return if filteredRelays.isEmpty {
237+
throw NoRelaysSatisfyingConstraintsError(reason: .relayConstraintNotMatching)
238+
} else {
239+
filteredRelays
240+
}
241+
}
242+
243+
private static func filterByCountryInclusion<T: AnyRelay>(
244+
relays: [RelayWithLocation<T>],
245+
constraint: RelayConstraint<UserSelectedRelays>
246+
) -> [RelayWithLocation<T>] {
247+
let filteredRelays = relays.filter { relayWithLocation in
248+
return switch constraint {
169249
case .any:
170250
true
171251
case let .only(relayConstraint):
172252
relayConstraint.locations.contains { location in
173253
if case .country = location {
174-
return relayWithLocation.relay.includeInCountry
254+
relayWithLocation.relay.includeInCountry
255+
} else {
256+
false
175257
}
176-
return false
177258
}
178259
}
179260
}
180261

181262
// If no relays should be included in the matched country, instead accept all.
182-
if includeInCountryFilteredRelays.isEmpty {
183-
return filteredRelays
263+
return if filteredRelays.isEmpty {
264+
relays
184265
} else {
185-
return includeInCountryFilteredRelays
186-
}
187-
}
188-
189-
/// Produce a port that is either user provided or randomly selected, satisfying the given constraints.
190-
static func applyPortConstraint(
191-
_ portConstraint: RelayConstraint<UInt16>,
192-
rawPortRanges: [[UInt16]],
193-
numberOfFailedAttempts: UInt
194-
) -> UInt16? {
195-
switch portConstraint {
196-
case let .only(port):
197-
return port
198-
199-
case .any:
200-
// 1. First two attempts should pick a random port.
201-
// 2. The next two should pick port 53.
202-
// 3. Repeat steps 1 and 2.
203-
let useDefaultPort = (numberOfFailedAttempts % 4 == 2) || (numberOfFailedAttempts % 4 == 3)
204-
205-
return useDefaultPort ? defaultPort : pickRandomPort(rawPortRanges: rawPortRanges)
266+
filteredRelays
206267
}
207268
}
208269
}

0 commit comments

Comments
 (0)