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 | |
| parent | 5fa06507df5386a970a44ed14f71e7a289657b99 (diff) | |
| download | mullvadvpn-aaa9bab2d0488e7d0bbeec0d5ed4981bbcd8eff1.tar.xz mullvadvpn-aaa9bab2d0488e7d0bbeec0d5ed4981bbcd8eff1.zip | |
Fix relay selector selection order and obfuscation
Diffstat (limited to 'ios')
18 files changed, 528 insertions, 455 deletions
diff --git a/ios/Assets/Localizable.xcstrings b/ios/Assets/Localizable.xcstrings index 57053d2910..ca2823228f 100644 --- a/ios/Assets/Localizable.xcstrings +++ b/ios/Assets/Localizable.xcstrings @@ -4362,6 +4362,9 @@ } } }, + "No servers match your obfuscation settings. Try changing location or obfuscation method." : { + + }, "No servers match your settings, try changing server or other settings." : { "localizations" : { "de" : { 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 } } } diff --git a/ios/MullvadSettings/WireGuardObfuscationSettings.swift b/ios/MullvadSettings/WireGuardObfuscationSettings.swift index 9c2f29a7a4..9397564b42 100644 --- a/ios/MullvadSettings/WireGuardObfuscationSettings.swift +++ b/ios/MullvadSettings/WireGuardObfuscationSettings.swift @@ -50,15 +50,9 @@ public enum WireGuardObfuscationState: Codable, Sendable { } } - #if DEBUG public var isEnabled: Bool { [.udpOverTcp, .shadowsocks, .quic].contains(self) } - #else - public var isEnabled: Bool { - [.udpOverTcp, .shadowsocks].contains(self) - } - #endif } public enum WireGuardObfuscationUdpOverTcpPort: Codable, Equatable, CustomStringConvertible, Sendable { diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 0a5c250eac..23718aa53c 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -599,6 +599,9 @@ 7A8A19282CF603EB000BCB5B /* SettingsViewControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A19272CF603E3000BCB5B /* SettingsViewControllerFactory.swift */; }; 7A95B67B2D5F758300687524 /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 7A95B67A2D5F758300687524 /* relays.json */; }; 7A95B67D2D5F7C5B00687524 /* DAITASettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A95B67C2D5F7C5B00687524 /* DAITASettingsCoordinator.swift */; }; + 7A964BB92E699A3F00C6A4EC /* ShadowsocksObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A964BB72E6999A500C6A4EC /* ShadowsocksObfuscator.swift */; }; + 7A964BBC2E699C8500C6A4EC /* QuicObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A964BBA2E699B3000C6A4EC /* QuicObfuscator.swift */; }; + 7A964BBE2E699CBF00C6A4EC /* UdpOverTcpObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A964BBD2E699CB400C6A4EC /* UdpOverTcpObfuscator.swift */; }; 7A99D36F2D56070400891FF7 /* MullvadApiRequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A99D36E2D5606F900891FF7 /* MullvadApiRequestFactory.swift */; }; 7A99D3712D56222000891FF7 /* MullvadApiCancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A99D3702D56220E00891FF7 /* MullvadApiCancellable.swift */; }; 7A9BE5A22B8F88C500E2A7D0 /* LocationNodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9BE5A12B8F88C500E2A7D0 /* LocationNodeTests.swift */; }; @@ -664,9 +667,9 @@ 7AD0AA1D2AD6A86700119E10 /* PacketTunnelActorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD0AA192AD69B6E00119E10 /* PacketTunnelActorProtocol.swift */; }; 7AD0AA1F2AD6C8B900119E10 /* URLRequestProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD0AA1E2AD6C8B900119E10 /* URLRequestProxyProtocol.swift */; }; 7AD0AA212AD6CB0000119E10 /* URLRequestProxyStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD0AA202AD6CB0000119E10 /* URLRequestProxyStub.swift */; }; - 7AD63A392CD520FD00445268 /* ObfuscatorPortSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD63A382CD520FD00445268 /* ObfuscatorPortSelector.swift */; }; + 7AD63A392CD520FD00445268 /* RelayObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD63A382CD520FD00445268 /* RelayObfuscator.swift */; }; 7AD63A3B2CD5278900445268 /* ObfuscationMethodSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD63A3A2CD5278900445268 /* ObfuscationMethodSelector.swift */; }; - 7AD63A3D2CD9065D00445268 /* ObfuscatorPortSelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD63A3C2CD9065100445268 /* ObfuscatorPortSelectorTests.swift */; }; + 7AD63A3D2CD9065D00445268 /* RelayObfuscatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD63A3C2CD9065100445268 /* RelayObfuscatorTests.swift */; }; 7AD63A3F2CDA53F600445268 /* ObfuscationMethodSelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD63A3E2CDA53E900445268 /* ObfuscationMethodSelectorTests.swift */; }; 7AD63A442CDA663300445268 /* UInt+Counting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD63A432CDA662900445268 /* UInt+Counting.swift */; }; 7AD63A472CDA666100445268 /* UIntTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD63A462CDA665A00445268 /* UIntTests.swift */; }; @@ -2176,6 +2179,9 @@ 7A95B6742D5DF86400687524 /* APIRequestProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIRequestProxy.swift; sourceTree = "<group>"; }; 7A95B67A2D5F758300687524 /* relays.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = relays.json; sourceTree = "<group>"; }; 7A95B67C2D5F7C5B00687524 /* DAITASettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAITASettingsCoordinator.swift; sourceTree = "<group>"; }; + 7A964BB72E6999A500C6A4EC /* ShadowsocksObfuscator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksObfuscator.swift; sourceTree = "<group>"; }; + 7A964BBA2E699B3000C6A4EC /* QuicObfuscator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuicObfuscator.swift; sourceTree = "<group>"; }; + 7A964BBD2E699CB400C6A4EC /* UdpOverTcpObfuscator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UdpOverTcpObfuscator.swift; sourceTree = "<group>"; }; 7A99D36E2D5606F900891FF7 /* MullvadApiRequestFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiRequestFactory.swift; sourceTree = "<group>"; }; 7A99D3702D56220E00891FF7 /* MullvadApiCancellable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiCancellable.swift; sourceTree = "<group>"; }; 7A9BE5A12B8F88C500E2A7D0 /* LocationNodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationNodeTests.swift; sourceTree = "<group>"; }; @@ -2234,9 +2240,9 @@ 7AD0AA1B2AD6A63F00119E10 /* PacketTunnelActorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelActorStub.swift; sourceTree = "<group>"; }; 7AD0AA1E2AD6C8B900119E10 /* URLRequestProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestProxyProtocol.swift; sourceTree = "<group>"; }; 7AD0AA202AD6CB0000119E10 /* URLRequestProxyStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestProxyStub.swift; sourceTree = "<group>"; }; - 7AD63A382CD520FD00445268 /* ObfuscatorPortSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObfuscatorPortSelector.swift; sourceTree = "<group>"; }; + 7AD63A382CD520FD00445268 /* RelayObfuscator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayObfuscator.swift; sourceTree = "<group>"; }; 7AD63A3A2CD5278900445268 /* ObfuscationMethodSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObfuscationMethodSelector.swift; sourceTree = "<group>"; }; - 7AD63A3C2CD9065100445268 /* ObfuscatorPortSelectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObfuscatorPortSelectorTests.swift; sourceTree = "<group>"; }; + 7AD63A3C2CD9065100445268 /* RelayObfuscatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayObfuscatorTests.swift; sourceTree = "<group>"; }; 7AD63A3E2CDA53E900445268 /* ObfuscationMethodSelectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObfuscationMethodSelectorTests.swift; sourceTree = "<group>"; }; 7AD63A432CDA662900445268 /* UInt+Counting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt+Counting.swift"; sourceTree = "<group>"; }; 7AD63A462CDA665A00445268 /* UIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIntTests.swift; sourceTree = "<group>"; }; @@ -2866,7 +2872,7 @@ A9EC20E72A5D3A8C0040D56E /* CoordinatesTests.swift */, 7ACE19142C1C429A00260BB6 /* MultihopDecisionFlowTests.swift */, 7AD63A3E2CDA53E900445268 /* ObfuscationMethodSelectorTests.swift */, - 7AD63A3C2CD9065100445268 /* ObfuscatorPortSelectorTests.swift */, + 7AD63A3C2CD9065100445268 /* RelayObfuscatorTests.swift */, A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */, 7ACE19122C1C352100260BB6 /* RelayPickingTests.swift */, 584B26F3237434D00073B10E /* RelaySelectorTests.swift */, @@ -4438,6 +4444,17 @@ path = DAITA; sourceTree = "<group>"; }; + 7A964BBB2E699B4300C6A4EC /* Obfuscation */ = { + isa = PBXGroup; + children = ( + 7A964BBA2E699B3000C6A4EC /* QuicObfuscator.swift */, + 7AD63A382CD520FD00445268 /* RelayObfuscator.swift */, + 7A964BB72E6999A500C6A4EC /* ShadowsocksObfuscator.swift */, + 7A964BBD2E699CB400C6A4EC /* UdpOverTcpObfuscator.swift */, + ); + path = Obfuscation; + sourceTree = "<group>"; + }; 7A9BE5A02B8F881B00E2A7D0 /* SelectLocation */ = { isa = PBXGroup; children = ( @@ -4766,13 +4783,13 @@ F0ACE3172BE4E487006D5333 /* MullvadREST */ = { isa = PBXGroup; children = ( - F0FA160D2D7F2C3D007E2546 /* MockRelayCache.swift */, F0164EB92B4456D30020268D /* AccessMethodRepository+Stub.swift */, A900E9BF2ACC661900C95F67 /* AccessTokenManager+Stubs.swift */, A900E9B72ACC5C2B00C95F67 /* AccountsProxy+Stubs.swift */, A900E9BD2ACC654100C95F67 /* APIProxy+Stubs.swift */, A900E9BB2ACC609200C95F67 /* DevicesProxy+Stubs.swift */, F0ACE32E2BE4EA8B006D5333 /* MockProxyFactory.swift */, + F0FA160D2D7F2C3D007E2546 /* MockRelayCache.swift */, 58FE25EF2AA77664003D1918 /* RelaySelectorStub.swift */, A900E9B92ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift */, 7AF84F452D12C59F00C72690 /* SelectedRelaysStub+Stubs.swift */, @@ -4806,6 +4823,8 @@ F0DC779F2B2222D20087F09D /* Relay */ = { isa = PBXGroup; children = ( + 7A964BBB2E699B4300C6A4EC /* Obfuscation */, + 7AFBE38E2D09AB4E002335FC /* RelayPicking */, 7ADCB2D72B6A6EB300C88F89 /* AnyRelay.swift */, 585DA87626B024A600B8C587 /* CachedRelays.swift */, F0DDE4272B220A15006B57A7 /* Haversine.swift */, @@ -4815,9 +4834,7 @@ 7ACE19102C1C349200260BB6 /* MultihopDecisionFlow.swift */, F0F3161A2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift */, 7AD63A3A2CD5278900445268 /* ObfuscationMethodSelector.swift */, - 7AD63A382CD520FD00445268 /* ObfuscatorPortSelector.swift */, 5820675A26E6576800655B05 /* RelayCache.swift */, - 7AFBE38E2D09AB4E002335FC /* RelayPicking */, F0791F1A2D76377400449F6D /* RelayCandidates.swift */, F0DDE4282B220A15006B57A7 /* RelaySelector.swift */, F0B894F42BF7528700817A42 /* RelaySelector+Shadowsocks.swift */, @@ -5942,13 +5959,14 @@ F05F39972B21C735006E60A7 /* RelayCache.swift in Sources */, A932D9EF2B5ADD0700999395 /* ProxyConfigurationTransportProvider.swift in Sources */, F01528BB2BFF3FEE00B01D00 /* ShadowsocksRelaySelector.swift in Sources */, - 7AD63A392CD520FD00445268 /* ObfuscatorPortSelector.swift in Sources */, + 7AD63A392CD520FD00445268 /* RelayObfuscator.swift in Sources */, 06799AE728F98E4800ACD94E /* RESTURLSession.swift in Sources */, A90763B52B2857D50045ADF0 /* Socks5Constants.swift in Sources */, A90763BA2B2857D50045ADF0 /* Socks5Error.swift in Sources */, 7A2C0E8C2D8B13F0003D8048 /* MullvadAPIProxy.swift in Sources */, 06799AF428F98E4800ACD94E /* RESTAuthorization.swift in Sources */, 06799AE228F98E4800ACD94E /* RESTRequestFactory.swift in Sources */, + 7A964BBC2E699C8500C6A4EC /* QuicObfuscator.swift in Sources */, A90763BD2B2857D50045ADF0 /* Socks5Connection.swift in Sources */, 06799AEC28F98E4800ACD94E /* RESTTaskIdentifier.swift in Sources */, 7A2E7B712D6C9FE0009EF2C3 /* APIError.swift in Sources */, @@ -5992,6 +6010,7 @@ 7AD63A442CDA663300445268 /* UInt+Counting.swift in Sources */, 7A516C3A2B7111A700BBD33D /* IPOverrideWrapper.swift in Sources */, A90763B62B2857D50045ADF0 /* Socks5ConnectNegotiation.swift in Sources */, + 7A964BB92E699A3F00C6A4EC /* ShadowsocksObfuscator.swift in Sources */, 7ACE19112C1C349200260BB6 /* MultihopDecisionFlow.swift in Sources */, F06045E62B231EB700B2D37A /* URLSessionTransport.swift in Sources */, 06799AE628F98E4800ACD94E /* ServerRelaysResponse.swift in Sources */, @@ -6010,6 +6029,7 @@ 06799AE528F98E4800ACD94E /* HTTP.swift in Sources */, F0164ED12B4F2DCB0020268D /* AccessMethodIterator.swift in Sources */, A9D99B9A2A1F7C3200DE27D3 /* RESTTransport.swift in Sources */, + 7A964BBE2E699CBF00C6A4EC /* UdpOverTcpObfuscator.swift in Sources */, A90763BB2B2857D50045ADF0 /* Socks5AddressType.swift in Sources */, 7A99D36F2D56070400891FF7 /* MullvadApiRequestFactory.swift in Sources */, F0A89CB32D9D6C2100580C27 /* MullvadDeviceProxy.swift in Sources */, @@ -6149,7 +6169,7 @@ F998EFF82D359C4600D88D01 /* SKProduct+Formatting.swift in Sources */, A9A5FA152ACB05160083449F /* RedeemVoucherOperation.swift in Sources */, A9A5FA162ACB05160083449F /* RotateKeyOperation.swift in Sources */, - 7AD63A3D2CD9065D00445268 /* ObfuscatorPortSelectorTests.swift in Sources */, + 7AD63A3D2CD9065D00445268 /* RelayObfuscatorTests.swift in Sources */, F072D3CF2C07122400906F64 /* SettingsUpdaterTests.swift in Sources */, 7ACE19132C1C352100260BB6 /* RelayPickingTests.swift in Sources */, F09D04B52AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift in Sources */, diff --git a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift index 953d9153ae..7d0502ae3a 100644 --- a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift @@ -250,6 +250,11 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific "No DAITA compatible servers match your location settings. Try changing location.", comment: "" ) + case .noRelaysSatisfyingObfuscationSettings: + NSLocalizedString( + "No servers match your obfuscation settings. Try changing location or obfuscation method.", + comment: "" + ) case .noRelaysSatisfyingConstraints: NSLocalizedString("No servers match your settings, try changing server or other settings.", comment: "") case .noRelaysSatisfyingPortConstraints: diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift index 8a5988ca83..286b287a5b 100644 --- a/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift +++ b/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift @@ -169,19 +169,22 @@ class MultihopDecisionFlowTests: XCTestCase { extension MultihopDecisionFlowTests { var picker: MultihopPicker { - let obfuscation = try? ObfuscatorPortSelector(relays: sampleRelays) - .obfuscate(tunnelSettings: LatestTunnelSettings(), connectionAttemptCount: 0) + let obfuscation = try? RelayObfuscator( + relays: sampleRelays, + tunnelSettings: LatestTunnelSettings(), + connectionAttemptCount: 0 + ).obfuscate() - let constraints = RelayConstraints( + var tunnelSettings = LatestTunnelSettings() + tunnelSettings.relayConstraints = RelayConstraints( entryLocations: .only(UserSelectedRelays(locations: [.city("se", "sto")])), exitLocations: .only(UserSelectedRelays(locations: [.city("se", "sto")])) ) return MultihopPicker( obfuscation: obfuscation.unsafelyUnwrapped, - constraints: constraints, - connectionAttemptCount: 0, - daitaSettings: DAITASettings(daitaState: .off) + tunnelSettings: tunnelSettings, + connectionAttemptCount: 0 ) } diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscatorPortSelectorTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelayObfuscatorTests.swift index 9fa4edb54d..df9778a2dc 100644 --- a/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscatorPortSelectorTests.swift +++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelayObfuscatorTests.swift @@ -1,5 +1,5 @@ // -// ObfuscatorPortSelectorTests.swift +// RelayObfuscatorTests.swift // MullvadVPN // // Created by Jon Petersson on 2024-11-04. @@ -12,7 +12,7 @@ import MullvadMockData @testable import MullvadTypes import XCTest -final class ObfuscatorPortSelectorTests: XCTestCase { +final class RelayObfuscatorTests: XCTestCase { let defaultWireguardPort: RelayConstraint<UInt16> = .only(56) let defaultQuicPort: RelayConstraint<UInt16> = .only(443) @@ -26,16 +26,71 @@ final class ObfuscatorPortSelectorTests: XCTestCase { func testObfuscateOffDoesNotChangeEndpoint() throws { tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings(state: .off) - let obfuscationResult = try ObfuscatorPortSelector( - relays: sampleRelays - ).obfuscate( + let obfuscationResult = try RelayObfuscator( + relays: sampleRelays, tunnelSettings: tunnelSettings, connectionAttemptCount: 0 - ) + ).obfuscate() XCTAssertEqual(obfuscationResult.port, defaultWireguardPort) } + func testObfuscationForSinglehop() throws { + let constraints = RelayConstraints(entryLocations: .any, exitLocations: .any, port: .only(5000)) + let settings = LatestTunnelSettings( + relayConstraints: constraints, + wireGuardObfuscation: WireGuardObfuscationSettings( + state: .udpOverTcp, + udpOverTcpPort: .port80 + ) + ) + + let obfuscationResult = try RelayObfuscator( + relays: sampleRelays, + tunnelSettings: settings, + connectionAttemptCount: 0 + ).obfuscate() + + let picker = SinglehopPicker( + obfuscation: obfuscationResult, + tunnelSettings: settings, + connectionAttemptCount: 0 + ) + + let selectedRelays = try picker.pick() + + XCTAssertNil(selectedRelays.entry) + XCTAssertEqual(selectedRelays.exit.endpoint.ipv4Relay.port, 80) + } + + func testObfuscationForMultihop() throws { + let constraints = RelayConstraints(entryLocations: .any, exitLocations: .any, port: .only(5000)) + let settings = LatestTunnelSettings( + relayConstraints: constraints, + wireGuardObfuscation: WireGuardObfuscationSettings( + state: .udpOverTcp, + udpOverTcpPort: .port80 + ) + ) + + let obfuscationResult = try RelayObfuscator( + relays: sampleRelays, + tunnelSettings: settings, + connectionAttemptCount: 0 + ).obfuscate() + + let picker = MultihopPicker( + obfuscation: obfuscationResult, + tunnelSettings: settings, + connectionAttemptCount: 0 + ) + + let selectedRelays = try picker.pick() + + XCTAssertEqual(selectedRelays.entry?.endpoint.ipv4Relay.port, 80) + XCTAssertEqual(selectedRelays.exit.endpoint.ipv4Relay.port, 5000) + } + // MARK: UdpOverTcp func testObfuscateUdpOverTcpPort80() throws { @@ -44,12 +99,11 @@ final class ObfuscatorPortSelectorTests: XCTestCase { udpOverTcpPort: .port80 ) - let obfuscationResult = try ObfuscatorPortSelector( - relays: sampleRelays - ).obfuscate( + let obfuscationResult = try RelayObfuscator( + relays: sampleRelays, tunnelSettings: tunnelSettings, connectionAttemptCount: 0 - ) + ).obfuscate() XCTAssertEqual(obfuscationResult.port, .only(80)) } @@ -60,12 +114,11 @@ final class ObfuscatorPortSelectorTests: XCTestCase { udpOverTcpPort: .port5001 ) - let obfuscationResult = try ObfuscatorPortSelector( - relays: sampleRelays - ).obfuscate( + let obfuscationResult = try RelayObfuscator( + relays: sampleRelays, tunnelSettings: tunnelSettings, connectionAttemptCount: 0 - ) + ).obfuscate() XCTAssertEqual(obfuscationResult.port, .only(5001)) } @@ -77,12 +130,11 @@ final class ObfuscatorPortSelectorTests: XCTestCase { ) try (0 ... 10).filter { $0.isMultiple(of: 2) }.forEach { attempt in - let obfuscationResult = try ObfuscatorPortSelector( - relays: sampleRelays - ).obfuscate( + let obfuscationResult = try RelayObfuscator( + relays: sampleRelays, tunnelSettings: tunnelSettings, connectionAttemptCount: UInt(attempt) - ) + ).obfuscate() let validPorts: [RelayConstraint<UInt16>] = [.only(80), .only(5001)] XCTAssertTrue(validPorts.contains(obfuscationResult.port)) @@ -97,12 +149,11 @@ final class ObfuscatorPortSelectorTests: XCTestCase { shadowsocksPort: .custom(5500) ) - let obfuscationResult = try ObfuscatorPortSelector( - relays: sampleRelays - ).obfuscate( + let obfuscationResult = try RelayObfuscator( + relays: sampleRelays, tunnelSettings: tunnelSettings, connectionAttemptCount: 0 - ) + ).obfuscate() XCTAssertEqual(obfuscationResult.port, .only(5500)) } @@ -113,12 +164,11 @@ final class ObfuscatorPortSelectorTests: XCTestCase { shadowsocksPort: .automatic ) - let obfuscationResult = try ObfuscatorPortSelector( - relays: sampleRelays - ).obfuscate( + let obfuscationResult = try RelayObfuscator( + relays: sampleRelays, tunnelSettings: tunnelSettings, connectionAttemptCount: 0 - ) + ).obfuscate() let portRanges = RelaySelector.parseRawPortRanges(sampleRelays.wireguard.shadowsocksPortRanges) @@ -144,18 +194,17 @@ final class ObfuscatorPortSelectorTests: XCTestCase { shadowsocksPort: .custom(port) ) - let obfuscationResult = try ObfuscatorPortSelector( - relays: sampleRelays - ).obfuscate( + let obfuscationResult = try RelayObfuscator( + relays: sampleRelays, tunnelSettings: tunnelSettings, connectionAttemptCount: 0 - ) + ).obfuscate() let relaysWithExtraAddresses = sampleRelays.wireguard.relays.filter { relay in !relay.shadowsocksExtraAddrIn.isNil } - XCTAssertEqual(obfuscationResult.wireguard.relays.count, relaysWithExtraAddresses.count) + XCTAssertEqual(obfuscationResult.obfuscatedRelays.wireguard.relays.count, relaysWithExtraAddresses.count) } func testObfuscateShadowsocksRelayFilteringWithPortInsideDefaultRanges() throws { @@ -167,14 +216,13 @@ final class ObfuscatorPortSelectorTests: XCTestCase { shadowsocksPort: .custom(port) ) - let obfuscationResult = try ObfuscatorPortSelector( - relays: sampleRelays - ).obfuscate( + let obfuscationResult = try RelayObfuscator( + relays: sampleRelays, tunnelSettings: tunnelSettings, connectionAttemptCount: 0 - ) + ).obfuscate() - XCTAssertEqual(obfuscationResult.wireguard.relays.count, sampleRelays.wireguard.relays.count) + XCTAssertEqual(obfuscationResult.obfuscatedRelays.wireguard.relays.count, sampleRelays.wireguard.relays.count) } // MARK: QUIC @@ -184,12 +232,11 @@ final class ObfuscatorPortSelectorTests: XCTestCase { state: .quic ) - let obfuscationResult = try ObfuscatorPortSelector( - relays: sampleRelays - ).obfuscate( + let obfuscationResult = try RelayObfuscator( + relays: sampleRelays, tunnelSettings: tunnelSettings, connectionAttemptCount: 0 - ) + ).obfuscate() XCTAssertEqual(obfuscationResult.port, defaultQuicPort) } diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift index ed7ae0ccdf..6488306221 100644 --- a/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift +++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift @@ -16,11 +16,15 @@ import XCTest class RelayPickingTests: XCTestCase { let sampleRelays = ServerRelaysResponseStubs.sampleRelays - var obfuscation: ObfuscatorPortSelection! + var obfuscation: RelayObfuscation! override func setUpWithError() throws { - obfuscation = try ObfuscatorPortSelector(relays: sampleRelays) - .obfuscate(tunnelSettings: LatestTunnelSettings(), connectionAttemptCount: 0) + // Default obfuscation settings to satisfy picker constructors for the tests below. + obfuscation = try RelayObfuscator( + relays: sampleRelays, + tunnelSettings: LatestTunnelSettings(), + connectionAttemptCount: 0 + ).obfuscate() } // MARK: Single-/multihop @@ -31,11 +35,13 @@ class RelayPickingTests: XCTestCase { exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])) ) + var settings = LatestTunnelSettings() + settings.relayConstraints = constraints + let picker = SinglehopPicker( obfuscation: obfuscation, - constraints: constraints, - connectionAttemptCount: 0, - daitaSettings: DAITASettings() + tunnelSettings: settings, + connectionAttemptCount: 0 ) let selectedRelays = try picker.pick() @@ -50,11 +56,13 @@ class RelayPickingTests: XCTestCase { exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])) ) + var settings = LatestTunnelSettings() + settings.relayConstraints = constraints + let picker = MultihopPicker( obfuscation: obfuscation, - constraints: constraints, - connectionAttemptCount: 0, - daitaSettings: DAITASettings() + tunnelSettings: settings, + connectionAttemptCount: 0 ) let selectedRelays = try picker.pick() @@ -69,11 +77,13 @@ class RelayPickingTests: XCTestCase { exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])) ) + var settings = LatestTunnelSettings() + settings.relayConstraints = constraints + let picker = MultihopPicker( obfuscation: obfuscation, - constraints: constraints, - connectionAttemptCount: 0, - daitaSettings: DAITASettings() + tunnelSettings: settings, + connectionAttemptCount: 0 ) XCTAssertThrowsError( @@ -93,11 +103,14 @@ class RelayPickingTests: XCTestCase { exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])) ) + var settings = LatestTunnelSettings() + settings.relayConstraints = constraints + settings.daita = DAITASettings(daitaState: .on) + let picker = SinglehopPicker( obfuscation: obfuscation, - constraints: constraints, - connectionAttemptCount: 0, - daitaSettings: DAITASettings(daitaState: .on, directOnlyState: .off) + tunnelSettings: settings, + connectionAttemptCount: 0 ) let selectedRelays = try picker.pick() @@ -113,11 +126,14 @@ class RelayPickingTests: XCTestCase { exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])) ) + var settings = LatestTunnelSettings() + settings.relayConstraints = constraints + settings.daita = DAITASettings(daitaState: .on, directOnlyState: .on) + let picker = SinglehopPicker( obfuscation: obfuscation, - constraints: constraints, - connectionAttemptCount: 0, - daitaSettings: DAITASettings(daitaState: .on, directOnlyState: .on) + tunnelSettings: settings, + connectionAttemptCount: 0 ) XCTAssertThrowsError(try picker.pick()) @@ -130,11 +146,14 @@ class RelayPickingTests: XCTestCase { exitLocations: .only(UserSelectedRelays(locations: [.hostname("es", "mad", "es1-wireguard")])) ) + var settings = LatestTunnelSettings() + settings.relayConstraints = constraints + settings.daita = DAITASettings(daitaState: .on) + let picker = SinglehopPicker( obfuscation: obfuscation, - constraints: constraints, - connectionAttemptCount: 0, - daitaSettings: DAITASettings(daitaState: .on, directOnlyState: .off) + tunnelSettings: settings, + connectionAttemptCount: 0 ) let selectedRelays = try picker.pick() @@ -150,11 +169,14 @@ class RelayPickingTests: XCTestCase { exitLocations: .only(UserSelectedRelays(locations: [.hostname("es", "mad", "es1-wireguard")])) ) + var settings = LatestTunnelSettings() + settings.relayConstraints = constraints + settings.daita = DAITASettings(daitaState: .on, directOnlyState: .on) + let picker = SinglehopPicker( obfuscation: obfuscation, - constraints: constraints, - connectionAttemptCount: 0, - daitaSettings: DAITASettings(daitaState: .on, directOnlyState: .on) + tunnelSettings: settings, + connectionAttemptCount: 0 ) let selectedRelays = try picker.pick() @@ -172,11 +194,14 @@ class RelayPickingTests: XCTestCase { exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])) ) + var settings = LatestTunnelSettings() + settings.relayConstraints = constraints + settings.daita = DAITASettings(daitaState: .on) + let picker = MultihopPicker( obfuscation: obfuscation, - constraints: constraints, - connectionAttemptCount: 0, - daitaSettings: DAITASettings(daitaState: .on, directOnlyState: .off) + tunnelSettings: settings, + connectionAttemptCount: 0 ) let selectedRelays = try picker.pick() @@ -194,11 +219,14 @@ class RelayPickingTests: XCTestCase { exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])) ) + var settings = LatestTunnelSettings() + settings.relayConstraints = constraints + settings.daita = DAITASettings(daitaState: .on) + let picker = MultihopPicker( obfuscation: obfuscation, - constraints: constraints, - connectionAttemptCount: 0, - daitaSettings: DAITASettings(daitaState: .on, directOnlyState: .off) + tunnelSettings: settings, + connectionAttemptCount: 0 ) let selectedRelays = try picker.pick() @@ -215,98 +243,16 @@ class RelayPickingTests: XCTestCase { exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])) ) - let picker = MultihopPicker( - obfuscation: obfuscation, - constraints: constraints, - connectionAttemptCount: 0, - daitaSettings: DAITASettings(daitaState: .on, directOnlyState: .on) - ) - - XCTAssertThrowsError(try picker.pick()) - } - - // MARK: Obfuscation - - func testObfuscationForSinglehop() throws { - let constraints = RelayConstraints(entryLocations: .any, exitLocations: .any, port: .only(5000)) - let tunnelSettings = LatestTunnelSettings( - wireGuardObfuscation: WireGuardObfuscationSettings( - state: .udpOverTcp, - udpOverTcpPort: .port80 - ) - ) - - obfuscation = try ObfuscatorPortSelector(relays: sampleRelays) - .obfuscate(tunnelSettings: tunnelSettings, connectionAttemptCount: 0) - - let picker = SinglehopPicker( - obfuscation: obfuscation, - constraints: constraints, - connectionAttemptCount: 0, - daitaSettings: DAITASettings() - ) - - let selectedRelays = try picker.pick() - - XCTAssertNil(selectedRelays.entry) - XCTAssertEqual(selectedRelays.exit.endpoint.ipv4Relay.port, 80) - } - - // If DAITA is on, the selected relay has DAITA and shadowsocks obfuscation yields no compatible relays, - // we should make sure that .noObfuscatedRelaysFound is thrown rather than triggering smart routing. - func testIncompatibleShadowsocksObfuscationNotTriggeringMultihop() throws { - let constraints = RelayConstraints( - entryLocations: .any, - exitLocations: .only(UserSelectedRelays(locations: [.country("us")])), - port: .only(5000) - ) - let tunnelSettings = LatestTunnelSettings( - wireGuardObfuscation: WireGuardObfuscationSettings( - state: .shadowsocks, - shadowsocksPort: .custom(1) - ) - ) - - obfuscation = try ObfuscatorPortSelector(relays: sampleRelays) - .obfuscate(tunnelSettings: tunnelSettings, connectionAttemptCount: 0) - - let picker = SinglehopPicker( - obfuscation: obfuscation, - constraints: constraints, - connectionAttemptCount: 0, - daitaSettings: DAITASettings(daitaState: .on) - ) - - do { - _ = try picker.pick() - XCTFail("Should have thrown error due to incompatible shadowsocks obfuscation") - } catch let error as NoRelaysSatisfyingConstraintsError { - XCTAssertEqual(error.reason, .noObfuscatedRelaysFound) - } - } - - func testObfuscationForMultihop() throws { - let constraints = RelayConstraints(entryLocations: .any, exitLocations: .any, port: .only(5000)) - let tunnelSettings = LatestTunnelSettings( - wireGuardObfuscation: WireGuardObfuscationSettings( - state: .udpOverTcp, - udpOverTcpPort: .port80 - ) - ) - - obfuscation = try ObfuscatorPortSelector(relays: sampleRelays) - .obfuscate(tunnelSettings: tunnelSettings, connectionAttemptCount: 0) + var settings = LatestTunnelSettings() + settings.relayConstraints = constraints + settings.daita = DAITASettings(daitaState: .on, directOnlyState: .on) let picker = MultihopPicker( obfuscation: obfuscation, - constraints: constraints, - connectionAttemptCount: 0, - daitaSettings: DAITASettings() + tunnelSettings: settings, + connectionAttemptCount: 0 ) - let selectedRelays = try picker.pick() - - XCTAssertEqual(selectedRelays.entry?.endpoint.ipv4Relay.port, 80) - XCTAssertEqual(selectedRelays.exit.endpoint.ipv4Relay.port, 5000) + XCTAssertThrowsError(try picker.pick()) } } |
