diff options
| author | Bug Magnet <marco.nikic@mullvad.net> | 2025-09-16 08:59:04 +0200 |
|---|---|---|
| committer | Bug Magnet <marco.nikic@mullvad.net> | 2025-09-16 09:19:29 +0200 |
| commit | 6856500e2e84da7737c9f3c75a8299a1cda5a846 (patch) | |
| tree | f8f7642eb227264b0fef1fe10da8d2d9516808ed | |
| parent | f49be85049222834a1c63d0385723f15cf56f9e9 (diff) | |
| download | mullvadvpn-6856500e2e84da7737c9f3c75a8299a1cda5a846.tar.xz mullvadvpn-6856500e2e84da7737c9f3c75a8299a1cda5a846.zip | |
Merge branch 'fix-quic-relay-selector-invalid-constraint'
25 files changed, 353 insertions, 144 deletions
diff --git a/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift b/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift index d7333dc915..f43e75b8ac 100644 --- a/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift +++ b/ios/MullvadMockData/MullvadREST/RelaySelectorStub.swift @@ -70,7 +70,8 @@ extension RelaySelectorStub { return SelectedRelays( entry: cityRelay, exit: cityRelay, - retryAttempt: 0 + retryAttempt: 0, + obfuscation: .off ) }, candidatesResult: nil) } diff --git a/ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift b/ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift index 57576da35c..51e72eaf9c 100644 --- a/ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift +++ b/ios/MullvadMockData/MullvadREST/SelectedRelaysStub+Stubs.swift @@ -32,6 +32,6 @@ public struct SelectedRelaysStub { ), features: nil ), - retryAttempt: 0 + retryAttempt: 0, obfuscation: .off ) } diff --git a/ios/MullvadMockData/MullvadREST/ServerRelaysResponse+Stubs.swift b/ios/MullvadMockData/MullvadREST/ServerRelaysResponse+Stubs.swift index 6bd66979b9..3f85e17651 100644 --- a/ios/MullvadMockData/MullvadREST/ServerRelaysResponse+Stubs.swift +++ b/ios/MullvadMockData/MullvadREST/ServerRelaysResponse+Stubs.swift @@ -150,7 +150,7 @@ public enum ServerRelaysResponseStubs { ipv6AddrIn: .loopback, publicKey: PrivateKey().publicKey.rawValue, includeInCountry: true, - daita: false, + daita: true, shadowsocksExtraAddrIn: ["0.0.0.0"], features: nil ), diff --git a/ios/MullvadREST/Relay/MultihopDecisionFlow.swift b/ios/MullvadREST/Relay/MultihopDecisionFlow.swift index dcb4106b95..9046f44dd5 100644 --- a/ios/MullvadREST/Relay/MultihopDecisionFlow.swift +++ b/ios/MullvadREST/Relay/MultihopDecisionFlow.swift @@ -55,7 +55,12 @@ struct OneToOne: MultihopDecisionFlow { useObfuscatedPortIfAvailable: true ) - return SelectedRelays(entry: entryMatch, exit: exitMatch, retryAttempt: relayPicker.connectionAttemptCount) + return SelectedRelays( + entry: entryMatch, + exit: exitMatch, + retryAttempt: relayPicker.connectionAttemptCount, + obfuscation: relayPicker.obfuscation.method + ) } func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { @@ -99,7 +104,12 @@ struct OneToMany: MultihopDecisionFlow { useObfuscatedPortIfAvailable: false ) - return SelectedRelays(entry: entryMatch, exit: exitMatch, retryAttempt: relayPicker.connectionAttemptCount) + return SelectedRelays( + entry: entryMatch, + exit: exitMatch, + retryAttempt: relayPicker.connectionAttemptCount, + obfuscation: relayPicker.obfuscation.method + ) } func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { @@ -144,7 +154,12 @@ struct ManyToOne: MultihopDecisionFlow { useObfuscatedPortIfAvailable: true ) - return SelectedRelays(entry: entryMatch, exit: exitMatch, retryAttempt: relayPicker.connectionAttemptCount) + return SelectedRelays( + entry: entryMatch, + exit: exitMatch, + retryAttempt: relayPicker.connectionAttemptCount, + obfuscation: relayPicker.obfuscation.method + ) } func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { @@ -189,7 +204,12 @@ struct ManyToMany: MultihopDecisionFlow { useObfuscatedPortIfAvailable: true ) - return SelectedRelays(entry: entryMatch, exit: exitMatch, retryAttempt: relayPicker.connectionAttemptCount) + return SelectedRelays( + entry: entryMatch, + exit: exitMatch, + retryAttempt: relayPicker.connectionAttemptCount, + obfuscation: relayPicker.obfuscation.method + ) } func canHandle(entryCandidates: [RelayCandidate], exitCandidates: [RelayCandidate]) -> Bool { diff --git a/ios/MullvadREST/Relay/Obfuscation/RelayObfuscator.swift b/ios/MullvadREST/Relay/Obfuscation/RelayObfuscator.swift index 41c2d418dd..7abd47f30b 100644 --- a/ios/MullvadREST/Relay/Obfuscation/RelayObfuscator.swift +++ b/ios/MullvadREST/Relay/Obfuscation/RelayObfuscator.swift @@ -27,11 +27,13 @@ struct RelayObfuscator: RelayObfuscating { let relays: REST.ServerRelaysResponse let tunnelSettings: LatestTunnelSettings let connectionAttemptCount: UInt + let obfuscationBypass: any ObfuscationProviding - func obfuscate() throws -> RelayObfuscation { + func obfuscate() -> RelayObfuscation { let obfuscationMethod = ObfuscationMethodSelector.obfuscationMethodBy( connectionAttemptCount: connectionAttemptCount, - tunnelSettings: tunnelSettings + tunnelSettings: tunnelSettings, + obfuscationBypass: obfuscationBypass ) return switch obfuscationMethod { diff --git a/ios/MullvadREST/Relay/Obfuscation/ShadowsocksObfuscator.swift b/ios/MullvadREST/Relay/Obfuscation/ShadowsocksObfuscator.swift index f196305075..10ccee7b4d 100644 --- a/ios/MullvadREST/Relay/Obfuscation/ShadowsocksObfuscator.swift +++ b/ios/MullvadREST/Relay/Obfuscation/ShadowsocksObfuscator.swift @@ -75,10 +75,7 @@ struct ShadowsocksObfuscator: RelayObfuscating { } } - guard - wireGuardObfuscation.state == .shadowsocks, - let port = shadowsockPort() - else { + guard let port = shadowsockPort() else { return tunnelSettings.relayConstraints.port } diff --git a/ios/MullvadREST/Relay/ObfuscationMethodSelector.swift b/ios/MullvadREST/Relay/ObfuscationMethodSelector.swift index 73944a8093..166c955147 100644 --- a/ios/MullvadREST/Relay/ObfuscationMethodSelector.swift +++ b/ios/MullvadREST/Relay/ObfuscationMethodSelector.swift @@ -7,6 +7,11 @@ // import MullvadSettings +import MullvadTypes + +public protocol ObfuscationProviding { + func bypassUnsupportedObfuscation(_: WireGuardObfuscationState) -> WireGuardObfuscationState +} public struct ObfuscationMethodSelector { /// This retry logic used is explained at the following link: @@ -15,20 +20,64 @@ public struct ObfuscationMethodSelector { /// - Note: This method should never return `.automatic`. public static func obfuscationMethodBy( connectionAttemptCount: UInt, - tunnelSettings: LatestTunnelSettings + tunnelSettings: LatestTunnelSettings, + obfuscationBypass: any ObfuscationProviding ) -> WireGuardObfuscationState { - if tunnelSettings.wireGuardObfuscation.state == .automatic { - if connectionAttemptCount.isOrdered(nth: 2, forEverySetOf: 4) { - .shadowsocks - } else if connectionAttemptCount.isOrdered(nth: 3, forEverySetOf: 4) { - .quic - } else if connectionAttemptCount.isOrdered(nth: 4, forEverySetOf: 4) { - .udpOverTcp + let selectedObfuscation: WireGuardObfuscationState = + if tunnelSettings.wireGuardObfuscation.state == .automatic { + if connectionAttemptCount.isOrdered(nth: 2, forEverySetOf: 4) { + .shadowsocks + } else if connectionAttemptCount.isOrdered(nth: 3, forEverySetOf: 4) { + .quic + } else if connectionAttemptCount.isOrdered(nth: 4, forEverySetOf: 4) { + .udpOverTcp + } else { + .off + } } else { - .off + tunnelSettings.wireGuardObfuscation.state } - } else { - tunnelSettings.wireGuardObfuscation.state + return obfuscationBypass.bypassUnsupportedObfuscation(selectedObfuscation) + } +} + +public struct UnsupportedObfuscationProvider: ObfuscationProviding { + let relayConstraint: RelayConstraint<UserSelectedRelays> + let relays: REST.ServerRelaysResponse + let filterConstraint: RelayConstraint<RelayFilter> + let daitaEnabled: Bool + + public init( + relayConstraint: RelayConstraint<UserSelectedRelays>, + relays: REST.ServerRelaysResponse, + filterConstraint: RelayConstraint<RelayFilter>, + daitaEnabled: Bool + ) { + self.relayConstraint = relayConstraint + self.relays = relays + self.filterConstraint = filterConstraint + self.daitaEnabled = daitaEnabled + } + + public func bypassUnsupportedObfuscation(_ obfuscation: WireGuardObfuscationState) -> WireGuardObfuscationState { + guard obfuscation != .off else { return .off } + do { + let candidates = try RelaySelector.WireGuard.findCandidates( + by: relayConstraint, + in: relays, + filterConstraint: filterConstraint, + daitaEnabled: daitaEnabled + ) + return candidates.isEmpty ? .udpOverTcp : obfuscation + } catch { + return .udpOverTcp } } } + +public struct IdentityObfuscationProvider: ObfuscationProviding { + public init() {} + public func bypassUnsupportedObfuscation(_ obfuscation: WireGuardObfuscationState) -> WireGuardObfuscationState { + obfuscation + } +} diff --git a/ios/MullvadREST/Relay/RelayPicking/MultihopPicker.swift b/ios/MullvadREST/Relay/RelayPicking/MultihopPicker.swift index 0dad74797f..f63bc25ba3 100644 --- a/ios/MullvadREST/Relay/RelayPicking/MultihopPicker.swift +++ b/ios/MullvadREST/Relay/RelayPicking/MultihopPicker.swift @@ -15,6 +15,43 @@ struct MultihopPicker: RelayPicking { let connectionAttemptCount: UInt func pick() throws -> SelectedRelays { + let constraints = tunnelSettings.relayConstraints + let daitaSettings = tunnelSettings.daita + + // Guarantee that the entry relay supports selected obfuscation + let obfuscationBypass = UnsupportedObfuscationProvider( + relayConstraint: constraints.entryLocations, + relays: obfuscation.obfuscatedRelays, + filterConstraint: constraints.filter, + daitaEnabled: daitaSettings.daitaState.isEnabled + ) + + let supportedObfuscation = RelayObfuscator( + relays: obfuscation.allRelays, + tunnelSettings: tunnelSettings, + connectionAttemptCount: connectionAttemptCount, + obfuscationBypass: obfuscationBypass + ).obfuscate() + + let entryCandidates = try RelaySelector.WireGuard.findCandidates( + by: daitaSettings.isAutomaticRouting ? .any : constraints.entryLocations, + in: supportedObfuscation.obfuscatedRelays, + filterConstraint: constraints.filter, + daitaEnabled: daitaSettings.daitaState.isEnabled + ) + + let exitCandidates = try RelaySelector.WireGuard.findCandidates( + by: constraints.exitLocations, + in: supportedObfuscation.allRelays, + filterConstraint: constraints.filter, + daitaEnabled: false + ) + + let picker = MultihopPicker( + obfuscation: supportedObfuscation, + tunnelSettings: tunnelSettings, + connectionAttemptCount: connectionAttemptCount + ) /* Relay selection is prioritised in the following order: 1. Both entry and exit constraints match only a single relay. Both relays are selected. @@ -30,30 +67,13 @@ struct MultihopPicker: RelayPicking { next: ManyToOne( next: ManyToMany( next: nil, - relayPicker: self + relayPicker: picker ), - relayPicker: self + relayPicker: picker ), - relayPicker: self + relayPicker: picker ), - relayPicker: self - ) - - let constraints = tunnelSettings.relayConstraints - let daitaSettings = tunnelSettings.daita - - 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 + relayPicker: picker ) return try decisionFlow.pick( diff --git a/ios/MullvadREST/Relay/RelayPicking/SinglehopPicker.swift b/ios/MullvadREST/Relay/RelayPicking/SinglehopPicker.swift index 4ce8518cb0..144a1bb50c 100644 --- a/ios/MullvadREST/Relay/RelayPicking/SinglehopPicker.swift +++ b/ios/MullvadREST/Relay/RelayPicking/SinglehopPicker.swift @@ -32,15 +32,38 @@ struct SinglehopPicker: RelayPicking { } } - private func pick(from exitRelays: REST.ServerRelaysResponse) throws -> SelectedRelays { + private func pick(from obfuscatedRelays: REST.ServerRelaysResponse) throws -> SelectedRelays { + let constraints = tunnelSettings.relayConstraints + let daitaSettings = tunnelSettings.daita + + // Guarantee that the chosen relay supports selected obfuscation + let obfuscationBypass = UnsupportedObfuscationProvider( + relayConstraint: constraints.exitLocations, + relays: obfuscatedRelays, + filterConstraint: constraints.filter, + daitaEnabled: daitaSettings.daitaState.isEnabled + ) + + let supportedObfuscation = RelayObfuscator( + relays: obfuscation.allRelays, + tunnelSettings: tunnelSettings, + connectionAttemptCount: connectionAttemptCount, + obfuscationBypass: obfuscationBypass + ).obfuscate() + let exitCandidates = try RelaySelector.WireGuard.findCandidates( by: tunnelSettings.relayConstraints.exitLocations, - in: exitRelays, - filterConstraint: tunnelSettings.relayConstraints.filter, - daitaEnabled: tunnelSettings.daita.daitaState.isEnabled + in: supportedObfuscation.obfuscatedRelays, + filterConstraint: constraints.filter, + daitaEnabled: daitaSettings.daitaState.isEnabled ) let match = try findBestMatch(from: exitCandidates, useObfuscatedPortIfAvailable: true) - return SelectedRelays(entry: nil, exit: match, retryAttempt: connectionAttemptCount) + return SelectedRelays( + entry: nil, + exit: match, + retryAttempt: connectionAttemptCount, + obfuscation: supportedObfuscation.method + ) } } diff --git a/ios/MullvadREST/Relay/RelaySelectorProtocol.swift b/ios/MullvadREST/Relay/RelaySelectorProtocol.swift index 174ff9ccb1..f400d81e86 100644 --- a/ios/MullvadREST/Relay/RelaySelectorProtocol.swift +++ b/ios/MullvadREST/Relay/RelaySelectorProtocol.swift @@ -55,21 +55,28 @@ public struct SelectedRelays: Equatable, Codable, Sendable { public let entry: SelectedRelay? public let exit: SelectedRelay public let retryAttempt: UInt + public let obfuscation: WireGuardObfuscationState public var ingress: SelectedRelay { entry ?? exit } - public init(entry: SelectedRelay?, exit: SelectedRelay, retryAttempt: UInt) { + public init( + entry: SelectedRelay?, + exit: SelectedRelay, + retryAttempt: UInt, + obfuscation: WireGuardObfuscationState + ) { self.entry = entry self.exit = exit self.retryAttempt = retryAttempt + self.obfuscation = obfuscation } } extension SelectedRelays: CustomDebugStringConvertible { public var debugDescription: String { "Entry: \(entry?.hostname ?? "-") -> \(entry?.endpoint.ipv4Relay.description ?? "-"), " + - "Exit: \(exit.hostname) -> \(exit.endpoint.ipv4Relay.description)" + "Exit: \(exit.hostname) -> \(exit.endpoint.ipv4Relay.description), obfuscation: \(obfuscation)" } } diff --git a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift index 197259ce11..0bc0c08120 100644 --- a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift +++ b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift @@ -23,10 +23,12 @@ public final class RelaySelectorWrapper: RelaySelectorProtocol, Sendable { let relays = try relayCache.read().relays try validateWireguardCustomPort(tunnelSettings, relays: relays) - let obfuscation = try RelayObfuscator( + // Filter for obfuscation + let obfuscation = RelayObfuscator( relays: relays, tunnelSettings: tunnelSettings, - connectionAttemptCount: connectionAttemptCount + connectionAttemptCount: connectionAttemptCount, + obfuscationBypass: IdentityObfuscationProvider() ).obfuscate() return switch tunnelSettings.tunnelMultihopState { @@ -48,10 +50,11 @@ public final class RelaySelectorWrapper: RelaySelectorProtocol, Sendable { public func findCandidates(tunnelSettings: LatestTunnelSettings) throws -> RelayCandidates { let relays = try relayCache.read().relays - let obfuscation = try RelayObfuscator( + let obfuscation = RelayObfuscator( relays: relays, tunnelSettings: tunnelSettings, - connectionAttemptCount: 0 + connectionAttemptCount: 0, + obfuscationBypass: IdentityObfuscationProvider() ).obfuscate() let findCandidates: (REST.ServerRelaysResponse, Bool) throws diff --git a/ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ChipView/ChipContainerView.swift b/ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ChipView/ChipContainerView.swift index 744beededc..ff97f2ae1c 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ChipView/ChipContainerView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/ConnectionView/ChipView/ChipContainerView.swift @@ -118,7 +118,8 @@ struct ChipContainerView<ViewModel>: View where ViewModel: ChipViewModelProtocol ), features: nil ), - retryAttempt: 0 + retryAttempt: 0, + obfuscation: .off ), isPostQuantum: false, isDaita: false diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift index 286b287a5b..a8e0f27559 100644 --- a/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift +++ b/ios/MullvadVPNTests/MullvadREST/Relay/MultihopDecisionFlowTests.swift @@ -169,10 +169,10 @@ class MultihopDecisionFlowTests: XCTestCase { extension MultihopDecisionFlowTests { var picker: MultihopPicker { - let obfuscation = try? RelayObfuscator( + let obfuscation = RelayObfuscator( relays: sampleRelays, tunnelSettings: LatestTunnelSettings(), - connectionAttemptCount: 0 + connectionAttemptCount: 0, obfuscationBypass: IdentityObfuscationProvider() ).obfuscate() var tunnelSettings = LatestTunnelSettings() @@ -182,7 +182,7 @@ extension MultihopDecisionFlowTests { ) return MultihopPicker( - obfuscation: obfuscation.unsafelyUnwrapped, + obfuscation: obfuscation, tunnelSettings: tunnelSettings, connectionAttemptCount: 0 ) diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscationMethodSelectorTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscationMethodSelectorTests.swift index 3526606d72..00a961183d 100644 --- a/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscationMethodSelectorTests.swift +++ b/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscationMethodSelectorTests.swift @@ -19,7 +19,7 @@ class ObfuscationMethodSelectorTests: XCTestCase { var method = ObfuscationMethodSelector.obfuscationMethodBy( connectionAttemptCount: attempt, - tunnelSettings: tunnelSettings + tunnelSettings: tunnelSettings, obfuscationBypass: IdentityObfuscationProvider() ) XCTAssertEqual(method, .off) @@ -27,7 +27,7 @@ class ObfuscationMethodSelectorTests: XCTestCase { method = ObfuscationMethodSelector.obfuscationMethodBy( connectionAttemptCount: attempt, - tunnelSettings: tunnelSettings + tunnelSettings: tunnelSettings, obfuscationBypass: IdentityObfuscationProvider() ) if attempt.isOrdered(nth: 1, forEverySetOf: 4) { XCTAssertEqual(method, .off) @@ -43,7 +43,7 @@ class ObfuscationMethodSelectorTests: XCTestCase { var method = ObfuscationMethodSelector.obfuscationMethodBy( connectionAttemptCount: attempt, - tunnelSettings: tunnelSettings + tunnelSettings: tunnelSettings, obfuscationBypass: IdentityObfuscationProvider() ) XCTAssertEqual(method, .shadowsocks) @@ -51,7 +51,7 @@ class ObfuscationMethodSelectorTests: XCTestCase { method = ObfuscationMethodSelector.obfuscationMethodBy( connectionAttemptCount: attempt, - tunnelSettings: tunnelSettings + tunnelSettings: tunnelSettings, obfuscationBypass: IdentityObfuscationProvider() ) if attempt.isOrdered(nth: 2, forEverySetOf: 4) { XCTAssertEqual(method, .shadowsocks) @@ -67,7 +67,7 @@ class ObfuscationMethodSelectorTests: XCTestCase { var method = ObfuscationMethodSelector.obfuscationMethodBy( connectionAttemptCount: attempt, - tunnelSettings: tunnelSettings + tunnelSettings: tunnelSettings, obfuscationBypass: IdentityObfuscationProvider() ) XCTAssertEqual(method, .quic) @@ -75,7 +75,7 @@ class ObfuscationMethodSelectorTests: XCTestCase { method = ObfuscationMethodSelector.obfuscationMethodBy( connectionAttemptCount: attempt, - tunnelSettings: tunnelSettings + tunnelSettings: tunnelSettings, obfuscationBypass: IdentityObfuscationProvider() ) if attempt.isOrdered(nth: 3, forEverySetOf: 4) { XCTAssertEqual(method, .quic) @@ -91,7 +91,7 @@ class ObfuscationMethodSelectorTests: XCTestCase { var method = ObfuscationMethodSelector.obfuscationMethodBy( connectionAttemptCount: attempt, - tunnelSettings: tunnelSettings + tunnelSettings: tunnelSettings, obfuscationBypass: IdentityObfuscationProvider() ) XCTAssertEqual(method, .udpOverTcp) @@ -99,7 +99,7 @@ class ObfuscationMethodSelectorTests: XCTestCase { method = ObfuscationMethodSelector.obfuscationMethodBy( connectionAttemptCount: attempt, - tunnelSettings: tunnelSettings + tunnelSettings: tunnelSettings, obfuscationBypass: IdentityObfuscationProvider() ) if attempt.isOrdered(nth: 4, forEverySetOf: 4) { XCTAssertEqual(method, .udpOverTcp) diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelayObfuscatorTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelayObfuscatorTests.swift index df9778a2dc..0593c1a08d 100644 --- a/ios/MullvadVPNTests/MullvadREST/Relay/RelayObfuscatorTests.swift +++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelayObfuscatorTests.swift @@ -26,10 +26,10 @@ final class RelayObfuscatorTests: XCTestCase { func testObfuscateOffDoesNotChangeEndpoint() throws { tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings(state: .off) - let obfuscationResult = try RelayObfuscator( + let obfuscationResult = RelayObfuscator( relays: sampleRelays, tunnelSettings: tunnelSettings, - connectionAttemptCount: 0 + connectionAttemptCount: 0, obfuscationBypass: IdentityObfuscationProvider() ).obfuscate() XCTAssertEqual(obfuscationResult.port, defaultWireguardPort) @@ -45,10 +45,10 @@ final class RelayObfuscatorTests: XCTestCase { ) ) - let obfuscationResult = try RelayObfuscator( + let obfuscationResult = RelayObfuscator( relays: sampleRelays, tunnelSettings: settings, - connectionAttemptCount: 0 + connectionAttemptCount: 0, obfuscationBypass: IdentityObfuscationProvider() ).obfuscate() let picker = SinglehopPicker( @@ -73,10 +73,10 @@ final class RelayObfuscatorTests: XCTestCase { ) ) - let obfuscationResult = try RelayObfuscator( + let obfuscationResult = RelayObfuscator( relays: sampleRelays, tunnelSettings: settings, - connectionAttemptCount: 0 + connectionAttemptCount: 0, obfuscationBypass: IdentityObfuscationProvider() ).obfuscate() let picker = MultihopPicker( @@ -99,10 +99,10 @@ final class RelayObfuscatorTests: XCTestCase { udpOverTcpPort: .port80 ) - let obfuscationResult = try RelayObfuscator( + let obfuscationResult = RelayObfuscator( relays: sampleRelays, tunnelSettings: tunnelSettings, - connectionAttemptCount: 0 + connectionAttemptCount: 0, obfuscationBypass: IdentityObfuscationProvider() ).obfuscate() XCTAssertEqual(obfuscationResult.port, .only(80)) @@ -114,10 +114,10 @@ final class RelayObfuscatorTests: XCTestCase { udpOverTcpPort: .port5001 ) - let obfuscationResult = try RelayObfuscator( + let obfuscationResult = RelayObfuscator( relays: sampleRelays, tunnelSettings: tunnelSettings, - connectionAttemptCount: 0 + connectionAttemptCount: 0, obfuscationBypass: IdentityObfuscationProvider() ).obfuscate() XCTAssertEqual(obfuscationResult.port, .only(5001)) @@ -130,10 +130,10 @@ final class RelayObfuscatorTests: XCTestCase { ) try (0 ... 10).filter { $0.isMultiple(of: 2) }.forEach { attempt in - let obfuscationResult = try RelayObfuscator( + let obfuscationResult = RelayObfuscator( relays: sampleRelays, tunnelSettings: tunnelSettings, - connectionAttemptCount: UInt(attempt) + connectionAttemptCount: UInt(attempt), obfuscationBypass: IdentityObfuscationProvider() ).obfuscate() let validPorts: [RelayConstraint<UInt16>] = [.only(80), .only(5001)] @@ -149,10 +149,10 @@ final class RelayObfuscatorTests: XCTestCase { shadowsocksPort: .custom(5500) ) - let obfuscationResult = try RelayObfuscator( + let obfuscationResult = RelayObfuscator( relays: sampleRelays, tunnelSettings: tunnelSettings, - connectionAttemptCount: 0 + connectionAttemptCount: 0, obfuscationBypass: IdentityObfuscationProvider() ).obfuscate() XCTAssertEqual(obfuscationResult.port, .only(5500)) @@ -164,10 +164,10 @@ final class RelayObfuscatorTests: XCTestCase { shadowsocksPort: .automatic ) - let obfuscationResult = try RelayObfuscator( + let obfuscationResult = RelayObfuscator( relays: sampleRelays, tunnelSettings: tunnelSettings, - connectionAttemptCount: 0 + connectionAttemptCount: 0, obfuscationBypass: IdentityObfuscationProvider() ).obfuscate() let portRanges = RelaySelector.parseRawPortRanges(sampleRelays.wireguard.shadowsocksPortRanges) @@ -194,10 +194,10 @@ final class RelayObfuscatorTests: XCTestCase { shadowsocksPort: .custom(port) ) - let obfuscationResult = try RelayObfuscator( + let obfuscationResult = RelayObfuscator( relays: sampleRelays, tunnelSettings: tunnelSettings, - connectionAttemptCount: 0 + connectionAttemptCount: 0, obfuscationBypass: IdentityObfuscationProvider() ).obfuscate() let relaysWithExtraAddresses = sampleRelays.wireguard.relays.filter { relay in @@ -216,10 +216,10 @@ final class RelayObfuscatorTests: XCTestCase { shadowsocksPort: .custom(port) ) - let obfuscationResult = try RelayObfuscator( + let obfuscationResult = RelayObfuscator( relays: sampleRelays, tunnelSettings: tunnelSettings, - connectionAttemptCount: 0 + connectionAttemptCount: 0, obfuscationBypass: IdentityObfuscationProvider() ).obfuscate() XCTAssertEqual(obfuscationResult.obfuscatedRelays.wireguard.relays.count, sampleRelays.wireguard.relays.count) @@ -232,12 +232,32 @@ final class RelayObfuscatorTests: XCTestCase { state: .quic ) - let obfuscationResult = try RelayObfuscator( + let obfuscationResult = RelayObfuscator( relays: sampleRelays, tunnelSettings: tunnelSettings, - connectionAttemptCount: 0 + connectionAttemptCount: 0, obfuscationBypass: IdentityObfuscationProvider() ).obfuscate() XCTAssertEqual(obfuscationResult.port, defaultQuicPort) } + + // MARK: Obfuscation Bypass + + func testObfuscatorBypass() throws { + tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings(state: .quic) + + let obfuscationResult = RelayObfuscator( + relays: sampleRelays, + tunnelSettings: tunnelSettings, + connectionAttemptCount: 0, obfuscationBypass: ForceShadowsocksObfuscationBypassStub() + ).obfuscate() + + XCTAssertEqual(obfuscationResult.method, .shadowsocks) + } +} + +struct ForceShadowsocksObfuscationBypassStub: ObfuscationProviding { + func bypassUnsupportedObfuscation(_: WireGuardObfuscationState) -> WireGuardObfuscationState { + .shadowsocks + } } diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift index 6488306221..314be7a61c 100644 --- a/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift +++ b/ios/MullvadVPNTests/MullvadREST/Relay/RelayPickingTests.swift @@ -20,10 +20,11 @@ class RelayPickingTests: XCTestCase { override func setUpWithError() throws { // Default obfuscation settings to satisfy picker constructors for the tests below. - obfuscation = try RelayObfuscator( + obfuscation = RelayObfuscator( relays: sampleRelays, tunnelSettings: LatestTunnelSettings(), - connectionAttemptCount: 0 + connectionAttemptCount: 0, + obfuscationBypass: IdentityObfuscationProvider() ).obfuscate() } @@ -255,4 +256,75 @@ class RelayPickingTests: XCTestCase { XCTAssertThrowsError(try picker.pick()) } + + // DAITA - ON, Direct only - ON, Entry supports DAITA - TRUE, Entry does not support QUIC + // Shadowsocks obfuscation should be picked instead of QUIC since entry does not support it + func testMultihopCannotPickAutomaticallyInvalidObfuscation() throws { + let constraints = RelayConstraints( + entryLocations: .only(UserSelectedRelays(locations: [.hostname("us", "dal", "us-dal-wg-001")])), + exitLocations: .only(UserSelectedRelays(locations: [.hostname("se", "got", "se10-wireguard")])) + ) + + var settings = LatestTunnelSettings() + settings.relayConstraints = constraints + settings.daita = DAITASettings(daitaState: .on, directOnlyState: .on) + + // Mimic the obfuscator ran by the relay selector wrapper prior to invoking the `MultihopPicker` + obfuscation = RelayObfuscator( + relays: sampleRelays, + tunnelSettings: LatestTunnelSettings(), + connectionAttemptCount: 2, + obfuscationBypass: IdentityObfuscationProvider() + ).obfuscate() + + // It will already have pre-filtered relays to select obfuscation via QUIC because it's the 2nd connection attempt + XCTAssertEqual(obfuscation.method, .quic) + + // The `MultihopPicker` will re-roll an obfuscator to find out that QUIC is not supported for the selected entry + // It will then fallback to picking shadowsocks obfuscation instead + let picker = MultihopPicker( + obfuscation: obfuscation, + tunnelSettings: settings, + connectionAttemptCount: 2 + ) + + let selectedRelays = try picker.pick() + + XCTAssertEqual(selectedRelays.obfuscation, .udpOverTcp) + XCTAssertEqual(selectedRelays.entry?.hostname, "us-dal-wg-001") + XCTAssertEqual(selectedRelays.exit.hostname, "se10-wireguard") + } + + // DAITA - OFF, Entry does not support QUIC + // Shadowsocks obfuscation should be picked instead of QUIC since entry does not support it + func testSinglehopCannotPickAutomaticallyInvalidObfuscation() throws { + let constraints = RelayConstraints( + exitLocations: .only(UserSelectedRelays(locations: [.hostname("us", "dal", "us-dal-wg-001")])) + ) + + var settings = LatestTunnelSettings() + settings.relayConstraints = constraints + + // Mimic the obfuscator ran by the relay selector wrapper prior to invoking the `SinglehopPicker` + obfuscation = RelayObfuscator( + relays: sampleRelays, + tunnelSettings: LatestTunnelSettings(), + connectionAttemptCount: 2, + obfuscationBypass: IdentityObfuscationProvider() + ).obfuscate() + + // It will already have pre-filtered relays to select obfuscation via QUIC because it's the 2nd connection attempt + XCTAssertEqual(obfuscation.method, .quic) + + let picker = SinglehopPicker( + obfuscation: obfuscation, + tunnelSettings: settings, + connectionAttemptCount: 2 + ) + + let selectedRelays = try picker.pick() + XCTAssertEqual(selectedRelays.obfuscation, .udpOverTcp) + XCTAssertEqual(selectedRelays.entry?.hostname, nil) + XCTAssertEqual(selectedRelays.exit.hostname, "us-dal-wg-001") + } } diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor+ErrorState.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+ErrorState.swift index 46c98c122c..3c3e3e7ac7 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor+ErrorState.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+ErrorState.swift @@ -76,6 +76,9 @@ extension PacketTunnelActor { case let .reconnecting(connState): return mapConnectionState(connState, reason: reason, priorState: .reconnecting) + case let .negotiatingEphemeralPeer(connState, _): + return mapConnectionState(connState, reason: reason, priorState: .connecting) + case var .error(blockedState): if blockedState.reason != reason { blockedState.reason = reason @@ -84,8 +87,7 @@ extension PacketTunnelActor { return nil } - // Ephemeral peer exchange cannot enter the blocked state - case .disconnecting, .disconnected, .negotiatingEphemeralPeer: + case .disconnecting, .disconnected: return nil } } diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift index 7cd230e2fb..5c2f58d449 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift @@ -266,10 +266,13 @@ extension PacketTunnelActor { startDefaultPathObserver() } + let entryConfiguration = configuration.entryConfiguration + let exitConfiguration = configuration.exitConfiguration + // Daita parameters are gotten from an ephemeral peer try await tunnelAdapter.startMultihop( - entryConfiguration: configuration.entryConfiguration, - exitConfiguration: configuration.exitConfiguration, + entryConfiguration: entryConfiguration, + exitConfiguration: exitConfiguration, daita: nil ) @@ -329,6 +332,7 @@ extension PacketTunnelActor { connectionState.relayConstraints = settings.relayConstraints connectionState.connectedEndpoint = connectedRelay.endpoint connectionState.remotePort = connectedRelay.endpoint.ipv4Relay.port + connectionState.obfuscationMethod = selectedRelays.obfuscation return connectionState case var .connecting(connectionState), var .reconnecting(connectionState): @@ -347,6 +351,7 @@ extension PacketTunnelActor { connectionState.currentKey = settings.privateKey connectionState.connectedEndpoint = connectedRelay.endpoint connectionState.remotePort = connectedRelay.endpoint.ipv4Relay.port + connectionState.obfuscationMethod = selectedRelays.obfuscation return connectionState case let .error(blockedState): keyPolicy = blockedState.keyPolicy @@ -369,7 +374,7 @@ extension PacketTunnelActor { remotePort: connectedRelay.endpoint.ipv4Relay.port, isPostQuantum: settings.quantumResistance.isEnabled, isDaitaEnabled: settings.daita.daitaState.isEnabled, - obfuscationMethod: .off + obfuscationMethod: selectedRelays.obfuscation ) case .disconnecting, .disconnected: return nil @@ -395,10 +400,8 @@ extension PacketTunnelActor { let obfuscated = protocolObfuscator.obfuscate( connectionState.connectedEndpoint, - settings: settings.tunnelSettings, - retryAttempts: connectionState.selectedRelays.retryAttempt, relayFeatures: connectionState.selectedRelays.entry?.features ?? connectionState.selectedRelays.exit - .features + .features, obfuscationMethod: connectionState.obfuscationMethod ) let transportLayer = protocolObfuscator.transportLayer.map { $0 } ?? .udp diff --git a/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift b/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift index d0ede0dcb5..6d59b5bc15 100644 --- a/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift +++ b/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift @@ -20,9 +20,8 @@ public struct ProtocolObfuscationResult { public protocol ProtocolObfuscation { func obfuscate( _ endpoint: MullvadEndpoint, - settings: LatestTunnelSettings, - retryAttempts: UInt, - relayFeatures: REST.ServerRelay.Features? + relayFeatures: REST.ServerRelay.Features?, + obfuscationMethod: WireGuardObfuscationState ) -> ProtocolObfuscationResult var transportLayer: TransportLayer? { get } var remotePort: UInt16 { get } @@ -46,15 +45,9 @@ public class ProtocolObfuscator<Obfuscator: TunnelObfuscation>: ProtocolObfuscat public func obfuscate( _ endpoint: MullvadEndpoint, - settings: LatestTunnelSettings, - retryAttempts: UInt = 0, - relayFeatures: REST.ServerRelay.Features? + relayFeatures: REST.ServerRelay.Features?, + obfuscationMethod: WireGuardObfuscationState ) -> ProtocolObfuscationResult { - let obfuscationMethod = ObfuscationMethodSelector.obfuscationMethodBy( - connectionAttemptCount: retryAttempts, - tunnelSettings: settings - ) - remotePort = endpoint.ipv4Relay.port let obfuscationProtocol: TunnelObfuscationProtocol? = switch obfuscationMethod { @@ -69,8 +62,6 @@ public class ProtocolObfuscator<Obfuscator: TunnelObfuscation>: ProtocolObfuscat nil } default: - // This is fine, since ObfuscationMethodSelector.obfuscationMethodBy` above should never - // return .automatic. nil } diff --git a/ios/PacketTunnelCore/Actor/Protocols/TunnelAdapterProtocol.swift b/ios/PacketTunnelCore/Actor/Protocols/TunnelAdapterProtocol.swift index fc8e89c0d7..887cd8c960 100644 --- a/ios/PacketTunnelCore/Actor/Protocols/TunnelAdapterProtocol.swift +++ b/ios/PacketTunnelCore/Actor/Protocols/TunnelAdapterProtocol.swift @@ -44,3 +44,9 @@ public struct TunnelPeer { public var publicKey: PublicKey public var preSharedKey: PreSharedKey? } + +extension TunnelAdapterConfiguration: CustomDebugStringConvertible { + public var debugDescription: String { + "interfaceAddresses: \(interfaceAddresses) peerEndpoint: \(peer?.endpoint) allowedIPs: \(allowedIPs)" + } +} diff --git a/ios/PacketTunnelCore/Actor/State.swift b/ios/PacketTunnelCore/Actor/State.swift index 6f837fc734..2b1dfd87f7 100644 --- a/ios/PacketTunnelCore/Actor/State.swift +++ b/ios/PacketTunnelCore/Actor/State.swift @@ -156,7 +156,7 @@ extension State { public let isDaitaEnabled: Bool /// The obfuscation method in force on the connection - public let obfuscationMethod: WireGuardObfuscationState + public var obfuscationMethod: WireGuardObfuscationState } /// Data associated with error state. diff --git a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift index 527f6f49ca..ea111b9953 100644 --- a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift +++ b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift @@ -141,7 +141,8 @@ final class AppMessageHandlerTests: XCTestCase { location: match.location, features: nil ), - retryAttempt: 0 + retryAttempt: 0, + obfuscation: .off ) _ = try? await appMessageHandler.handleAppMessage( diff --git a/ios/PacketTunnelCoreTests/EphemeralPeerExchangingPipelineTests.swift b/ios/PacketTunnelCoreTests/EphemeralPeerExchangingPipelineTests.swift index 23b231f8d3..77fd9ebe6d 100644 --- a/ios/PacketTunnelCoreTests/EphemeralPeerExchangingPipelineTests.swift +++ b/ios/PacketTunnelCoreTests/EphemeralPeerExchangingPipelineTests.swift @@ -190,7 +190,12 @@ final class EphemeralPeerExchangingPipelineTests: XCTestCase { enableDaita: Bool ) -> ObservedConnectionState { ObservedConnectionState( - selectedRelays: SelectedRelays(entry: enableMultiHop ? entryRelay : nil, exit: exitRelay, retryAttempt: 0), + selectedRelays: SelectedRelays( + entry: enableMultiHop ? entryRelay : nil, + exit: exitRelay, + retryAttempt: 0, + obfuscation: .off + ), relayConstraints: relayConstraints, networkReachability: NetworkReachability.reachable, connectionAttemptCount: 0, diff --git a/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift b/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift index 4f65cb65e9..acbd721ca5 100644 --- a/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift +++ b/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift @@ -16,9 +16,8 @@ struct ProtocolObfuscationStub: ProtocolObfuscation { func obfuscate( _ endpoint: MullvadEndpoint, - settings: LatestTunnelSettings, - retryAttempts: UInt, - relayFeatures: REST.ServerRelay.Features? + relayFeatures: REST.ServerRelay.Features?, + obfuscationMethod: WireGuardObfuscationState ) -> ProtocolObfuscationResult { .init(endpoint: endpoint, method: .off) } diff --git a/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift b/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift index 1ef7dfe597..33ba532eb5 100644 --- a/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift +++ b/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift @@ -34,14 +34,14 @@ final class ProtocolObfuscatorTests: XCTestCase { func testObfuscateOffDoesNotChangeEndpoint() { let settings = settings(.off, obfuscationPort: .automatic) - let nonObfuscated = obfuscator.obfuscate(endpoint, settings: settings, relayFeatures: nil) + let nonObfuscated = obfuscator.obfuscate(endpoint, relayFeatures: nil, obfuscationMethod: .off) XCTAssertEqual(endpoint, nonObfuscated.endpoint) } func testObfuscateUdpOverTcp() throws { let settings = settings(.udpOverTcp, obfuscationPort: .automatic) - let obfuscated = obfuscator.obfuscate(endpoint, settings: settings, relayFeatures: nil) + let obfuscated = obfuscator.obfuscate(endpoint, relayFeatures: nil, obfuscationMethod: .udpOverTcp) let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) validate(obfuscated.endpoint, against: obfuscationProtocol) @@ -49,7 +49,7 @@ final class ProtocolObfuscatorTests: XCTestCase { func testObfuscateShadowsocks() throws { let settings = settings(.shadowsocks, obfuscationPort: .automatic) - let obfuscated = obfuscator.obfuscate(endpoint, settings: settings, relayFeatures: nil) + let obfuscated = obfuscator.obfuscate(endpoint, relayFeatures: nil, obfuscationMethod: .shadowsocks) let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) validate(obfuscated.endpoint, against: obfuscationProtocol) @@ -59,35 +59,22 @@ final class ProtocolObfuscatorTests: XCTestCase { let settings = settings(.quic, obfuscationPort: .automatic) let obfuscated = obfuscator.obfuscate( endpoint, - settings: settings, - relayFeatures: .init(daita: nil, quic: .init(addrIn: [], domain: "", token: "")) + relayFeatures: .init(daita: nil, quic: .init(addrIn: [], domain: "", token: "")), obfuscationMethod: .quic ) let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) validate(obfuscated.endpoint, against: obfuscationProtocol) } - func testObfuscateAutomatic() throws { - let settings = settings(.automatic, obfuscationPort: .automatic) - - try (UInt(0) ... 3).forEach { attempt in - let obfuscated = obfuscator.obfuscate( - endpoint, - settings: settings, - retryAttempts: attempt, - relayFeatures: .init(daita: nil, quic: .init(addrIn: [], domain: "", token: "")) - ) + func testObfuscateAutomaticDoesNotObfuscate() throws { + let obfuscated = obfuscator.obfuscate( + endpoint, + relayFeatures: .init(daita: nil, quic: .init(addrIn: [], domain: "", token: "")), + obfuscationMethod: .automatic + ) - switch attempt { - case 0: - XCTAssertEqual(endpoint, obfuscated.endpoint) - case 1, 2, 3: - let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) - validate(obfuscated.endpoint, against: obfuscationProtocol) - default: - XCTExpectFailure("Should not end up here, test setup is wrong") - } - } + XCTAssertEqual(endpoint, obfuscated.endpoint) + XCTAssertEqual(.off, obfuscated.method) } } |
