summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@mullvad.net>2024-12-13 10:57:22 +0100
committerJon Petersson <jon.petersson@mullvad.net>2024-12-13 10:57:22 +0100
commitefecc8e7e4914a8dc12c55642919661206c014db (patch)
tree76032161a278997f21aa30c9431c7d2997d2ec05
parentd7201616edaeb3d5f3b90589286719112b77a7c1 (diff)
parent44185aead3faad574b2848ebb1cde3ecc50c6c3a (diff)
downloadmullvadvpn-efecc8e7e4914a8dc12c55642919661206c014db.tar.xz
mullvadvpn-efecc8e7e4914a8dc12c55642919661206c014db.zip
Merge branch 'relay-selector-doesnt-force-a-blocked-state-ios-975'
-rw-r--r--ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift3
-rw-r--r--ios/MullvadREST/Relay/ObfuscatorPortSelector.swift2
-rw-r--r--ios/MullvadREST/Relay/RelayPicking.swift176
-rw-r--r--ios/MullvadREST/Relay/RelayPicking/MultihopPicker.swift82
-rw-r--r--ios/MullvadREST/Relay/RelayPicking/RelayPicking.swift70
-rw-r--r--ios/MullvadREST/Relay/RelayPicking/SinglehopPicker.swift56
-rw-r--r--ios/MullvadREST/Relay/RelaySelector.swift2
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj18
-rw-r--r--ios/MullvadVPNTests/MullvadREST/ApiHandlers/ServerRelaysResponse+Stubs.swift4
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift39
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/BlockedStateErrorMapper.swift2
-rw-r--r--ios/PacketTunnelCore/Actor/State+Extensions.swift2
-rw-r--r--ios/PacketTunnelCore/Actor/State.swift3
13 files changed, 275 insertions, 184 deletions
diff --git a/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift b/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift
index bfed08a410..4250a7ffb6 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 4b2f2dca7f..a2922b52cd 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<UInt16>
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 5d7460abec..0000000000
--- 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<REST.ServerRelay>],
- 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<REST.ServerRelay>],
- 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 0000000000..32f4f72ac5
--- /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<REST.ServerRelay>],
+ 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 0000000000..15d8a0f6d1
--- /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<REST.ServerRelay>],
+ 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 0000000000..af891c341a
--- /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 96118e6ae5..d20f3039bc 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 36ca682853..4924105424 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 = "<group>"; };
7AF9BE942A40461100DBFEDB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = "<group>"; };
7AF9BE962A41C71F00DBFEDB /* ChipViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipViewCell.swift; sourceTree = "<group>"; };
+ 7AFBE38A2D09AAFF002335FC /* SinglehopPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SinglehopPicker.swift; sourceTree = "<group>"; };
+ 7AFBE38C2D09AB2E002335FC /* MultihopPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopPicker.swift; sourceTree = "<group>"; };
85006A8E2B73EF67004AD8FB /* MullvadVPNUITestsSmoke.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MullvadVPNUITestsSmoke.xctestplan; sourceTree = "<group>"; };
850201DA2B503D7700EF8C96 /* RelayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayTests.swift; sourceTree = "<group>"; };
850201DC2B503D8C00EF8C96 /* SelectLocationPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationPage.swift; sourceTree = "<group>"; };
@@ -4094,6 +4098,16 @@
path = RelayFilter;
sourceTree = "<group>";
};
+ 7AFBE38E2D09AB4E002335FC /* RelayPicking */ = {
+ isa = PBXGroup;
+ children = (
+ 7AFBE38C2D09AB2E002335FC /* MultihopPicker.swift */,
+ 7A3AD5002C1068A800E9AD90 /* RelayPicking.swift */,
+ 7AFBE38A2D09AAFF002335FC /* SinglehopPicker.swift */,
+ );
+ path = RelayPicking;
+ sourceTree = "<group>";
+ };
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 09511a0494..87ccc3ea50 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 39970afc25..cb0f403987 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 df709b53f5..5098d2d037 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 a1e2c18d11..7dbecf2a52 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 10a28b5a24..88b69b0485 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