summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@mullvad.net>2025-10-08 13:54:23 +0200
committerJon Petersson <jon.petersson@mullvad.net>2025-10-15 13:48:39 +0200
commit7b03506e3a33f7af7d2ee86abfbd06b6f799168a (patch)
treea29e64f9859114c3b6e90ab47115fed67881f8a8
parent9f118a6215493105c9ad66283e5c647e7b1b4d4f (diff)
downloadmullvadvpn-7b03506e3a33f7af7d2ee86abfbd06b6f799168a.tar.xz
mullvadvpn-7b03506e3a33f7af7d2ee86abfbd06b6f799168a.zip
Enable quantum resistant tunnel setting by default
-rw-r--r--ios/CHANGELOG.md3
-rw-r--r--ios/MullvadMockData/Extensions/TimeInterval+Timeout.swift2
-rw-r--r--ios/MullvadSettings/QuantumResistanceSettings.swift2
-rw-r--r--ios/MullvadVPNTests/MullvadSettings/TunnelSettingsUpdateTests.swift11
-rw-r--r--ios/MullvadVPNUITests/Pages/TunnelControlPage.swift14
-rw-r--r--ios/MullvadVPNUITests/RelayTests.swift69
-rw-r--r--ios/MullvadVPNUITests/Screenshots/ScreenshotTests.swift14
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift4
-rw-r--r--ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift57
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)