diff options
| author | Jon Petersson <jon.petersson@mullvad.net> | 2025-09-01 16:32:55 +0200 |
|---|---|---|
| committer | Jon Petersson <jon.petersson@mullvad.net> | 2025-09-05 15:10:55 +0200 |
| commit | aaa9bab2d0488e7d0bbeec0d5ed4981bbcd8eff1 (patch) | |
| tree | 07807fd19b50368e075373fa97c74ee9dc67399d /ios/MullvadREST | |
| parent | 5fa06507df5386a970a44ed14f71e7a289657b99 (diff) | |
| download | mullvadvpn-aaa9bab2d0488e7d0bbeec0d5ed4981bbcd8eff1.tar.xz mullvadvpn-aaa9bab2d0488e7d0bbeec0d5ed4981bbcd8eff1.zip | |
Fix relay selector selection order and obfuscation
Diffstat (limited to 'ios/MullvadREST')
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 } } } |
