summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--ios/Assets/Localizable.xcstrings3
-rw-r--r--ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift4
-rw-r--r--ios/MullvadREST/Relay/Obfuscation/QuicObfuscator.swift39
-rw-r--r--ios/MullvadREST/Relay/Obfuscation/RelayObfuscator.swift65
-rw-r--r--ios/MullvadREST/Relay/Obfuscation/ShadowsocksObfuscator.swift87
-rw-r--r--ios/MullvadREST/Relay/Obfuscation/UdpOverTcpObfuscator.swift42
-rw-r--r--ios/MullvadREST/Relay/ObfuscatorPortSelector.swift186
-rw-r--r--ios/MullvadREST/Relay/RelayPicking/MultihopPicker.swift46
-rw-r--r--ios/MullvadREST/Relay/RelayPicking/RelayPicking.swift13
-rw-r--r--ios/MullvadREST/Relay/RelayPicking/SinglehopPicker.swift27
-rw-r--r--ios/MullvadREST/Relay/RelaySelector.swift2
-rw-r--r--ios/MullvadREST/Relay/RelaySelectorWrapper.swift94
-rw-r--r--ios/MullvadSettings/WireGuardObfuscationSettings.swift6
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj40
-rw-r--r--ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift5
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift15
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/RelayObfuscatorTests.swift (renamed from ios/MullvadVPNTests/MullvadREST/Relay/ObfuscatorPortSelectorTests.swift)127
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift182
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())
}
}