summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadREST
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@mullvad.net>2025-09-01 16:32:55 +0200
committerJon Petersson <jon.petersson@mullvad.net>2025-09-05 15:10:55 +0200
commitaaa9bab2d0488e7d0bbeec0d5ed4981bbcd8eff1 (patch)
tree07807fd19b50368e075373fa97c74ee9dc67399d /ios/MullvadREST
parent5fa06507df5386a970a44ed14f71e7a289657b99 (diff)
downloadmullvadvpn-aaa9bab2d0488e7d0bbeec0d5ed4981bbcd8eff1.tar.xz
mullvadvpn-aaa9bab2d0488e7d0bbeec0d5ed4981bbcd8eff1.zip
Fix relay selector selection order and obfuscation
Diffstat (limited to 'ios/MullvadREST')
-rw-r--r--ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift4
-rw-r--r--ios/MullvadREST/Relay/Obfuscation/QuicObfuscator.swift39
-rw-r--r--ios/MullvadREST/Relay/Obfuscation/RelayObfuscator.swift65
-rw-r--r--ios/MullvadREST/Relay/Obfuscation/ShadowsocksObfuscator.swift87
-rw-r--r--ios/MullvadREST/Relay/Obfuscation/UdpOverTcpObfuscator.swift42
-rw-r--r--ios/MullvadREST/Relay/ObfuscatorPortSelector.swift186
-rw-r--r--ios/MullvadREST/Relay/RelayPicking/MultihopPicker.swift46
-rw-r--r--ios/MullvadREST/Relay/RelayPicking/RelayPicking.swift13
-rw-r--r--ios/MullvadREST/Relay/RelayPicking/SinglehopPicker.swift27
-rw-r--r--ios/MullvadREST/Relay/RelaySelector.swift2
-rw-r--r--ios/MullvadREST/Relay/RelaySelectorWrapper.swift94
11 files changed, 330 insertions, 275 deletions
diff --git a/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift b/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift
index 8d26081577..2a643f0742 100644
--- a/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift
+++ b/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift
@@ -81,6 +81,10 @@ extension REST {
public let shadowsocksExtraAddrIn: [String]?
public let features: Features?
+ public var supportsQuic: Bool {
+ !(features?.quic?.addrIn.isEmpty ?? true)
+ }
+
public func override(ipv4AddrIn: IPv4Address?, ipv6AddrIn: IPv6Address?) -> Self {
ServerRelay(
hostname: hostname,
diff --git a/ios/MullvadREST/Relay/Obfuscation/QuicObfuscator.swift b/ios/MullvadREST/Relay/Obfuscation/QuicObfuscator.swift
new file mode 100644
index 0000000000..f75dafde2d
--- /dev/null
+++ b/ios/MullvadREST/Relay/Obfuscation/QuicObfuscator.swift
@@ -0,0 +1,39 @@
+//
+// QuicObfuscator.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2025-09-04.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadSettings
+import MullvadTypes
+
+struct QuicObfuscator: RelayObfuscating {
+ let relays: REST.ServerRelaysResponse
+ let tunnelSettings: LatestTunnelSettings
+ let connectionAttemptCount: UInt
+
+ func obfuscate() -> RelayObfuscation {
+ RelayObfuscation(
+ allRelays: relays,
+ obfuscatedRelays: filterQuicRelays(from: relays),
+ port: .only(443),
+ method: .quic
+ )
+ }
+
+ private func filterQuicRelays(from relays: REST.ServerRelaysResponse) -> REST.ServerRelaysResponse {
+ REST.ServerRelaysResponse(
+ locations: relays.locations,
+ wireguard: REST.ServerWireguardTunnels(
+ ipv4Gateway: relays.wireguard.ipv4Gateway,
+ ipv6Gateway: relays.wireguard.ipv6Gateway,
+ portRanges: relays.wireguard.portRanges,
+ relays: relays.wireguard.relays.filter { $0.supportsQuic },
+ shadowsocksPortRanges: relays.wireguard.shadowsocksPortRanges
+ ),
+ bridge: relays.bridge
+ )
+ }
+}
diff --git a/ios/MullvadREST/Relay/Obfuscation/RelayObfuscator.swift b/ios/MullvadREST/Relay/Obfuscation/RelayObfuscator.swift
new file mode 100644
index 0000000000..41c2d418dd
--- /dev/null
+++ b/ios/MullvadREST/Relay/Obfuscation/RelayObfuscator.swift
@@ -0,0 +1,65 @@
+//
+// ObfuscatorPortSelector.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2024-11-01.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadSettings
+import MullvadTypes
+
+protocol RelayObfuscating {
+ var relays: REST.ServerRelaysResponse { get }
+ var tunnelSettings: LatestTunnelSettings { get }
+ var connectionAttemptCount: UInt { get }
+ func obfuscate() throws -> RelayObfuscation
+}
+
+struct RelayObfuscation {
+ let allRelays: REST.ServerRelaysResponse
+ let obfuscatedRelays: REST.ServerRelaysResponse
+ let port: RelayConstraint<UInt16>
+ var method: WireGuardObfuscationState
+}
+
+struct RelayObfuscator: RelayObfuscating {
+ let relays: REST.ServerRelaysResponse
+ let tunnelSettings: LatestTunnelSettings
+ let connectionAttemptCount: UInt
+
+ func obfuscate() throws -> RelayObfuscation {
+ let obfuscationMethod = ObfuscationMethodSelector.obfuscationMethodBy(
+ connectionAttemptCount: connectionAttemptCount,
+ tunnelSettings: tunnelSettings
+ )
+
+ return switch obfuscationMethod {
+ case .udpOverTcp:
+ UdpOverTcpObfuscator(
+ relays: relays,
+ tunnelSettings: tunnelSettings,
+ connectionAttemptCount: connectionAttemptCount
+ ).obfuscate()
+ case .shadowsocks:
+ ShadowsocksObfuscator(
+ relays: relays,
+ tunnelSettings: tunnelSettings,
+ connectionAttemptCount: connectionAttemptCount
+ ).obfuscate()
+ case .quic:
+ QuicObfuscator(
+ relays: relays,
+ tunnelSettings: tunnelSettings,
+ connectionAttemptCount: connectionAttemptCount
+ ).obfuscate()
+ default:
+ RelayObfuscation(
+ allRelays: relays,
+ obfuscatedRelays: relays,
+ port: tunnelSettings.relayConstraints.port,
+ method: obfuscationMethod
+ )
+ }
+ }
+}
diff --git a/ios/MullvadREST/Relay/Obfuscation/ShadowsocksObfuscator.swift b/ios/MullvadREST/Relay/Obfuscation/ShadowsocksObfuscator.swift
new file mode 100644
index 0000000000..f196305075
--- /dev/null
+++ b/ios/MullvadREST/Relay/Obfuscation/ShadowsocksObfuscator.swift
@@ -0,0 +1,87 @@
+//
+// ShadowsocksObfuscator.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2025-09-04.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadSettings
+import MullvadTypes
+
+struct ShadowsocksObfuscator: RelayObfuscating {
+ let relays: REST.ServerRelaysResponse
+ let tunnelSettings: LatestTunnelSettings
+ let connectionAttemptCount: UInt
+
+ func obfuscate() -> RelayObfuscation {
+ RelayObfuscation(
+ allRelays: relays,
+ obfuscatedRelays: filterShadowsocksRelays(
+ from: relays,
+ for: tunnelSettings.wireGuardObfuscation.shadowsocksPort
+ ),
+ port: obfuscateShadowsocksPort(
+ tunnelSettings: tunnelSettings,
+ shadowsocksPortRanges: relays.wireguard.shadowsocksPortRanges
+ ),
+ method: .shadowsocks
+ )
+ }
+
+ private func filterShadowsocksRelays(
+ from relays: REST.ServerRelaysResponse,
+ for port: WireGuardObfuscationShadowsocksPort
+ ) -> REST.ServerRelaysResponse {
+ let portRanges = RelaySelector.parseRawPortRanges(relays.wireguard.shadowsocksPortRanges)
+
+ // If the selected port is within the shadowsocks port ranges we can select from all relays.
+ guard
+ case let .custom(port) = port,
+ !portRanges.contains(where: { $0.contains(port) })
+ else {
+ return relays
+ }
+
+ let filteredRelays = relays.wireguard.relays.filter { relay in
+ relay.shadowsocksExtraAddrIn != nil
+ }
+
+ return REST.ServerRelaysResponse(
+ locations: relays.locations,
+ wireguard: REST.ServerWireguardTunnels(
+ ipv4Gateway: relays.wireguard.ipv4Gateway,
+ ipv6Gateway: relays.wireguard.ipv6Gateway,
+ portRanges: relays.wireguard.portRanges,
+ relays: filteredRelays,
+ shadowsocksPortRanges: relays.wireguard.shadowsocksPortRanges
+ ),
+ bridge: relays.bridge
+ )
+ }
+
+ private func obfuscateShadowsocksPort(
+ tunnelSettings: LatestTunnelSettings,
+ shadowsocksPortRanges: [[UInt16]]
+ ) -> RelayConstraint<UInt16> {
+ let wireGuardObfuscation = tunnelSettings.wireGuardObfuscation
+
+ let shadowsockPort: () -> UInt16? = {
+ switch wireGuardObfuscation.shadowsocksPort {
+ case let .custom(port):
+ port
+ default:
+ RelaySelector.pickRandomPort(rawPortRanges: shadowsocksPortRanges)
+ }
+ }
+
+ guard
+ wireGuardObfuscation.state == .shadowsocks,
+ let port = shadowsockPort()
+ else {
+ return tunnelSettings.relayConstraints.port
+ }
+
+ return .only(port)
+ }
+}
diff --git a/ios/MullvadREST/Relay/Obfuscation/UdpOverTcpObfuscator.swift b/ios/MullvadREST/Relay/Obfuscation/UdpOverTcpObfuscator.swift
new file mode 100644
index 0000000000..692f801db6
--- /dev/null
+++ b/ios/MullvadREST/Relay/Obfuscation/UdpOverTcpObfuscator.swift
@@ -0,0 +1,42 @@
+//
+// UdpOverTcpObfuscator.swift
+// MullvadVPN
+//
+// Created by Jon Petersson on 2025-09-04.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadSettings
+import MullvadTypes
+
+struct UdpOverTcpObfuscator: RelayObfuscating {
+ let relays: REST.ServerRelaysResponse
+ let tunnelSettings: LatestTunnelSettings
+ let connectionAttemptCount: UInt
+
+ func obfuscate() -> RelayObfuscation {
+ RelayObfuscation(
+ allRelays: relays,
+ obfuscatedRelays: relays,
+ port: obfuscateUdpOverTcpPort(
+ tunnelSettings: tunnelSettings,
+ connectionAttemptCount: connectionAttemptCount
+ ),
+ method: .udpOverTcp
+ )
+ }
+
+ private func obfuscateUdpOverTcpPort(
+ tunnelSettings: LatestTunnelSettings,
+ connectionAttemptCount: UInt
+ ) -> RelayConstraint<UInt16> {
+ switch tunnelSettings.wireGuardObfuscation.udpOverTcpPort {
+ case .automatic:
+ return [.only(80), .only(5001)].randomElement()!
+ case .port5001:
+ return .only(5001)
+ case .port80:
+ return .only(80)
+ }
+ }
+}
diff --git a/ios/MullvadREST/Relay/ObfuscatorPortSelector.swift b/ios/MullvadREST/Relay/ObfuscatorPortSelector.swift
deleted file mode 100644
index 2bd2f4ec8b..0000000000
--- a/ios/MullvadREST/Relay/ObfuscatorPortSelector.swift
+++ /dev/null
@@ -1,186 +0,0 @@
-//
-// ObfuscatorPortSelector.swift
-// MullvadVPN
-//
-// Created by Jon Petersson on 2024-11-01.
-// Copyright © 2025 Mullvad VPN AB. All rights reserved.
-//
-
-import MullvadSettings
-import MullvadTypes
-
-struct ObfuscatorPortSelection {
- let entryRelays: REST.ServerRelaysResponse
- let exitRelays: REST.ServerRelaysResponse
- let unfilteredRelays: REST.ServerRelaysResponse
- let port: RelayConstraint<UInt16>
- let method: WireGuardObfuscationState
-
- var wireguard: REST.ServerWireguardTunnels {
- exitRelays.wireguard
- }
-}
-
-struct ObfuscatorPortSelector {
- let relays: REST.ServerRelaysResponse
-
- func obfuscate(
- tunnelSettings: LatestTunnelSettings,
- connectionAttemptCount: UInt
- ) throws -> ObfuscatorPortSelection {
- var entryRelays = relays
- var exitRelays = relays
-
- var port = tunnelSettings.relayConstraints.port
- let obfuscationMethod = ObfuscationMethodSelector.obfuscationMethodBy(
- connectionAttemptCount: connectionAttemptCount,
- tunnelSettings: tunnelSettings
- )
-
- switch obfuscationMethod {
- case .udpOverTcp:
- port = obfuscateUdpOverTcpPort(
- tunnelSettings: tunnelSettings,
- connectionAttemptCount: connectionAttemptCount
- )
- case .shadowsocks:
- let filteredRelays = obfuscateShadowsocksRelays(tunnelSettings: tunnelSettings)
- if tunnelSettings.tunnelMultihopState.isEnabled {
- entryRelays = filteredRelays
- } else {
- exitRelays = filteredRelays
- }
-
- port = obfuscateShadowsocksPort(
- tunnelSettings: tunnelSettings,
- shadowsocksPortRanges: relays.wireguard.shadowsocksPortRanges
- )
- case .quic:
- let filteredRelays = obfuscateQUICRelays(tunnelSettings: tunnelSettings)
- if tunnelSettings.tunnelMultihopState.isEnabled {
- entryRelays = filteredRelays
- } else {
- exitRelays = filteredRelays
- }
-
- port = .only(443)
- default:
- break
- }
-
- return ObfuscatorPortSelection(
- entryRelays: entryRelays,
- exitRelays: exitRelays,
- unfilteredRelays: relays,
- port: port,
- method: obfuscationMethod
- )
- }
-
- private func obfuscateShadowsocksRelays(tunnelSettings: LatestTunnelSettings) -> REST.ServerRelaysResponse {
- let relays = relays
- let wireGuardObfuscation = tunnelSettings.wireGuardObfuscation
-
- return wireGuardObfuscation.state == .shadowsocks
- ? filterShadowsocksRelays(from: relays, for: wireGuardObfuscation.shadowsocksPort)
- : relays
- }
-
- private func filterShadowsocksRelays(
- from relays: REST.ServerRelaysResponse,
- for port: WireGuardObfuscationShadowsocksPort
- ) -> REST.ServerRelaysResponse {
- let portRanges = RelaySelector.parseRawPortRanges(relays.wireguard.shadowsocksPortRanges)
-
- // If the selected port is within the shadowsocks port ranges we can select from all relays.
- guard
- case let .custom(port) = port,
- !portRanges.contains(where: { $0.contains(port) })
- else {
- return relays
- }
-
- let filteredRelays = relays.wireguard.relays.filter { relay in
- relay.shadowsocksExtraAddrIn != nil
- }
-
- return REST.ServerRelaysResponse(
- locations: relays.locations,
- wireguard: REST.ServerWireguardTunnels(
- ipv4Gateway: relays.wireguard.ipv4Gateway,
- ipv6Gateway: relays.wireguard.ipv6Gateway,
- portRanges: relays.wireguard.portRanges,
- relays: filteredRelays,
- shadowsocksPortRanges: relays.wireguard.shadowsocksPortRanges
- ),
- bridge: relays.bridge
- )
- }
-
- private func obfuscateQUICRelays(tunnelSettings: LatestTunnelSettings) -> REST.ServerRelaysResponse {
- let relays = relays
- let wireGuardObfuscation = tunnelSettings.wireGuardObfuscation
-
- return wireGuardObfuscation.state == .quic
- ? filterQUICRelays(from: relays)
- : relays
- }
-
- private func filterQUICRelays(from relays: REST.ServerRelaysResponse) -> REST.ServerRelaysResponse {
- let filteredRelays = relays.wireguard.relays.filter { relay in
- let addressListIsEmpty = relay.features?.quic?.addrIn.isEmpty ?? true
- return !addressListIsEmpty
- }
-
- return REST.ServerRelaysResponse(
- locations: relays.locations,
- wireguard: REST.ServerWireguardTunnels(
- ipv4Gateway: relays.wireguard.ipv4Gateway,
- ipv6Gateway: relays.wireguard.ipv6Gateway,
- portRanges: relays.wireguard.portRanges,
- relays: filteredRelays,
- shadowsocksPortRanges: relays.wireguard.shadowsocksPortRanges
- ),
- bridge: relays.bridge
- )
- }
-
- private func obfuscateUdpOverTcpPort(
- tunnelSettings: LatestTunnelSettings,
- connectionAttemptCount: UInt
- ) -> RelayConstraint<UInt16> {
- switch tunnelSettings.wireGuardObfuscation.udpOverTcpPort {
- case .automatic:
- return [.only(80), .only(5001)].randomElement()!
- case .port5001:
- return .only(5001)
- case .port80:
- return .only(80)
- }
- }
-
- private func obfuscateShadowsocksPort(
- tunnelSettings: LatestTunnelSettings,
- shadowsocksPortRanges: [[UInt16]]
- ) -> RelayConstraint<UInt16> {
- let wireGuardObfuscation = tunnelSettings.wireGuardObfuscation
-
- let shadowsockPort: () -> UInt16? = {
- switch wireGuardObfuscation.shadowsocksPort {
- case let .custom(port):
- port
- default:
- RelaySelector.pickRandomPort(rawPortRanges: shadowsocksPortRanges)
- }
- }
-
- guard
- wireGuardObfuscation.state == .shadowsocks,
- let port = shadowsockPort()
- else {
- return tunnelSettings.relayConstraints.port
- }
-
- return .only(port)
- }
-}
diff --git a/ios/MullvadREST/Relay/RelayPicking/MultihopPicker.swift b/ios/MullvadREST/Relay/RelayPicking/MultihopPicker.swift
index ba853e10ba..0dad74797f 100644
--- a/ios/MullvadREST/Relay/RelayPicking/MultihopPicker.swift
+++ b/ios/MullvadREST/Relay/RelayPicking/MultihopPicker.swift
@@ -10,19 +10,11 @@ import MullvadSettings
import MullvadTypes
struct MultihopPicker: RelayPicking {
- let obfuscation: ObfuscatorPortSelection
- let constraints: RelayConstraints
+ let obfuscation: RelayObfuscation
+ let tunnelSettings: LatestTunnelSettings
let connectionAttemptCount: UInt
- let daitaSettings: DAITASettings
func pick() throws -> SelectedRelays {
- let exitCandidates = try RelaySelector.WireGuard.findCandidates(
- by: constraints.exitLocations,
- in: obfuscation.exitRelays,
- filterConstraint: constraints.filter,
- daitaEnabled: false
- )
-
/*
Relay selection is prioritised in the following order:
1. Both entry and exit constraints match only a single relay. Both relays are selected.
@@ -47,20 +39,28 @@ struct MultihopPicker: RelayPicking {
relayPicker: self
)
- do {
- let entryCandidates = try RelaySelector.WireGuard.findCandidates(
- by: daitaSettings.isAutomaticRouting ? .any : constraints.entryLocations,
- in: obfuscation.entryRelays,
- filterConstraint: constraints.filter,
- daitaEnabled: daitaSettings.daitaState.isEnabled
- )
+ let constraints = tunnelSettings.relayConstraints
+ let daitaSettings = tunnelSettings.daita
- return try decisionFlow.pick(
- entryCandidates: entryCandidates,
- exitCandidates: exitCandidates,
- daitaAutomaticRouting: daitaSettings.isAutomaticRouting
- )
- }
+ let entryCandidates = try RelaySelector.WireGuard.findCandidates(
+ by: daitaSettings.isAutomaticRouting ? .any : constraints.entryLocations,
+ in: obfuscation.obfuscatedRelays,
+ filterConstraint: constraints.filter,
+ daitaEnabled: daitaSettings.daitaState.isEnabled
+ )
+
+ let exitCandidates = try RelaySelector.WireGuard.findCandidates(
+ by: constraints.exitLocations,
+ in: obfuscation.allRelays,
+ filterConstraint: constraints.filter,
+ daitaEnabled: false
+ )
+
+ return try decisionFlow.pick(
+ entryCandidates: entryCandidates,
+ exitCandidates: exitCandidates,
+ daitaAutomaticRouting: daitaSettings.isAutomaticRouting
+ )
}
func exclude(
diff --git a/ios/MullvadREST/Relay/RelayPicking/RelayPicking.swift b/ios/MullvadREST/Relay/RelayPicking/RelayPicking.swift
index 18421de3f3..57750571a2 100644
--- a/ios/MullvadREST/Relay/RelayPicking/RelayPicking.swift
+++ b/ios/MullvadREST/Relay/RelayPicking/RelayPicking.swift
@@ -11,10 +11,9 @@ import MullvadTypes
import Network
protocol RelayPicking {
- var obfuscation: ObfuscatorPortSelection { get }
- var constraints: RelayConstraints { get }
+ var obfuscation: RelayObfuscation { get }
+ var tunnelSettings: LatestTunnelSettings { get }
var connectionAttemptCount: UInt { get }
- var daitaSettings: DAITASettings { get }
func pick() throws -> SelectedRelays
}
@@ -26,8 +25,10 @@ extension RelayPicking {
) throws -> SelectedRelay {
var match = try RelaySelector.WireGuard.pickCandidate(
from: candidates,
- wireguard: obfuscation.wireguard,
- portConstraint: useObfuscatedPortIfAvailable ? obfuscation.port : constraints.port,
+ wireguard: obfuscation.allRelays.wireguard,
+ portConstraint: useObfuscatedPortIfAvailable
+ ? obfuscation.port
+ : tunnelSettings.relayConstraints.port,
numberOfFailedAttempts: connectionAttemptCount,
closeTo: location
)
@@ -46,7 +47,7 @@ extension RelayPicking {
private func applyShadowsocksIpAddress(in match: RelaySelectorMatch) -> RelaySelectorMatch {
let port = match.endpoint.ipv4Relay.port
- let portRanges = RelaySelector.parseRawPortRanges(obfuscation.wireguard.shadowsocksPortRanges)
+ let portRanges = RelaySelector.parseRawPortRanges(obfuscation.allRelays.wireguard.shadowsocksPortRanges)
let portIsWithinRange = portRanges.contains(where: { $0.contains(port) })
var endpoint = match.endpoint
diff --git a/ios/MullvadREST/Relay/RelayPicking/SinglehopPicker.swift b/ios/MullvadREST/Relay/RelayPicking/SinglehopPicker.swift
index cf51578031..4ce8518cb0 100644
--- a/ios/MullvadREST/Relay/RelayPicking/SinglehopPicker.swift
+++ b/ios/MullvadREST/Relay/RelayPicking/SinglehopPicker.swift
@@ -10,30 +10,21 @@ import MullvadSettings
import MullvadTypes
struct SinglehopPicker: RelayPicking {
- let obfuscation: ObfuscatorPortSelection
- let constraints: RelayConstraints
+ let obfuscation: RelayObfuscation
+ let tunnelSettings: LatestTunnelSettings
let connectionAttemptCount: UInt
- let daitaSettings: DAITASettings
func pick() throws -> SelectedRelays {
do {
- return try pick(from: obfuscation.exitRelays)
+ return try pick(from: obfuscation.obfuscatedRelays)
} catch let error as NoRelaysSatisfyingConstraintsError where error.reason == .noDaitaRelaysFound {
- // If DAITA is on, Direct only is off and obfuscation is on, and no supported relays are found, we should see if
- // the obfuscated subset of exit relays is the cause of this. We can do this by checking if relay selection would
- // have been successful with all relays available. If that's the case, throw error and point to obfuscation.
- if (try? pick(from: obfuscation.unfilteredRelays)) != nil {
- throw NoRelaysSatisfyingConstraintsError(.noObfuscatedRelaysFound)
- }
-
// If DAITA is on, Direct only is off and obfuscation has been ruled out, 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.isAutomaticRouting {
+ if tunnelSettings.daita.isAutomaticRouting {
return try MultihopPicker(
obfuscation: obfuscation,
- constraints: constraints,
- connectionAttemptCount: connectionAttemptCount,
- daitaSettings: daitaSettings
+ tunnelSettings: tunnelSettings,
+ connectionAttemptCount: connectionAttemptCount
).pick()
} else {
throw error
@@ -43,10 +34,10 @@ struct SinglehopPicker: RelayPicking {
private func pick(from exitRelays: REST.ServerRelaysResponse) throws -> SelectedRelays {
let exitCandidates = try RelaySelector.WireGuard.findCandidates(
- by: constraints.exitLocations,
+ by: tunnelSettings.relayConstraints.exitLocations,
in: exitRelays,
- filterConstraint: constraints.filter,
- daitaEnabled: daitaSettings.daitaState.isEnabled
+ filterConstraint: tunnelSettings.relayConstraints.filter,
+ daitaEnabled: tunnelSettings.daita.daitaState.isEnabled
)
let match = try findBestMatch(from: exitCandidates, useObfuscatedPortIfAvailable: true)
diff --git a/ios/MullvadREST/Relay/RelaySelector.swift b/ios/MullvadREST/Relay/RelaySelector.swift
index 8393f997e6..811692aecb 100644
--- a/ios/MullvadREST/Relay/RelaySelector.swift
+++ b/ios/MullvadREST/Relay/RelaySelector.swift
@@ -67,7 +67,7 @@ public enum RelaySelector {
daitaEnabled: Bool,
relays: [RelayWithLocation<T>]
) throws -> [RelayWithLocation<T>] {
- // Filter on active status, daita support, filter constraint and relay constraint.
+ // Filter on various settings and constraints.
var filteredRelays = try filterByActive(relays: relays)
filteredRelays = try filterByFilterConstraint(relays: filteredRelays, constraint: filterConstraint)
filteredRelays = try filterByLocationConstraint(relays: filteredRelays, constraint: relayConstraint)
diff --git a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift
index 4eb65eaddb..197259ce11 100644
--- a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift
+++ b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift
@@ -21,39 +21,38 @@ public final class RelaySelectorWrapper: RelaySelectorProtocol, Sendable {
connectionAttemptCount: UInt
) throws -> SelectedRelays {
let relays = try relayCache.read().relays
- try validateWireguardPort(tunnelSettings, relays: relays)
+ try validateWireguardCustomPort(tunnelSettings, relays: relays)
- let obfuscation = try prepareObfuscation(
- for: tunnelSettings,
- connectionAttemptCount: connectionAttemptCount,
- relays: relays
- )
+ let obfuscation = try RelayObfuscator(
+ relays: relays,
+ tunnelSettings: tunnelSettings,
+ connectionAttemptCount: connectionAttemptCount
+ ).obfuscate()
return switch tunnelSettings.tunnelMultihopState {
case .off:
try SinglehopPicker(
obfuscation: obfuscation,
- constraints: tunnelSettings.relayConstraints,
- connectionAttemptCount: connectionAttemptCount,
- daitaSettings: tunnelSettings.daita
+ tunnelSettings: tunnelSettings,
+ connectionAttemptCount: connectionAttemptCount
).pick()
case .on:
try MultihopPicker(
obfuscation: obfuscation,
- constraints: tunnelSettings.relayConstraints,
- connectionAttemptCount: connectionAttemptCount,
- daitaSettings: tunnelSettings.daita
+ tunnelSettings: tunnelSettings,
+ connectionAttemptCount: connectionAttemptCount
).pick()
}
}
public func findCandidates(tunnelSettings: LatestTunnelSettings) throws -> RelayCandidates {
let relays = try relayCache.read().relays
- let obfuscation = try prepareObfuscation(
- for: tunnelSettings,
- connectionAttemptCount: 0,
- relays: relays
- )
+
+ let obfuscation = try RelayObfuscator(
+ relays: relays,
+ tunnelSettings: tunnelSettings,
+ connectionAttemptCount: 0
+ ).obfuscate()
let findCandidates: (REST.ServerRelaysResponse, Bool) throws
-> [RelayWithLocation<REST.ServerRelay>] = { relays, daitaEnabled in
@@ -65,36 +64,51 @@ public final class RelaySelectorWrapper: RelaySelectorProtocol, Sendable {
)
}
- if tunnelSettings.daita.isAutomaticRouting || tunnelSettings.tunnelMultihopState.isEnabled {
- let entryCandidates = try findCandidates(
- tunnelSettings.tunnelMultihopState.isEnabled ? obfuscation.entryRelays : obfuscation.exitRelays,
- tunnelSettings.daita.daitaState.isEnabled
+ 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(
+ 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
+ )
)
- let exitCandidates = try findCandidates(obfuscation.exitRelays, false)
- return RelayCandidates(entryRelays: entryCandidates, exitRelays: exitCandidates)
} else {
- let exitCandidates = try findCandidates(obfuscation.exitRelays, tunnelSettings.daita.daitaState.isEnabled)
- return RelayCandidates(entryRelays: nil, exitRelays: exitCandidates)
+ // Singlehop. Always apply DAITA and obfuscation.
+ RelayCandidates(
+ entryRelays: nil,
+ exitRelays: try findCandidates(
+ obfuscation.obfuscatedRelays,
+ tunnelSettings.daita.daitaState.isEnabled
+ )
+ )
}
}
- private func prepareObfuscation(
- for tunnelSettings: LatestTunnelSettings,
- connectionAttemptCount: UInt,
- relays: REST.ServerRelaysResponse
- ) throws -> ObfuscatorPortSelection {
- return try ObfuscatorPortSelector(relays: relays).obfuscate(
- tunnelSettings: tunnelSettings,
- connectionAttemptCount: connectionAttemptCount
- )
- }
-
- private func validateWireguardPort(
+ private func validateWireguardCustomPort(
_ tunnelSettings: LatestTunnelSettings,
relays: REST.ServerRelaysResponse
) throws {
- switch tunnelSettings.wireGuardObfuscation.state {
- case .automatic, .off:
+ if [.automatic, .off].contains(tunnelSettings.wireGuardObfuscation.state) {
if case let .only(port) = tunnelSettings.relayConstraints.port {
let isPortWithinValidWireGuardRanges: Bool =
relays.wireguard.portRanges
@@ -108,8 +122,6 @@ public final class RelaySelectorWrapper: RelaySelectorProtocol, Sendable {
throw NoRelaysSatisfyingConstraintsError(.invalidPort)
}
}
- case .on, .udpOverTcp, .shadowsocks, .quic:
- break
}
}
}