diff options
| author | Jon Petersson <jon.petersson@mullvad.net> | 2025-10-15 13:51:51 +0200 |
|---|---|---|
| committer | Jon Petersson <jon.petersson@mullvad.net> | 2025-10-15 13:51:51 +0200 |
| commit | 550d41448649ba8238acb0640681cecd7f08f5e0 (patch) | |
| tree | a29e64f9859114c3b6e90ab47115fed67881f8a8 | |
| parent | 9f118a6215493105c9ad66283e5c647e7b1b4d4f (diff) | |
| parent | 7b03506e3a33f7af7d2ee86abfbd06b6f799168a (diff) | |
| download | mullvadvpn-550d41448649ba8238acb0640681cecd7f08f5e0.tar.xz mullvadvpn-550d41448649ba8238acb0640681cecd7f08f5e0.zip | |
Merge branch 'enable-quantum-resistant-tunnels-by-default-ios-1336'
| -rw-r--r-- | ios/CHANGELOG.md | 3 | ||||
| -rw-r--r-- | ios/MullvadMockData/Extensions/TimeInterval+Timeout.swift | 2 | ||||
| -rw-r--r-- | ios/MullvadSettings/QuantumResistanceSettings.swift | 2 | ||||
| -rw-r--r-- | ios/MullvadVPNTests/MullvadSettings/TunnelSettingsUpdateTests.swift | 11 | ||||
| -rw-r--r-- | ios/MullvadVPNUITests/Pages/TunnelControlPage.swift | 14 | ||||
| -rw-r--r-- | ios/MullvadVPNUITests/RelayTests.swift | 69 | ||||
| -rw-r--r-- | ios/MullvadVPNUITests/Screenshots/ScreenshotTests.swift | 14 | ||||
| -rw-r--r-- | ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift | 4 | ||||
| -rw-r--r-- | ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift | 57 |
9 files changed, 129 insertions, 47 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index e403a792f1..76c5ba8e1a 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -23,6 +23,9 @@ Line wrap the file at 100 chars. Th ## UNRELEASED +### Changed +- Quantum-resistant tunnel setting is now on by default through the "Automatic" setting. + ### Fixed - Fix IP overrides breaking certain obfuscation methods. diff --git a/ios/MullvadMockData/Extensions/TimeInterval+Timeout.swift b/ios/MullvadMockData/Extensions/TimeInterval+Timeout.swift index 7d9d05781d..83989671f6 100644 --- a/ios/MullvadMockData/Extensions/TimeInterval+Timeout.swift +++ b/ios/MullvadMockData/Extensions/TimeInterval+Timeout.swift @@ -8,7 +8,7 @@ extension TimeInterval { struct UnitTest { - static let timeout: TimeInterval = 60 + static let timeout: TimeInterval = 10 static let invertedTimeout: TimeInterval = 0.5 } } diff --git a/ios/MullvadSettings/QuantumResistanceSettings.swift b/ios/MullvadSettings/QuantumResistanceSettings.swift index 3ec2289b12..9b06c38987 100644 --- a/ios/MullvadSettings/QuantumResistanceSettings.swift +++ b/ios/MullvadSettings/QuantumResistanceSettings.swift @@ -17,6 +17,6 @@ public enum TunnelQuantumResistance: Codable, Sendable { public extension TunnelQuantumResistance { /// A single source of truth for whether the current state counts as on var isEnabled: Bool { - self == .on + [.on, .automatic].contains(self) } } diff --git a/ios/MullvadVPNTests/MullvadSettings/TunnelSettingsUpdateTests.swift b/ios/MullvadVPNTests/MullvadSettings/TunnelSettingsUpdateTests.swift index 3f33450767..672ee64a9e 100644 --- a/ios/MullvadVPNTests/MullvadSettings/TunnelSettingsUpdateTests.swift +++ b/ios/MullvadVPNTests/MullvadSettings/TunnelSettingsUpdateTests.swift @@ -112,11 +112,18 @@ final class TunnelSettingsUpdateTests: XCTestCase { var settings = LatestTunnelSettings() // When: - let update = TunnelSettingsUpdate.quantumResistance(.on) + var update = TunnelSettingsUpdate.quantumResistance(.on) update.apply(to: &settings) // Then: - XCTAssertEqual(settings.tunnelQuantumResistance, .on) + XCTAssertTrue(settings.tunnelQuantumResistance.isEnabled) + + // When again: + update = TunnelSettingsUpdate.quantumResistance(.automatic) + update.apply(to: &settings) + + // Then again: + XCTAssertTrue(settings.tunnelQuantumResistance.isEnabled) } func testApplyMultihop() { diff --git a/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift b/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift index 05aa3cd08f..33fc4f56c3 100644 --- a/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift +++ b/ios/MullvadVPNUITests/Pages/TunnelControlPage.swift @@ -204,7 +204,7 @@ class TunnelControlPage: Page { return self } - /// Verify that the app attempts to connect over Multihop. + /// Verify that the app does not attempt to connect over Multihop. @discardableResult func verifyNotConnectingOverMultihop() -> Self { XCTAssertFalse(app.buttons["Multihop"].exists) return self @@ -222,6 +222,18 @@ class TunnelControlPage: Page { return self } + /// Verify that the app attempts to connect using quantum resistance. + @discardableResult func verifyConnectingUsingQuantumResistance() -> Self { + XCTAssertTrue(app.buttons["Quantum resistance"].exists) + return self + } + + /// Verify that the app does not attempt to connect using quantum resistance. + @discardableResult func verifyNotConnectingUsingQuantumResistance() -> Self { + XCTAssertFalse(app.buttons["Quantum resistance"].exists) + return self + } + func getInIPAddressAndPortFromConnectionStatus() -> (String, Int) { let inAddressRow = app.staticTexts[.connectionPanelInAddressRow] // The row looks like this "85.203.53.145:43030 UDP" diff --git a/ios/MullvadVPNUITests/RelayTests.swift b/ios/MullvadVPNUITests/RelayTests.swift index 44bf3a24ea..bee1dbe0bd 100644 --- a/ios/MullvadVPNUITests/RelayTests.swift +++ b/ios/MullvadVPNUITests/RelayTests.swift @@ -581,20 +581,6 @@ class RelayTests: LoggedInWithTimeUITestCase { XCTAssertTrue(meanPacketSizeWithDaita > meanPacketSizeWithoutDaita) } - private func disableDaitaInTeardown() throws { - // Undo enabling DAITA in teardown - addTeardownBlock { - HeaderBar(self.app) - .tapSettingsButton() - - SettingsPage(self.app) - .tapDAITACell() - - DAITAPage(self.app) - .tapEnableSwitchIfOn() - } - } - func testMultihopSettings() throws { // Undo enabling Multihop in teardown addTeardownBlock { @@ -669,9 +655,64 @@ class RelayTests: LoggedInWithTimeUITestCase { try Networking.verifyDNSServerProvider(dnsServerProviderName, isMullvad: false) } + + func testQuantumResistanceSettings() throws { + addTeardownBlock { + HeaderBar(self.app) + .tapSettingsButton() + + SettingsPage(self.app) + .tapVPNSettingsCell() + + VPNSettingsPage(self.app) + .tapQuantumResistantTunnelExpandButton() + .tapQuantumResistantTunnelAutomaticCell() + } + + TunnelControlPage(app) + .tapConnectButton() + + allowAddVPNConfigurationsIfAsked() + + TunnelControlPage(app) + .verifyConnectingUsingQuantumResistance() + + HeaderBar(app) + .tapSettingsButton() + + SettingsPage(app) + .tapVPNSettingsCell() + + VPNSettingsPage(app) + .tapQuantumResistantTunnelExpandButton() + .tapQuantumResistantTunnelOffCell() + .tapBackButton() + + SettingsPage(app) + .tapDoneButton() + + TunnelControlPage(app) + .waitForConnectedLabel() + .verifyNotConnectingUsingQuantumResistance() + .tapDisconnectButton() + } } extension RelayTests { + private func disableDaitaInTeardown() throws { + // Undo enabling DAITA in teardown + addTeardownBlock { + HeaderBar(self.app) + .tapSettingsButton() + + SettingsPage(self.app) + .tapDAITACell() + + DAITAPage(self.app) + .tapEnableSwitchIfOn() + } + } + /// Connect to a relay in the default country and city, get name and IP address of the relay the app successfully connects to. Assumes user is logged on and at tunnel control page. private func getDefaultRelayInfo() -> RelayInfo { TunnelControlPage(app) diff --git a/ios/MullvadVPNUITests/Screenshots/ScreenshotTests.swift b/ios/MullvadVPNUITests/Screenshots/ScreenshotTests.swift index e736da1d75..2b7a51cb9b 100644 --- a/ios/MullvadVPNUITests/Screenshots/ScreenshotTests.swift +++ b/ios/MullvadVPNUITests/Screenshots/ScreenshotTests.swift @@ -29,20 +29,6 @@ class ScreenshotTests: LoggedInWithTimeUITestCase { app.terminate() app.launch() - HeaderBar(app) - .tapSettingsButton() - - SettingsPage(app) - .tapVPNSettingsCell() - - VPNSettingsPage(app) - .tapQuantumResistantTunnelExpandButton() - .tapQuantumResistantTunnelOnCell() - .tapBackButton() - - SettingsPage(app) - .tapDoneButton() - TunnelControlPage(app) .tapSelectLocationButton() diff --git a/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift b/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift index d3778a9be4..0f78fa7c51 100644 --- a/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift +++ b/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift @@ -43,7 +43,7 @@ extension SettingsReaderStub { } } - static func postQuantumConfiguration() -> SettingsReaderStub { + static func noPostQuantumConfiguration() -> SettingsReaderStub { let staticSettings = Settings( privateKey: PrivateKey(), interfaceAddresses: [IPAddressRange(from: "127.0.0.1/32")!], @@ -51,7 +51,7 @@ extension SettingsReaderStub { relayConstraints: RelayConstraints(), dnsSettings: DNSSettings(), wireGuardObfuscation: WireGuardObfuscationSettings(state: .off), - tunnelQuantumResistance: .on, + tunnelQuantumResistance: .off, tunnelMultihopState: .off, daita: DAITASettings() ) diff --git a/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift index 0bacee4903..8206985c64 100644 --- a/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift +++ b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift @@ -34,12 +34,18 @@ final class PacketTunnelActorTests: XCTestCase { let actor = PacketTunnelActor.mock() // As actor starts it should transition through the following states based on simulation: - // .initial → .connecting → .connected + // .initial → .negotiatingEphemeralPeer -> .connecting → .connected let initialStateExpectation = expectation(description: "Expect initial state") + let negotiatingPeerExpectation = expectation(description: "Expect peer negotiation") let connectingExpectation = expectation(description: "Expect connecting state") let connectedStateExpectation = expectation(description: "Expect connected state") - let allExpectations = [initialStateExpectation, connectingExpectation, connectedStateExpectation] + let allExpectations = [ + initialStateExpectation, + negotiatingPeerExpectation, + connectingExpectation, + connectedStateExpectation, + ] stateSink = await actor.$observedState .receive(on: DispatchQueue.main) @@ -47,6 +53,9 @@ final class PacketTunnelActorTests: XCTestCase { switch newState { case .initial: initialStateExpectation.fulfill() + case .negotiatingEphemeralPeer: + negotiatingPeerExpectation.fulfill() + actor.notifyEphemeralPeerNegotiated() case .connecting: connectingExpectation.fulfill() case .connected: @@ -65,12 +74,18 @@ final class PacketTunnelActorTests: XCTestCase { let actor = PacketTunnelActor.mock() // As actor starts it should transition through the following states based on simulation: - // .initial → .connecting → .connected + // .initial → .negotiatingEphemeralPeer -> .connecting → .connected let initialStateExpectation = expectation(description: "Expect initial state") + let negotiatingPeerExpectation = expectation(description: "Expect peer negotiation") let connectingExpectation = expectation(description: "Expect connecting state") let connectedStateExpectation = expectation(description: "Expect connected state") - let allExpectations = [initialStateExpectation, connectingExpectation, connectedStateExpectation] + let allExpectations = [ + initialStateExpectation, + negotiatingPeerExpectation, + connectingExpectation, + connectedStateExpectation, + ] stateSink = await actor.$observedState .receive(on: DispatchQueue.main) @@ -78,6 +93,9 @@ final class PacketTunnelActorTests: XCTestCase { switch newState { case .initial: initialStateExpectation.fulfill() + case .negotiatingEphemeralPeer: + negotiatingPeerExpectation.fulfill() + actor.notifyEphemeralPeerNegotiated() case .connecting: connectingExpectation.fulfill() case .connected: @@ -99,7 +117,10 @@ final class PacketTunnelActorTests: XCTestCase { */ func testConnectionAttemptTransition() async throws { let tunnelMonitor = TunnelMonitorStub { _, _ in } - let actor = PacketTunnelActor.mock(tunnelMonitor: tunnelMonitor) + let actor = PacketTunnelActor.mock( + tunnelMonitor: tunnelMonitor, + settingsReader: SettingsReaderStub.noPostQuantumConfiguration() + ) let connectingStateExpectation = expectation(description: "Expect connecting state") connectingStateExpectation.expectedFulfillmentCount = 5 var nextAttemptCount: UInt = 0 @@ -127,10 +148,7 @@ final class PacketTunnelActorTests: XCTestCase { func testPostQuantumReconnectionTransition() async throws { let tunnelMonitor = TunnelMonitorStub { _, _ in } - let actor = PacketTunnelActor.mock( - tunnelMonitor: tunnelMonitor, - settingsReader: SettingsReaderStub.postQuantumConfiguration() - ) + let actor = PacketTunnelActor.mock(tunnelMonitor: tunnelMonitor) let negotiatingPostQuantumKeyStateExpectation = expectation(description: "Expect post quantum state") negotiatingPostQuantumKeyStateExpectation.expectedFulfillmentCount = 5 var nextAttemptCount: UInt = 0 @@ -162,7 +180,10 @@ final class PacketTunnelActorTests: XCTestCase { */ func testReconnectionAttemptTransition() async throws { let tunnelMonitor = TunnelMonitorStub { _, _ in } - let actor = PacketTunnelActor.mock(tunnelMonitor: tunnelMonitor) + let actor = PacketTunnelActor.mock( + tunnelMonitor: tunnelMonitor, + settingsReader: SettingsReaderStub.noPostQuantumConfiguration() + ) let connectingStateExpectation = expectation(description: "Expect connecting state") let connectedStateExpectation = expectation(description: "Expect connected state") let reconnectingStateExpectation = expectation(description: "Expect reconnecting state") @@ -207,16 +228,18 @@ final class PacketTunnelActorTests: XCTestCase { 1. The first attempt to read settings yields an error indicating that device is locked. 2. An actor should set up a task to reconnect the tunnel periodically. 3. The issue goes away on the second attempt to read settings. - 4. An actor should transition through `.connecting` towards`.connected` state. + 4. An actor should transition through `.negotiatingEphemeralPeer` towards`.connected` state. */ func testLockedDeviceErrorOnBoot() async throws { let initialStateExpectation = expectation(description: "Expect initial state") let errorStateExpectation = expectation(description: "Expect error state") + let negotiatingPeerExpectation = expectation(description: "Expect peer negotiation") let connectingStateExpectation = expectation(description: "Expect connecting state") let connectedStateExpectation = expectation(description: "Expect connected state") let allExpectations = [ initialStateExpectation, errorStateExpectation, + negotiatingPeerExpectation, connectingStateExpectation, connectedStateExpectation, ] @@ -252,6 +275,9 @@ final class PacketTunnelActorTests: XCTestCase { initialStateExpectation.fulfill() case .error: errorStateExpectation.fulfill() + case .negotiatingEphemeralPeer: + negotiatingPeerExpectation.fulfill() + actor.notifyEphemeralPeerNegotiated() case .connecting: connectingStateExpectation.fulfill() case .connected: @@ -279,6 +305,7 @@ final class PacketTunnelActorTests: XCTestCase { // Wait for the connected state to happen so it doesn't get coalesced immediately after the call to `actor.stop` actor.start(options: launchOptions) + actor.notifyEphemeralPeerNegotiated() await fulfillment(of: [connectedStateExpectation], timeout: .UnitTest.timeout) await expect(.disconnected, on: actor) { @@ -321,6 +348,7 @@ final class PacketTunnelActorTests: XCTestCase { connectingStateExpectation.fulfill() } actor.start(options: launchOptions) + actor.notifyEphemeralPeerNegotiated() await fulfillment(of: [connectingStateExpectation], timeout: .UnitTest.timeout) stateSink = await actor.$observedState @@ -372,6 +400,7 @@ final class PacketTunnelActorTests: XCTestCase { } actor.start(options: launchOptions) + actor.notifyEphemeralPeerNegotiated() // Wait for the connected state to happen so it doesn't get coalesced immediately after the call to `actor.stop` await fulfillment(of: [connectedStateExpectation], timeout: .UnitTest.timeout) @@ -411,6 +440,7 @@ final class PacketTunnelActorTests: XCTestCase { connectedExpectation.fulfill() } actor.start(options: launchOptions) + actor.notifyEphemeralPeerNegotiated() await fulfillment(of: [connectedExpectation], timeout: .UnitTest.timeout) // Cancel the state sink to avoid overfulfilling the connected expectation @@ -434,7 +464,10 @@ final class PacketTunnelActorTests: XCTestCase { } } - let actor = PacketTunnelActor.mock(blockedStateErrorMapper: blockedStateMapper) + let actor = PacketTunnelActor.mock( + blockedStateErrorMapper: blockedStateMapper, + settingsReader: SettingsReaderStub.noPostQuantumConfiguration() + ) actor.start(options: launchOptions) |
