From 44185aead3faad574b2848ebb1cde3ecc50c6c3a Mon Sep 17 00:00:00 2001 From: Jon Petersson Date: Wed, 11 Dec 2024 13:07:34 +0100 Subject: [PATCH] Fix relay selector to force blocked state with daita and obfuscation on --- .../NoRelaysSatisfyingConstraintsError.swift | 3 + .../Relay/ObfuscatorPortSelector.swift | 2 + ios/MullvadREST/Relay/RelayPicking.swift | 176 ------------------ .../Relay/RelayPicking/MultihopPicker.swift | 82 ++++++++ .../Relay/RelayPicking/RelayPicking.swift | 70 +++++++ .../Relay/RelayPicking/SinglehopPicker.swift | 56 ++++++ ios/MullvadREST/Relay/RelaySelector.swift | 2 +- ios/MullvadVPN.xcodeproj/project.pbxproj | 18 +- .../ServerRelaysResponse+Stubs.swift | 4 +- .../MullvadREST/Relay/RelayPickingTests.swift | 39 +++- .../BlockedStateErrorMapper.swift | 2 + .../Actor/State+Extensions.swift | 2 +- ios/PacketTunnelCore/Actor/State.swift | 3 + 13 files changed, 275 insertions(+), 184 deletions(-) delete mode 100644 ios/MullvadREST/Relay/RelayPicking.swift create mode 100644 ios/MullvadREST/Relay/RelayPicking/MultihopPicker.swift create mode 100644 ios/MullvadREST/Relay/RelayPicking/RelayPicking.swift create mode 100644 ios/MullvadREST/Relay/RelayPicking/SinglehopPicker.swift diff --git a/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift b/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift index bfed08a41044..4250a7ffb6af 100644 --- a/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift +++ b/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift @@ -15,6 +15,7 @@ public enum NoRelaysSatisfyingConstraintsReason { case multihopInvalidFlow case noActiveRelaysFound case noDaitaRelaysFound + case noObfuscatedRelaysFound case relayConstraintNotMatching } @@ -35,6 +36,8 @@ public struct NoRelaysSatisfyingConstraintsError: LocalizedError { "No active relays found" case .noDaitaRelaysFound: "No DAITA relays found" + case .noObfuscatedRelaysFound: + "No obfuscated relays found" case .relayConstraintNotMatching: "Invalid constraint created to pick a relay" } diff --git a/ios/MullvadREST/Relay/ObfuscatorPortSelector.swift b/ios/MullvadREST/Relay/ObfuscatorPortSelector.swift index 4b2f2dca7f4a..a2922b52cd71 100644 --- a/ios/MullvadREST/Relay/ObfuscatorPortSelector.swift +++ b/ios/MullvadREST/Relay/ObfuscatorPortSelector.swift @@ -12,6 +12,7 @@ import MullvadTypes struct ObfuscatorPortSelection { let entryRelays: REST.ServerRelaysResponse let exitRelays: REST.ServerRelaysResponse + let unfilteredRelays: REST.ServerRelaysResponse let port: RelayConstraint let method: WireGuardObfuscationState @@ -61,6 +62,7 @@ struct ObfuscatorPortSelector { return ObfuscatorPortSelection( entryRelays: entryRelays, exitRelays: exitRelays, + unfilteredRelays: relays, port: port, method: obfuscationMethod ) diff --git a/ios/MullvadREST/Relay/RelayPicking.swift b/ios/MullvadREST/Relay/RelayPicking.swift deleted file mode 100644 index 5d7460abecb7..000000000000 --- a/ios/MullvadREST/Relay/RelayPicking.swift +++ /dev/null @@ -1,176 +0,0 @@ -// -// RelaySelectorPicker.swift -// MullvadREST -// -// Created by Jon Petersson on 2024-06-05. -// Copyright © 2024 Mullvad VPN AB. All rights reserved. -// - -import MullvadSettings -import MullvadTypes -import Network - -protocol RelayPicking { - var obfuscation: ObfuscatorPortSelection { get } - var constraints: RelayConstraints { get } - var connectionAttemptCount: UInt { get } - var daitaSettings: DAITASettings { get } - func pick() throws -> SelectedRelays -} - -extension RelayPicking { - func findBestMatch( - from candidates: [RelayWithLocation], - closeTo location: Location? = nil, - useObfuscatedPortIfAvailable: Bool - ) throws -> SelectedRelay { - var match = try RelaySelector.WireGuard.pickCandidate( - from: candidates, - wireguard: obfuscation.wireguard, - portConstraint: useObfuscatedPortIfAvailable ? obfuscation.port : constraints.port, - numberOfFailedAttempts: connectionAttemptCount, - closeTo: location - ) - - if useObfuscatedPortIfAvailable && obfuscation.method == .shadowsocks { - match = applyShadowsocksIpAddress(in: match) - } - - return SelectedRelay( - endpoint: match.endpoint, - hostname: match.relay.hostname, - location: match.location - ) - } - - private func applyShadowsocksIpAddress(in match: RelaySelectorMatch) -> RelaySelectorMatch { - let port = match.endpoint.ipv4Relay.port - let portRanges = RelaySelector.parseRawPortRanges(obfuscation.wireguard.shadowsocksPortRanges) - let portIsWithinRange = portRanges.contains(where: { $0.contains(port) }) - - var endpoint = match.endpoint - - // If the currently selected obfuscation port is not within the allowed range (as specified - // in the relay list), we should use one of the extra Shadowsocks IP addresses instead of - // the default one. - if !portIsWithinRange { - var ipv4Address = match.endpoint.ipv4Relay.ip - if let shadowsocksAddress = match.relay.shadowsocksExtraAddrIn?.randomElement() { - ipv4Address = IPv4Address(shadowsocksAddress) ?? ipv4Address - } - - endpoint = match.endpoint.override(ipv4Relay: IPv4Endpoint( - ip: ipv4Address, - port: port - )) - } - - return RelaySelectorMatch(endpoint: endpoint, relay: match.relay, location: match.location) - } -} - -struct SinglehopPicker: RelayPicking { - let obfuscation: ObfuscatorPortSelection - let constraints: RelayConstraints - let connectionAttemptCount: UInt - let daitaSettings: DAITASettings - - func pick() throws -> SelectedRelays { - do { - let exitCandidates = try RelaySelector.WireGuard.findCandidates( - by: constraints.exitLocations, - in: obfuscation.exitRelays, - filterConstraint: constraints.filter, - daitaEnabled: daitaSettings.daitaState.isEnabled - ) - - let match = try findBestMatch(from: exitCandidates, useObfuscatedPortIfAvailable: true) - return SelectedRelays(entry: nil, exit: match, retryAttempt: connectionAttemptCount) - } catch let error as NoRelaysSatisfyingConstraintsError where error.reason == .noDaitaRelaysFound { - // If DAITA is on and Direct only is off, 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 { - return try MultihopPicker( - obfuscation: obfuscation, - constraints: constraints, - connectionAttemptCount: connectionAttemptCount, - daitaSettings: daitaSettings - ).pick() - } else { - throw error - } - } - } -} - -struct MultihopPicker: RelayPicking { - let obfuscation: ObfuscatorPortSelection - let constraints: RelayConstraints - 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. - 2. Entry constraint matches only a single relay and the other multiple relays. The single relay - is selected and excluded from the list of multiple relays. - 3. Exit constraint matches multiple relays and the other a single relay. The single relay - is selected and excluded from the list of multiple relays. - 4. Both entry and exit constraints match multiple relays. Exit relay is picked first and then - excluded from the list of entry relays. - */ - let decisionFlow = OneToOne( - next: OneToMany( - next: ManyToOne( - next: ManyToMany( - next: nil, - relayPicker: self - ), - relayPicker: self - ), - relayPicker: self - ), - 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 - ) - - return try decisionFlow.pick( - entryCandidates: entryCandidates, - exitCandidates: exitCandidates, - daitaAutomaticRouting: daitaSettings.isAutomaticRouting - ) - } - } - - func exclude( - relay: SelectedRelay, - from candidates: [RelayWithLocation], - closeTo location: Location? = nil, - useObfuscatedPortIfAvailable: Bool - ) throws -> SelectedRelay { - let filteredCandidates = candidates.filter { relayWithLocation in - relayWithLocation.relay.hostname != relay.hostname - } - - return try findBestMatch( - from: filteredCandidates, - closeTo: location, - useObfuscatedPortIfAvailable: useObfuscatedPortIfAvailable - ) - } -} diff --git a/ios/MullvadREST/Relay/RelayPicking/MultihopPicker.swift b/ios/MullvadREST/Relay/RelayPicking/MultihopPicker.swift new file mode 100644 index 000000000000..32f4f72ac5c1 --- /dev/null +++ b/ios/MullvadREST/Relay/RelayPicking/MultihopPicker.swift @@ -0,0 +1,82 @@ +// +// MultihopPicker.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-12-11. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import MullvadSettings +import MullvadTypes + +struct MultihopPicker: RelayPicking { + let obfuscation: ObfuscatorPortSelection + let constraints: RelayConstraints + 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. + 2. Entry constraint matches only a single relay and the other multiple relays. The single relay + is selected and excluded from the list of multiple relays. + 3. Exit constraint matches multiple relays and the other a single relay. The single relay + is selected and excluded from the list of multiple relays. + 4. Both entry and exit constraints match multiple relays. Exit relay is picked first and then + excluded from the list of entry relays. + */ + let decisionFlow = OneToOne( + next: OneToMany( + next: ManyToOne( + next: ManyToMany( + next: nil, + relayPicker: self + ), + relayPicker: self + ), + relayPicker: self + ), + 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 + ) + + return try decisionFlow.pick( + entryCandidates: entryCandidates, + exitCandidates: exitCandidates, + daitaAutomaticRouting: daitaSettings.isAutomaticRouting + ) + } + } + + func exclude( + relay: SelectedRelay, + from candidates: [RelayWithLocation], + closeTo location: Location? = nil, + useObfuscatedPortIfAvailable: Bool + ) throws -> SelectedRelay { + let filteredCandidates = candidates.filter { relayWithLocation in + relayWithLocation.relay.hostname != relay.hostname + } + + return try findBestMatch( + from: filteredCandidates, + closeTo: location, + useObfuscatedPortIfAvailable: useObfuscatedPortIfAvailable + ) + } +} diff --git a/ios/MullvadREST/Relay/RelayPicking/RelayPicking.swift b/ios/MullvadREST/Relay/RelayPicking/RelayPicking.swift new file mode 100644 index 000000000000..15d8a0f6d14e --- /dev/null +++ b/ios/MullvadREST/Relay/RelayPicking/RelayPicking.swift @@ -0,0 +1,70 @@ +// +// RelaySelectorPicker.swift +// MullvadREST +// +// Created by Jon Petersson on 2024-06-05. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import MullvadSettings +import MullvadTypes +import Network + +protocol RelayPicking { + var obfuscation: ObfuscatorPortSelection { get } + var constraints: RelayConstraints { get } + var connectionAttemptCount: UInt { get } + var daitaSettings: DAITASettings { get } + func pick() throws -> SelectedRelays +} + +extension RelayPicking { + func findBestMatch( + from candidates: [RelayWithLocation], + closeTo location: Location? = nil, + useObfuscatedPortIfAvailable: Bool + ) throws -> SelectedRelay { + var match = try RelaySelector.WireGuard.pickCandidate( + from: candidates, + wireguard: obfuscation.wireguard, + portConstraint: useObfuscatedPortIfAvailable ? obfuscation.port : constraints.port, + numberOfFailedAttempts: connectionAttemptCount, + closeTo: location + ) + + if useObfuscatedPortIfAvailable && obfuscation.method == .shadowsocks { + match = applyShadowsocksIpAddress(in: match) + } + + return SelectedRelay( + endpoint: match.endpoint, + hostname: match.relay.hostname, + location: match.location + ) + } + + private func applyShadowsocksIpAddress(in match: RelaySelectorMatch) -> RelaySelectorMatch { + let port = match.endpoint.ipv4Relay.port + let portRanges = RelaySelector.parseRawPortRanges(obfuscation.wireguard.shadowsocksPortRanges) + let portIsWithinRange = portRanges.contains(where: { $0.contains(port) }) + + var endpoint = match.endpoint + + // If the currently selected obfuscation port is not within the allowed range (as specified + // in the relay list), we should use one of the extra Shadowsocks IP addresses instead of + // the default one. + if !portIsWithinRange { + var ipv4Address = match.endpoint.ipv4Relay.ip + if let shadowsocksAddress = match.relay.shadowsocksExtraAddrIn?.randomElement() { + ipv4Address = IPv4Address(shadowsocksAddress) ?? ipv4Address + } + + endpoint = match.endpoint.override(ipv4Relay: IPv4Endpoint( + ip: ipv4Address, + port: port + )) + } + + return RelaySelectorMatch(endpoint: endpoint, relay: match.relay, location: match.location) + } +} diff --git a/ios/MullvadREST/Relay/RelayPicking/SinglehopPicker.swift b/ios/MullvadREST/Relay/RelayPicking/SinglehopPicker.swift new file mode 100644 index 000000000000..af891c341ac5 --- /dev/null +++ b/ios/MullvadREST/Relay/RelayPicking/SinglehopPicker.swift @@ -0,0 +1,56 @@ +// +// SinglehopPicker.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-12-11. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import MullvadSettings +import MullvadTypes + +struct SinglehopPicker: RelayPicking { + let obfuscation: ObfuscatorPortSelection + let constraints: RelayConstraints + let connectionAttemptCount: UInt + let daitaSettings: DAITASettings + + func pick() throws -> SelectedRelays { + do { + return try pick(from: obfuscation.exitRelays) + } 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. + do { + _ = try pick(from: obfuscation.unfilteredRelays) + throw NoRelaysSatisfyingConstraintsError(.noObfuscatedRelaysFound) + } catch let error as NoRelaysSatisfyingConstraintsError where error.reason == .noDaitaRelaysFound { + // 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 { + return try MultihopPicker( + obfuscation: obfuscation, + constraints: constraints, + connectionAttemptCount: connectionAttemptCount, + daitaSettings: daitaSettings + ).pick() + } else { + throw error + } + } + } + } + + private func pick(from exitRelays: REST.ServerRelaysResponse) throws -> SelectedRelays { + let exitCandidates = try RelaySelector.WireGuard.findCandidates( + by: constraints.exitLocations, + in: exitRelays, + filterConstraint: constraints.filter, + daitaEnabled: daitaSettings.daitaState.isEnabled + ) + + let match = try findBestMatch(from: exitCandidates, useObfuscatedPortIfAvailable: true) + return SelectedRelays(entry: nil, exit: match, retryAttempt: connectionAttemptCount) + } +} diff --git a/ios/MullvadREST/Relay/RelaySelector.swift b/ios/MullvadREST/Relay/RelaySelector.swift index 96118e6ae558..d20f3039bc70 100644 --- a/ios/MullvadREST/Relay/RelaySelector.swift +++ b/ios/MullvadREST/Relay/RelaySelector.swift @@ -259,7 +259,7 @@ public enum RelaySelector { } } - // If no relays should be included in the matched country, instead accept all. + // If no relays are included in the matched country, instead accept all. return if filteredRelays.isEmpty { relays } else { diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 36ca68285350..492410542419 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -656,6 +656,8 @@ 7AF9BE902A39F26000DBFEDB /* Collection+Sorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE8F2A39F26000DBFEDB /* Collection+Sorting.swift */; }; 7AF9BE952A40461100DBFEDB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE942A40461100DBFEDB /* RelayFilterView.swift */; }; 7AF9BE972A41C71F00DBFEDB /* ChipViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF9BE962A41C71F00DBFEDB /* ChipViewCell.swift */; }; + 7AFBE38B2D09AAFF002335FC /* SinglehopPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE38A2D09AAFF002335FC /* SinglehopPicker.swift */; }; + 7AFBE38D2D09AB2E002335FC /* MultihopPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE38C2D09AB2E002335FC /* MultihopPicker.swift */; }; 850201DB2B503D7700EF8C96 /* RelayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850201DA2B503D7700EF8C96 /* RelayTests.swift */; }; 850201DD2B503D8C00EF8C96 /* SelectLocationPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850201DC2B503D8C00EF8C96 /* SelectLocationPage.swift */; }; 850201DF2B5040A500EF8C96 /* TunnelControlPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850201DE2B5040A500EF8C96 /* TunnelControlPage.swift */; }; @@ -2010,6 +2012,8 @@ 7AF9BE8F2A39F26000DBFEDB /* Collection+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Sorting.swift"; sourceTree = ""; }; 7AF9BE942A40461100DBFEDB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = ""; }; 7AF9BE962A41C71F00DBFEDB /* ChipViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipViewCell.swift; sourceTree = ""; }; + 7AFBE38A2D09AAFF002335FC /* SinglehopPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SinglehopPicker.swift; sourceTree = ""; }; + 7AFBE38C2D09AB2E002335FC /* MultihopPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopPicker.swift; sourceTree = ""; }; 85006A8E2B73EF67004AD8FB /* MullvadVPNUITestsSmoke.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MullvadVPNUITestsSmoke.xctestplan; sourceTree = ""; }; 850201DA2B503D7700EF8C96 /* RelayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayTests.swift; sourceTree = ""; }; 850201DC2B503D8C00EF8C96 /* SelectLocationPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationPage.swift; sourceTree = ""; }; @@ -4094,6 +4098,16 @@ path = RelayFilter; sourceTree = ""; }; + 7AFBE38E2D09AB4E002335FC /* RelayPicking */ = { + isa = PBXGroup; + children = ( + 7AFBE38C2D09AB2E002335FC /* MultihopPicker.swift */, + 7A3AD5002C1068A800E9AD90 /* RelayPicking.swift */, + 7AFBE38A2D09AAFF002335FC /* SinglehopPicker.swift */, + ); + path = RelayPicking; + sourceTree = ""; + }; 8518F6392B601910009EB113 /* Base */ = { isa = PBXGroup; children = ( @@ -4357,6 +4371,7 @@ F0DC779F2B2222D20087F09D /* Relay */ = { isa = PBXGroup; children = ( + 7AFBE38E2D09AB4E002335FC /* RelayPicking */, 7ADCB2D72B6A6EB300C88F89 /* AnyRelay.swift */, 585DA87626B024A600B8C587 /* CachedRelays.swift */, F0DDE4272B220A15006B57A7 /* Haversine.swift */, @@ -4367,7 +4382,6 @@ 7AD63A3A2CD5278900445268 /* ObfuscationMethodSelector.swift */, 7AD63A382CD520FD00445268 /* ObfuscatorPortSelector.swift */, 5820675A26E6576800655B05 /* RelayCache.swift */, - 7A3AD5002C1068A800E9AD90 /* RelayPicking.swift */, F0DDE4282B220A15006B57A7 /* RelaySelector.swift */, F0B894F42BF7528700817A42 /* RelaySelector+Shadowsocks.swift */, F0B894F22BF7526700817A42 /* RelaySelector+Wireguard.swift */, @@ -5399,6 +5413,7 @@ F0164EBE2B4BFF940020268D /* ShadowsocksLoader.swift in Sources */, A9A1DE792AD5708E0073F689 /* TransportStrategy.swift in Sources */, A90763BF2B2857D50045ADF0 /* Socks5Handshake.swift in Sources */, + 7AFBE38D2D09AB2E002335FC /* MultihopPicker.swift in Sources */, 7A3AD5012C1068A800E9AD90 /* RelayPicking.swift in Sources */, A90763C52B2858B40045ADF0 /* AnyIPEndpoint+Socks5.swift in Sources */, F06045EC2B2322A500B2D37A /* Jittered.swift in Sources */, @@ -5418,6 +5433,7 @@ F0E5B2F82C9C68CF0007F78C /* EncryptedDNSTransport.swift in Sources */, 06799AF228F98E4800ACD94E /* RESTAccessTokenManager.swift in Sources */, A90763B12B2857D50045ADF0 /* Socks5Endpoint.swift in Sources */, + 7AFBE38B2D09AAFF002335FC /* SinglehopPicker.swift in Sources */, 06799AF328F98E4800ACD94E /* RESTAuthenticationProxy.swift in Sources */, F0B894F12BF751E300817A42 /* RelayWithDistance.swift in Sources */, 7AD63A442CDA663300445268 /* UInt+Counting.swift in Sources */, diff --git a/ios/MullvadVPNTests/MullvadREST/ApiHandlers/ServerRelaysResponse+Stubs.swift b/ios/MullvadVPNTests/MullvadREST/ApiHandlers/ServerRelaysResponse+Stubs.swift index 09511a0494a1..87ccc3ea5037 100644 --- a/ios/MullvadVPNTests/MullvadREST/ApiHandlers/ServerRelaysResponse+Stubs.swift +++ b/ios/MullvadVPNTests/MullvadREST/ApiHandlers/ServerRelaysResponse+Stubs.swift @@ -143,8 +143,8 @@ enum ServerRelaysResponseStubs { ipv6AddrIn: .loopback, publicKey: PrivateKey().publicKey.rawValue, includeInCountry: true, - daita: true, - shadowsocksExtraAddrIn: nil + daita: false, + shadowsocksExtraAddrIn: ["0.0.0.0"] ), REST.ServerRelay( hostname: "us-nyc-wg-301", diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift index 39970afc2508..cb0f4039876a 100644 --- a/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift +++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift @@ -226,7 +226,7 @@ class RelayPickingTests: XCTestCase { // MARK: Obfuscation - func testObfuscationOnSinglehop() throws { + func testObfuscationForSinglehop() throws { let constraints = RelayConstraints(entryLocations: .any, exitLocations: .any, port: .only(5000)) let tunnelSettings = LatestTunnelSettings( wireGuardObfuscation: WireGuardObfuscationSettings( @@ -247,11 +247,44 @@ class RelayPickingTests: XCTestCase { let selectedRelays = try picker.pick() - XCTAssertNil(selectedRelays.entry?.endpoint.ipv4Relay.port) + XCTAssertNil(selectedRelays.entry) XCTAssertEqual(selectedRelays.exit.endpoint.ipv4Relay.port, 80) } - func testObfuscationOnMultihop() throws { + // 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( diff --git a/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift b/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift index df709b53f5b5..5098d2d03786 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift @@ -56,6 +56,8 @@ public struct BlockedStateErrorMapper: BlockedStateErrorMapperProtocol { .multihopEntryEqualsExit case .noDaitaRelaysFound: .noRelaysSatisfyingDaitaConstraints + case .noObfuscatedRelaysFound: + .noRelaysSatisfyingObfuscationSettings default: .noRelaysSatisfyingConstraints } diff --git a/ios/PacketTunnelCore/Actor/State+Extensions.swift b/ios/PacketTunnelCore/Actor/State+Extensions.swift index a1e2c18d11bd..7dbecf2a523c 100644 --- a/ios/PacketTunnelCore/Actor/State+Extensions.swift +++ b/ios/PacketTunnelCore/Actor/State+Extensions.swift @@ -204,7 +204,7 @@ extension BlockedStateReason { case .deviceLocked, .tunnelAdapter: return true case .noRelaysSatisfyingConstraints, .noRelaysSatisfyingFilterConstraints, - .multihopEntryEqualsExit, + .multihopEntryEqualsExit, .noRelaysSatisfyingObfuscationSettings, .noRelaysSatisfyingDaitaConstraints, .readSettings, .invalidAccount, .accountExpired, .deviceRevoked, .unknown, .deviceLoggedOut, .outdatedSchema, .invalidRelayPublicKey: return false diff --git a/ios/PacketTunnelCore/Actor/State.swift b/ios/PacketTunnelCore/Actor/State.swift index 10a28b5a2486..88b69b048516 100644 --- a/ios/PacketTunnelCore/Actor/State.swift +++ b/ios/PacketTunnelCore/Actor/State.swift @@ -207,6 +207,9 @@ public enum BlockedStateReason: String, Codable, Equatable { /// No relays satisfying DAITA constraints. case noRelaysSatisfyingDaitaConstraints + /// No relays satisfying DAITA constraints. + case noRelaysSatisfyingObfuscationSettings + /// Any other failure when reading settings. case readSettings