summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrew Bulhak <andrew.bulhak@mullvad.net>2024-05-02 18:29:12 +0200
committerAndrew Bulhak <andrew.bulhak@mullvad.net>2024-05-07 11:10:12 +0200
commitc90da3760810da79258819b7bb225456e6084003 (patch)
treec2eb999870a21cdb018b1e855fdbda136f3b05e4
parent77a28c0357fc402d35162aa76544206bb1a51e12 (diff)
downloadmullvadvpn-ios-662-update-ui-to-indicate-pq-connection-status.tar.xz
mullvadvpn-ios-662-update-ui-to-indicate-pq-connection-status.zip
Update the UI to indicate PQ stateios-662-update-ui-to-indicate-pq-connection-status
-rw-r--r--ios/MullvadSettings/QuantumResistanceSettings.swift7
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj10
-rw-r--r--ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift6
-rw-r--r--ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift6
-rw-r--r--ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift5
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelState+UI.swift260
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelState.swift28
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift216
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift6
-rw-r--r--ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelStateTests.swift139
-rw-r--r--ios/PacketTunnelCore/Actor/ObservedState.swift8
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor.swift8
-rw-r--r--ios/PacketTunnelCore/Actor/State.swift3
13 files changed, 460 insertions, 242 deletions
diff --git a/ios/MullvadSettings/QuantumResistanceSettings.swift b/ios/MullvadSettings/QuantumResistanceSettings.swift
index b5c12ae703..956b2fd0de 100644
--- a/ios/MullvadSettings/QuantumResistanceSettings.swift
+++ b/ios/MullvadSettings/QuantumResistanceSettings.swift
@@ -13,3 +13,10 @@ public enum TunnelQuantumResistance: Codable {
case on
case off
}
+
+public extension TunnelQuantumResistance {
+ /// A single source of truth for whether the current state counts as on
+ var isEnabled: Bool {
+ self == .on
+ }
+}
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index c8fb16cb01..01a4df4ae5 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -47,6 +47,9 @@
449EBA262B975B9700DFA4EB /* PostQuantumKeyReceiving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 449EBA252B975B9700DFA4EB /* PostQuantumKeyReceiving.swift */; };
44B02E3B2BC5732D008EDF34 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44B02E3A2BC5732D008EDF34 /* LoggingTests.swift */; };
44B02E3C2BC5B8A5008EDF34 /* Bundle+ProductVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5891BF1B25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift */; };
+ 44BB5F972BE527F4002520EB /* TunnelState+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44BB5F962BE527F4002520EB /* TunnelState+UI.swift */; };
+ 44BB5F982BE527F4002520EB /* TunnelState+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44BB5F962BE527F4002520EB /* TunnelState+UI.swift */; };
+ 44BB5F9A2BE529FF002520EB /* TunnelStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44BB5F992BE529FE002520EB /* TunnelStateTests.swift */; };
44DD7D242B6CFFD70005F67F /* StartTunnelOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */; };
44DD7D272B6D18FB0005F67F /* MockTunnelInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */; };
44DD7D292B7113CA0005F67F /* MockTunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D282B7113CA0005F67F /* MockTunnel.swift */; };
@@ -1379,6 +1382,8 @@
449EB9FE2B95FF2500DFA4EB /* AccountMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMock.swift; sourceTree = "<group>"; };
449EBA252B975B9700DFA4EB /* PostQuantumKeyReceiving.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostQuantumKeyReceiving.swift; sourceTree = "<group>"; };
44B02E3A2BC5732D008EDF34 /* LoggingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggingTests.swift; sourceTree = "<group>"; };
+ 44BB5F962BE527F4002520EB /* TunnelState+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelState+UI.swift"; sourceTree = "<group>"; };
+ 44BB5F992BE529FE002520EB /* TunnelStateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelStateTests.swift; sourceTree = "<group>"; };
44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartTunnelOperationTests.swift; sourceTree = "<group>"; };
44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnelInteractor.swift; sourceTree = "<group>"; };
44DD7D282B7113CA0005F67F /* MockTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnel.swift; sourceTree = "<group>"; };
@@ -2483,6 +2488,7 @@
44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */,
44DD7D232B6CFFD70005F67F /* StartTunnelOperationTests.swift */,
A9A5F9A12ACB003D0083449F /* TunnelManagerTests.swift */,
+ 44BB5F992BE529FE002520EB /* TunnelStateTests.swift */,
A9E0317B2ACBFC7E0095D843 /* TunnelStore+Stubs.swift */,
A9E031792ACB0AE70095D843 /* UIApplication+Stubs.swift */,
58165EBD2A262CBB00688EAD /* WgKeyRotationTests.swift */,
@@ -2596,6 +2602,7 @@
5820676326E771DB00655B05 /* TunnelManagerErrors.swift */,
5823FA5326CE49F600283BF8 /* TunnelObserver.swift */,
58B93A1226C3F13600A55733 /* TunnelState.swift */,
+ 44BB5F962BE527F4002520EB /* TunnelState+UI.swift */,
5803B4B12940A48700C23744 /* TunnelStore.swift */,
5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */,
58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */,
@@ -5199,6 +5206,7 @@
A9A5F9FE2ACB05160083449F /* NotificationManager.swift in Sources */,
A9A5F9FF2ACB05160083449F /* NotificationManagerDelegate.swift in Sources */,
7A9BE5AD2B90DF2D00E2A7D0 /* AllLocationsDataSourceTests.swift in Sources */,
+ 44BB5F9A2BE529FF002520EB /* TunnelStateTests.swift in Sources */,
A900E9BE2ACC654100C95F67 /* APIProxy+Stubs.swift in Sources */,
A900E9BA2ACC5D0600C95F67 /* RESTRequestExecutor+Stubs.swift in Sources */,
A9A5FA002ACB05160083449F /* ProductsRequestOperation.swift in Sources */,
@@ -5267,6 +5275,7 @@
7A9BE5AB2B909A1700E2A7D0 /* LocationDataSourceProtocol.swift in Sources */,
A9A5FA2A2ACB05160083449F /* CoordinatesTests.swift in Sources */,
44DD7D242B6CFFD70005F67F /* StartTunnelOperationTests.swift in Sources */,
+ 44BB5F982BE527F4002520EB /* TunnelState+UI.swift in Sources */,
A9A5FA2B2ACB05160083449F /* CustomDateComponentsFormattingTests.swift in Sources */,
A9A5FA2C2ACB05160083449F /* DeviceCheckOperationTests.swift in Sources */,
A9A5FA2D2ACB05160083449F /* DurationTests.swift in Sources */,
@@ -5444,6 +5453,7 @@
7A9CCCB72A96302800DD6A34 /* RevokedCoordinator.swift in Sources */,
7A6389F82B864CDF008E77E1 /* LocationNode.swift in Sources */,
587D96742886D87C00CD8F1C /* DeviceManagementContentView.swift in Sources */,
+ 44BB5F972BE527F4002520EB /* TunnelState+UI.swift in Sources */,
7A11DD0B2A9495D400098CD8 /* AppRoutes.swift in Sources */,
5827B0902B0CAA0500CCBBA1 /* EditAccessMethodCoordinator.swift in Sources */,
5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */,
diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift
index e7bf690f87..ea5260b8af 100644
--- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift
+++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift
@@ -176,14 +176,16 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate {
guard let selectedRelay = selectedRelay else { return }
do {
+ let settings = try SettingsManager.readSettings()
observedState = .connected(
ObservedConnectionState(
selectedRelay: selectedRelay,
- relayConstraints: try SettingsManager.readSettings().relayConstraints,
+ relayConstraints: settings.relayConstraints,
networkReachability: .reachable,
connectionAttemptCount: 0,
transportLayer: .udp,
- remotePort: selectedRelay.endpoint.ipv4Relay.port
+ remotePort: selectedRelay.endpoint.ipv4Relay.port,
+ isPostQuantum: settings.tunnelQuantumResistance.isEnabled
)
)
} catch {
diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
index f92fd37e07..4957af7c52 100644
--- a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
@@ -51,11 +51,11 @@ class MapConnectionStatusOperation: AsyncOperation {
switch observedState {
case let .connected(connectionState):
return connectionState.isNetworkReachable
- ? .connected(connectionState.selectedRelay)
+ ? .connected(connectionState.selectedRelay, isPostQuantum: connectionState.isPostQuantum)
: .waitingForConnectivity(.noConnection)
case let .connecting(connectionState):
return connectionState.isNetworkReachable
- ? .connecting(connectionState.selectedRelay)
+ ? .connecting(connectionState.selectedRelay, isPostQuantum: connectionState.isPostQuantum)
: .waitingForConnectivity(.noConnection)
case let .negotiatingPostQuantumKey(connectionState, privateKey):
return connectionState.isNetworkReachable
@@ -63,7 +63,7 @@ class MapConnectionStatusOperation: AsyncOperation {
: .waitingForConnectivity(.noConnection)
case let .reconnecting(connectionState):
return connectionState.isNetworkReachable
- ? .reconnecting(connectionState.selectedRelay)
+ ? .reconnecting(connectionState.selectedRelay, isPostQuantum: connectionState.isPostQuantum)
: .waitingForConnectivity(.noConnection)
case let .error(blockedState):
return .error(blockedState.reason)
diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
index 9474a0a481..cd9e8b7a88 100644
--- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
+++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift
@@ -90,7 +90,10 @@ class StartTunnelOperation: ResultOperation<Void> {
interactor.updateTunnelStatus { tunnelStatus in
tunnelStatus = TunnelStatus()
- tunnelStatus.state = .connecting(selectedRelay)
+ tunnelStatus.state = .connecting(
+ selectedRelay,
+ isPostQuantum: interactor.settings.tunnelQuantumResistance.isEnabled
+ )
}
try tunnel.start(options: tunnelOptions.rawOptions())
diff --git a/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift b/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift
new file mode 100644
index 0000000000..090fdbfe22
--- /dev/null
+++ b/ios/MullvadVPN/TunnelManager/TunnelState+UI.swift
@@ -0,0 +1,260 @@
+//
+// TunnelState+UI.swift
+// MullvadVPN
+//
+// Created by Andrew Bulhak on 2024-05-03.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+extension TunnelState {
+ var textColorForSecureLabel: UIColor {
+ switch self {
+ case .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .negotiatingPostQuantumKey:
+ .white
+
+ case .connected:
+ .successColor
+
+ case .disconnecting, .disconnected, .pendingReconnect, .waitingForConnectivity(.noNetwork), .error:
+ .dangerColor
+ }
+ }
+
+ var shouldEnableButtons: Bool {
+ if case .waitingForConnectivity(.noNetwork) = self {
+ return false
+ }
+
+ return true
+ }
+
+ var localizedTitleForSecureLabel: String {
+ switch self {
+ case let .connecting(_, isPostQuantum), let .reconnecting(_, isPostQuantum):
+ if isPostQuantum {
+ NSLocalizedString(
+ "TUNNEL_STATE_PQ_CONNECTING",
+ tableName: "Main",
+ value: "Creating quantum secure connection",
+ comment: ""
+ )
+ } else {
+ NSLocalizedString(
+ "TUNNEL_STATE_CONNECTING",
+ tableName: "Main",
+ value: "Creating secure connection",
+ comment: ""
+ )
+ }
+
+ case .negotiatingPostQuantumKey:
+ NSLocalizedString(
+ "TUNNEL_STATE_NEGOTIATING_KEY",
+ tableName: "Main",
+ value: "Creating quantum secure connection",
+ comment: ""
+ )
+
+ case let .connected(_, isPostQuantum):
+ if isPostQuantum {
+ NSLocalizedString(
+ "TUNNEL_STATE_PQ_CONNECTED",
+ tableName: "Main",
+ value: "Quantum secure connection",
+ comment: ""
+ )
+ } else {
+ NSLocalizedString(
+ "TUNNEL_STATE_CONNECTED",
+ tableName: "Main",
+ value: "Secure connection",
+ comment: ""
+ )
+ }
+
+ case .disconnecting(.nothing):
+ NSLocalizedString(
+ "TUNNEL_STATE_DISCONNECTING",
+ tableName: "Main",
+ value: "Disconnecting",
+ comment: ""
+ )
+ case .disconnecting(.reconnect), .pendingReconnect:
+ NSLocalizedString(
+ "TUNNEL_STATE_PENDING_RECONNECT",
+ tableName: "Main",
+ value: "Reconnecting",
+ comment: ""
+ )
+
+ case .disconnected:
+ NSLocalizedString(
+ "TUNNEL_STATE_DISCONNECTED",
+ tableName: "Main",
+ value: "Unsecured connection",
+ comment: ""
+ )
+
+ case .waitingForConnectivity(.noConnection), .error:
+ NSLocalizedString(
+ "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY",
+ tableName: "Main",
+ value: "Blocked connection",
+ comment: ""
+ )
+
+ case .waitingForConnectivity(.noNetwork):
+ NSLocalizedString(
+ "TUNNEL_STATE_NO_NETWORK",
+ tableName: "Main",
+ value: "No network",
+ comment: ""
+ )
+ }
+ }
+
+ var localizedTitleForSelectLocationButton: String? {
+ switch self {
+ case .disconnecting(.reconnect), .pendingReconnect:
+ NSLocalizedString(
+ "SWITCH_LOCATION_BUTTON_TITLE",
+ tableName: "Main",
+ value: "Select location",
+ comment: ""
+ )
+
+ case .disconnected, .disconnecting(.nothing):
+ NSLocalizedString(
+ "SELECT_LOCATION_BUTTON_TITLE",
+ tableName: "Main",
+ value: "Select location",
+ comment: ""
+ )
+
+ case .connecting, .connected, .reconnecting, .waitingForConnectivity, .error:
+ NSLocalizedString(
+ "SWITCH_LOCATION_BUTTON_TITLE",
+ tableName: "Main",
+ value: "Switch location",
+ comment: ""
+ )
+
+ case .negotiatingPostQuantumKey:
+ NSLocalizedString(
+ "SWITCH_LOCATION_BUTTON_TITLE",
+ tableName: "Main",
+ value: "Switch location",
+ comment: ""
+ )
+ }
+ }
+
+ var localizedAccessibilityLabel: String {
+ switch self {
+ case let .connecting(_, isPostQuantum):
+ if isPostQuantum {
+ NSLocalizedString(
+ "TUNNEL_STATE_PQ_CONNECTING_ACCESSIBILITY_LABEL",
+ tableName: "Main",
+ value: "Creating quantum secure connection",
+ comment: ""
+ )
+ } else {
+ NSLocalizedString(
+ "TUNNEL_STATE_CONNECTING_ACCESSIBILITY_LABEL",
+ tableName: "Main",
+ value: "Creating secure connection",
+ comment: ""
+ )
+ }
+
+ // TODO: Is this correct ?
+ case .negotiatingPostQuantumKey:
+ NSLocalizedString(
+ "TUNNEL_STATE_CONNECTING_ACCESSIBILITY_LABEL",
+ tableName: "Main",
+ value: "Creating quantum secure connection",
+ comment: ""
+ )
+
+ case let .connected(tunnelInfo, isPostQuantum):
+ if isPostQuantum {
+ String(
+ format: NSLocalizedString(
+ "TUNNEL_STATE_PQ_CONNECTED_ACCESSIBILITY_LABEL",
+ tableName: "Main",
+ value: "Quantum secure connection. Connected to %@, %@",
+ comment: ""
+ ),
+ tunnelInfo.location.city,
+ tunnelInfo.location.country
+ )
+ } else {
+ String(
+ format: NSLocalizedString(
+ "TUNNEL_STATE_CONNECTED_ACCESSIBILITY_LABEL",
+ tableName: "Main",
+ value: "Secure connection. Connected to %@, %@",
+ comment: ""
+ ),
+ tunnelInfo.location.city,
+ tunnelInfo.location.country
+ )
+ }
+
+ case .disconnected:
+ NSLocalizedString(
+ "TUNNEL_STATE_DISCONNECTED_ACCESSIBILITY_LABEL",
+ tableName: "Main",
+ value: "Unsecured connection",
+ comment: ""
+ )
+
+ case let .reconnecting(tunnelInfo, _):
+ String(
+ format: NSLocalizedString(
+ "TUNNEL_STATE_RECONNECTING_ACCESSIBILITY_LABEL",
+ tableName: "Main",
+ value: "Reconnecting to %@, %@",
+ comment: ""
+ ),
+ tunnelInfo.location.city,
+ tunnelInfo.location.country
+ )
+
+ case .waitingForConnectivity(.noConnection), .error:
+ NSLocalizedString(
+ "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY_ACCESSIBILITY_LABEL",
+ tableName: "Main",
+ value: "Blocked connection",
+ comment: ""
+ )
+
+ case .waitingForConnectivity(.noNetwork):
+ NSLocalizedString(
+ "TUNNEL_STATE_NO_NETWORK_ACCESSIBILITY_LABEL",
+ tableName: "Main",
+ value: "No network",
+ comment: ""
+ )
+
+ case .disconnecting(.nothing):
+ NSLocalizedString(
+ "TUNNEL_STATE_DISCONNECTING_ACCESSIBILITY_LABEL",
+ tableName: "Main",
+ value: "Disconnecting",
+ comment: ""
+ )
+
+ case .disconnecting(.reconnect), .pendingReconnect:
+ NSLocalizedString(
+ "TUNNEL_STATE_PENDING_RECONNECT_ACCESSIBILITY_LABEL",
+ tableName: "Main",
+ value: "Reconnecting",
+ comment: ""
+ )
+ }
+ }
+}
diff --git a/ios/MullvadVPN/TunnelManager/TunnelState.swift b/ios/MullvadVPN/TunnelManager/TunnelState.swift
index 1d480fe3a5..76148bdbb8 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelState.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelState.swift
@@ -49,13 +49,13 @@ enum TunnelState: Equatable, CustomStringConvertible {
case pendingReconnect
/// Connecting the tunnel.
- case connecting(SelectedRelay?)
+ case connecting(SelectedRelay?, isPostQuantum: Bool)
/// Negotiating a key for post-quantum resistance
case negotiatingPostQuantumKey(SelectedRelay, PrivateKey)
/// Connected the tunnel
- case connected(SelectedRelay)
+ case connected(SelectedRelay, isPostQuantum: Bool)
/// Disconnecting the tunnel
case disconnecting(ActionAfterDisconnect)
@@ -68,7 +68,7 @@ enum TunnelState: Equatable, CustomStringConvertible {
/// 1. Asking the running tunnel to reconnect to new relay via IPC.
/// 2. Tunnel attempts to reconnect to new relay as the current relay appears to be
/// dysfunctional.
- case reconnecting(SelectedRelay)
+ case reconnecting(SelectedRelay, isPostQuantum: Bool)
/// Waiting for connectivity to come back up.
case waitingForConnectivity(WaitingForConnectionReason)
@@ -80,20 +80,20 @@ enum TunnelState: Equatable, CustomStringConvertible {
switch self {
case .pendingReconnect:
"pending reconnect after disconnect"
- case let .connecting(tunnelRelay):
+ case let .connecting(tunnelRelay, isPostQuantum):
if let tunnelRelay {
- "connecting to \(tunnelRelay.hostname)"
+ "connecting \(isPostQuantum ? "(PQ) " : "")to \(tunnelRelay.hostname)"
} else {
- "connecting, fetching relay"
+ "connecting\(isPostQuantum ? " (PQ)" : ""), fetching relay"
}
- case let .connected(tunnelRelay):
- "connected to \(tunnelRelay.hostname)"
+ case let .connected(tunnelRelay, isPostQuantum):
+ "connected \(isPostQuantum ? "(PQ) " : "")to \(tunnelRelay.hostname)"
case let .disconnecting(actionAfterDisconnect):
"disconnecting and then \(actionAfterDisconnect)"
case .disconnected:
"disconnected"
- case let .reconnecting(tunnelRelay):
- "reconnecting to \(tunnelRelay.hostname)"
+ case let .reconnecting(tunnelRelay, isPostQuantum):
+ "reconnecting \(isPostQuantum ? "(PQ) " : "")to \(tunnelRelay.hostname)"
case .waitingForConnectivity:
"waiting for connectivity"
case let .error(blockedStateReason):
@@ -106,20 +106,18 @@ enum TunnelState: Equatable, CustomStringConvertible {
var isSecured: Bool {
switch self {
case .reconnecting, .connecting, .connected, .waitingForConnectivity(.noConnection), .error(.accountExpired),
- .error(.deviceRevoked):
+ .error(.deviceRevoked), .negotiatingPostQuantumKey:
true
case .pendingReconnect, .disconnecting, .disconnected, .waitingForConnectivity(.noNetwork), .error:
false
- case .negotiatingPostQuantumKey:
- false
}
}
var relay: SelectedRelay? {
switch self {
- case let .connected(relay), let .reconnecting(relay), let .negotiatingPostQuantumKey(relay, _):
+ case let .connected(relay, _), let .reconnecting(relay, _), let .negotiatingPostQuantumKey(relay, _):
relay
- case let .connecting(relay):
+ case let .connecting(relay, _):
relay
case .disconnecting, .disconnected, .waitingForConnectivity, .pendingReconnect, .error:
nil
diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
index 2d348b2528..92acdb7a55 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
@@ -27,7 +27,7 @@ private enum TunnelControlActionButton {
}
final class TunnelControlView: UIView {
- private let secureLabel = makeBoldTextLabel(ofSize: 20)
+ private let secureLabel = makeBoldTextLabel(ofSize: 20, numberOfLines: 0)
private let cityLabel = makeBoldTextLabel(ofSize: 34)
private let countryLabel = makeBoldTextLabel(ofSize: 34)
@@ -420,11 +420,12 @@ final class TunnelControlView: UIView {
)
}
- private class func makeBoldTextLabel(ofSize fontSize: CGFloat) -> UILabel {
+ private class func makeBoldTextLabel(ofSize fontSize: CGFloat, numberOfLines: Int = 1) -> UILabel {
let textLabel = UILabel()
textLabel.translatesAutoresizingMaskIntoConstraints = false
textLabel.font = UIFont.boldSystemFont(ofSize: fontSize)
textLabel.textColor = .white
+ textLabel.numberOfLines = numberOfLines
return textLabel
}
@@ -452,217 +453,6 @@ final class TunnelControlView: UIView {
}
private extension TunnelState {
- var textColorForSecureLabel: UIColor {
- switch self {
- case .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .negotiatingPostQuantumKey:
- .white
-
- case .connected:
- .successColor
-
- case .disconnecting, .disconnected, .pendingReconnect, .waitingForConnectivity(.noNetwork), .error:
- .dangerColor
- }
- }
-
- var shouldEnableButtons: Bool {
- if case .waitingForConnectivity(.noNetwork) = self {
- return false
- }
-
- return true
- }
-
- var localizedTitleForSecureLabel: String {
- switch self {
- case .connecting, .reconnecting:
- NSLocalizedString(
- "TUNNEL_STATE_CONNECTING",
- tableName: "Main",
- value: "Creating secure connection",
- comment: ""
- )
-
- // TODO: Is this the correct message here ?
- case .negotiatingPostQuantumKey:
- NSLocalizedString(
- "TUNNEL_STATE_NEGOTIATING_KEY",
- tableName: "Main",
- value: "Creating quantum secure connection",
- comment: ""
- )
-
- case .connected:
- NSLocalizedString(
- "TUNNEL_STATE_CONNECTED",
- tableName: "Main",
- value: "Secure connection",
- comment: ""
- )
-
- case .disconnecting(.nothing):
- NSLocalizedString(
- "TUNNEL_STATE_DISCONNECTING",
- tableName: "Main",
- value: "Disconnecting",
- comment: ""
- )
- case .disconnecting(.reconnect), .pendingReconnect:
- NSLocalizedString(
- "TUNNEL_STATE_PENDING_RECONNECT",
- tableName: "Main",
- value: "Reconnecting",
- comment: ""
- )
-
- case .disconnected:
- NSLocalizedString(
- "TUNNEL_STATE_DISCONNECTED",
- tableName: "Main",
- value: "Unsecured connection",
- comment: ""
- )
-
- case .waitingForConnectivity(.noConnection), .error:
- NSLocalizedString(
- "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY",
- tableName: "Main",
- value: "Blocked connection",
- comment: ""
- )
-
- case .waitingForConnectivity(.noNetwork):
- NSLocalizedString(
- "TUNNEL_STATE_NO_NETWORK",
- tableName: "Main",
- value: "No network",
- comment: ""
- )
- }
- }
-
- var localizedTitleForSelectLocationButton: String? {
- switch self {
- case .disconnecting(.reconnect), .pendingReconnect:
- NSLocalizedString(
- "SWITCH_LOCATION_BUTTON_TITLE",
- tableName: "Main",
- value: "Select location",
- comment: ""
- )
-
- case .disconnected, .disconnecting(.nothing):
- NSLocalizedString(
- "SELECT_LOCATION_BUTTON_TITLE",
- tableName: "Main",
- value: "Select location",
- comment: ""
- )
-
- case .connecting, .connected, .reconnecting, .waitingForConnectivity, .error:
- NSLocalizedString(
- "SWITCH_LOCATION_BUTTON_TITLE",
- tableName: "Main",
- value: "Switch location",
- comment: ""
- )
-
- // TODO: Is this correct ?
- case .negotiatingPostQuantumKey:
- NSLocalizedString(
- "SWITCH_LOCATION_BUTTON_TITLE",
- tableName: "Main",
- value: "Switch location",
- comment: ""
- )
- }
- }
-
- var localizedAccessibilityLabel: String {
- switch self {
- case .connecting:
- NSLocalizedString(
- "TUNNEL_STATE_CONNECTING_ACCESSIBILITY_LABEL",
- tableName: "Main",
- value: "Creating secure connection",
- comment: ""
- )
-
- // TODO: Is this correct ?
- case .negotiatingPostQuantumKey:
- NSLocalizedString(
- "TUNNEL_STATE_CONNECTING_ACCESSIBILITY_LABEL",
- tableName: "Main",
- value: "Creating secure connection",
- comment: ""
- )
-
- case let .connected(tunnelInfo):
- String(
- format: NSLocalizedString(
- "TUNNEL_STATE_CONNECTED_ACCESSIBILITY_LABEL",
- tableName: "Main",
- value: "Secure connection. Connected to %@, %@",
- comment: ""
- ),
- tunnelInfo.location.city,
- tunnelInfo.location.country
- )
-
- case .disconnected:
- NSLocalizedString(
- "TUNNEL_STATE_DISCONNECTED_ACCESSIBILITY_LABEL",
- tableName: "Main",
- value: "Unsecured connection",
- comment: ""
- )
-
- case let .reconnecting(tunnelInfo):
- String(
- format: NSLocalizedString(
- "TUNNEL_STATE_RECONNECTING_ACCESSIBILITY_LABEL",
- tableName: "Main",
- value: "Reconnecting to %@, %@",
- comment: ""
- ),
- tunnelInfo.location.city,
- tunnelInfo.location.country
- )
-
- case .waitingForConnectivity(.noConnection), .error:
- NSLocalizedString(
- "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY_ACCESSIBILITY_LABEL",
- tableName: "Main",
- value: "Blocked connection",
- comment: ""
- )
-
- case .waitingForConnectivity(.noNetwork):
- NSLocalizedString(
- "TUNNEL_STATE_NO_NETWORK_ACCESSIBILITY_LABEL",
- tableName: "Main",
- value: "No network",
- comment: ""
- )
-
- case .disconnecting(.nothing):
- NSLocalizedString(
- "TUNNEL_STATE_DISCONNECTING_ACCESSIBILITY_LABEL",
- tableName: "Main",
- value: "Disconnecting",
- comment: ""
- )
-
- case .disconnecting(.reconnect), .pendingReconnect:
- NSLocalizedString(
- "TUNNEL_STATE_PENDING_RECONNECT_ACCESSIBILITY_LABEL",
- tableName: "Main",
- value: "Reconnecting",
- comment: ""
- )
- }
- }
-
func actionButtons(traitCollection: UITraitCollection) -> [TunnelControlActionButton] {
switch (traitCollection.userInterfaceIdiom, traitCollection.horizontalSizeClass) {
case (.phone, _), (.pad, .compact):
diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
index ce7b72a9f3..36f8535047 100644
--- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
+++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift
@@ -147,17 +147,17 @@ class TunnelViewController: UIViewController, RootContainment {
private func updateMap(animated: Bool) {
switch tunnelState {
- case let .connecting(tunnelRelay):
+ case let .connecting(tunnelRelay, _):
mapViewController.removeLocationMarker()
contentView.setAnimatingActivity(true)
mapViewController.setCenter(tunnelRelay?.location.geoCoordinate, animated: animated)
- case let .reconnecting(tunnelRelay), let .negotiatingPostQuantumKey(tunnelRelay, _):
+ case let .reconnecting(tunnelRelay, _), let .negotiatingPostQuantumKey(tunnelRelay, _):
mapViewController.removeLocationMarker()
contentView.setAnimatingActivity(true)
mapViewController.setCenter(tunnelRelay.location.geoCoordinate, animated: animated)
- case let .connected(tunnelRelay):
+ case let .connected(tunnelRelay, _):
let center = tunnelRelay.location.geoCoordinate
mapViewController.setCenter(center, animated: animated) {
self.contentView.setAnimatingActivity(false)
diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelStateTests.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelStateTests.swift
new file mode 100644
index 0000000000..9a707fb30d
--- /dev/null
+++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelStateTests.swift
@@ -0,0 +1,139 @@
+//
+// TunnelStateTests.swift
+// MullvadVPNTests
+//
+// Created by Andrew Bulhak on 2024-05-03.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadTypes
+import PacketTunnelCore
+import XCTest
+
+final class TunnelStateTests: XCTestCase {
+ let arbitrarySelectedRelay = SelectedRelay(
+ endpoint: MullvadEndpoint(
+ ipv4Relay: IPv4Endpoint(ip: .any, port: 0),
+ ipv4Gateway: .any,
+ ipv6Gateway: .any,
+ publicKey: Data()
+ ),
+ hostname: "hostname-goes-here",
+ location: Location(country: "country", countryCode: "", city: "city", cityCode: "", latitude: 0, longitude: 0),
+ retryAttempts: 0
+ )
+
+ // MARK: description
+
+ func testDescription_Connecting_NoRelay() {
+ XCTAssertEqual(
+ TunnelState.connecting(nil, isPostQuantum: false).description,
+ "connecting, fetching relay"
+ )
+
+ XCTAssertEqual(
+ TunnelState.connecting(nil, isPostQuantum: true).description,
+ "connecting (PQ), fetching relay"
+ )
+ }
+
+ func testDescription_Connecting_WithRelay() {
+ XCTAssertEqual(
+ TunnelState.connecting(arbitrarySelectedRelay, isPostQuantum: false).description,
+ "connecting to hostname-goes-here"
+ )
+
+ XCTAssertEqual(
+ TunnelState.connecting(arbitrarySelectedRelay, isPostQuantum: true).description,
+ "connecting (PQ) to hostname-goes-here"
+ )
+ }
+
+ func testDescription_Connected() {
+ XCTAssertEqual(
+ TunnelState.connected(arbitrarySelectedRelay, isPostQuantum: false).description,
+ "connected to hostname-goes-here"
+ )
+
+ XCTAssertEqual(
+ TunnelState.connected(arbitrarySelectedRelay, isPostQuantum: true).description,
+ "connected (PQ) to hostname-goes-here"
+ )
+ }
+
+ // MARK: localizedTitleForSecureLabel
+
+ func testLocalizedTitleForSecureLabel_Connecting() {
+ XCTAssertEqual(
+ TunnelState.connecting(nil, isPostQuantum: false).localizedTitleForSecureLabel,
+ "Creating secure connection"
+ )
+
+ XCTAssertEqual(
+ TunnelState.connecting(nil, isPostQuantum: true).localizedTitleForSecureLabel,
+ "Creating quantum secure connection"
+ )
+ }
+
+ func testLocalizedTitleForSecureLabel_Reconnecting() {
+ XCTAssertEqual(
+ TunnelState.reconnecting(arbitrarySelectedRelay, isPostQuantum: false).localizedTitleForSecureLabel,
+ "Creating secure connection"
+ )
+
+ XCTAssertEqual(
+ TunnelState.reconnecting(arbitrarySelectedRelay, isPostQuantum: true).localizedTitleForSecureLabel,
+ "Creating quantum secure connection"
+ )
+ }
+
+ func testLocalizedTitleForSecureLabel_Connected() {
+ XCTAssertEqual(
+ TunnelState.connected(arbitrarySelectedRelay, isPostQuantum: false).localizedTitleForSecureLabel,
+ "Secure connection"
+ )
+
+ XCTAssertEqual(
+ TunnelState.connected(arbitrarySelectedRelay, isPostQuantum: true).localizedTitleForSecureLabel,
+ "Quantum secure connection"
+ )
+ }
+
+ // MARK: localizedAccessibilityLabel
+
+ func testLocalizedAccessibilityLabel_Connecting() {
+ XCTAssertEqual(
+ TunnelState.connecting(nil, isPostQuantum: false).localizedAccessibilityLabel,
+ "Creating secure connection"
+ )
+
+ XCTAssertEqual(
+ TunnelState.connecting(nil, isPostQuantum: true).localizedAccessibilityLabel,
+ "Creating quantum secure connection"
+ )
+ }
+
+ func testLocalizedAccessibilityLabel_Reconnecting() {
+ XCTAssertEqual(
+ TunnelState.reconnecting(arbitrarySelectedRelay, isPostQuantum: false).localizedAccessibilityLabel,
+ "Reconnecting to city, country"
+ )
+
+ XCTAssertEqual(
+ TunnelState.reconnecting(arbitrarySelectedRelay, isPostQuantum: true).localizedAccessibilityLabel,
+ "Reconnecting to city, country"
+ )
+ }
+
+ func testLocalizedAccessibilityLabel_Connected() {
+ XCTAssertEqual(
+ TunnelState.connected(arbitrarySelectedRelay, isPostQuantum: false).localizedAccessibilityLabel,
+ "Secure connection. Connected to city, country"
+ )
+
+ XCTAssertEqual(
+ TunnelState.connected(arbitrarySelectedRelay, isPostQuantum: true).localizedAccessibilityLabel,
+ "Quantum secure connection. Connected to city, country"
+ )
+ }
+}
diff --git a/ios/PacketTunnelCore/Actor/ObservedState.swift b/ios/PacketTunnelCore/Actor/ObservedState.swift
index f05d68e525..bdb85a8e51 100644
--- a/ios/PacketTunnelCore/Actor/ObservedState.swift
+++ b/ios/PacketTunnelCore/Actor/ObservedState.swift
@@ -33,6 +33,7 @@ public struct ObservedConnectionState: Equatable, Codable {
public var transportLayer: TransportLayer
public var remotePort: UInt16
public var lastKeyRotation: Date?
+ public let isPostQuantum: Bool
public var isNetworkReachable: Bool {
networkReachability != .unreachable
@@ -45,7 +46,8 @@ public struct ObservedConnectionState: Equatable, Codable {
connectionAttemptCount: UInt,
transportLayer: TransportLayer,
remotePort: UInt16,
- lastKeyRotation: Date? = nil
+ lastKeyRotation: Date? = nil,
+ isPostQuantum: Bool
) {
self.selectedRelay = selectedRelay
self.relayConstraints = relayConstraints
@@ -54,6 +56,7 @@ public struct ObservedConnectionState: Equatable, Codable {
self.transportLayer = transportLayer
self.remotePort = remotePort
self.lastKeyRotation = lastKeyRotation
+ self.isPostQuantum = isPostQuantum
}
}
@@ -97,7 +100,8 @@ extension State.ConnectionData {
connectionAttemptCount: connectionAttemptCount,
transportLayer: transportLayer,
remotePort: remotePort,
- lastKeyRotation: lastKeyRotation
+ lastKeyRotation: lastKeyRotation,
+ isPostQuantum: isPostQuantum
)
}
}
diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift
index 768024ab23..552624d504 100644
--- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift
+++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift
@@ -283,7 +283,7 @@ extension PacketTunnelActor {
) async throws {
let settings: Settings = try settingsReader.read()
- guard settings.quantumResistance == .off || settings.quantumResistance == .automatic else {
+ if settings.quantumResistance.isEnabled {
if let connectionState = try makeConnectionState(nextRelay: nextRelay, settings: settings, reason: reason) {
let selectedEndpoint = connectionState.selectedRelay.endpoint
let activeKey = activeKey(from: connectionState, in: settings)
@@ -411,7 +411,8 @@ extension PacketTunnelActor {
lastKeyRotation: lastKeyRotation,
connectedEndpoint: selectedRelay.endpoint,
transportLayer: .udp,
- remotePort: selectedRelay.endpoint.ipv4Relay.port
+ remotePort: selectedRelay.endpoint.ipv4Relay.port,
+ isPostQuantum: settings.quantumResistance.isEnabled
)
}
@@ -449,7 +450,8 @@ extension PacketTunnelActor {
lastKeyRotation: connectionState.lastKeyRotation,
connectedEndpoint: obfuscatedEndpoint,
transportLayer: transportLayer,
- remotePort: protocolObfuscator.remotePort
+ remotePort: protocolObfuscator.remotePort,
+ isPostQuantum: connectionState.isPostQuantum
)
}
diff --git a/ios/PacketTunnelCore/Actor/State.swift b/ios/PacketTunnelCore/Actor/State.swift
index 259993a7f8..f99799201c 100644
--- a/ios/PacketTunnelCore/Actor/State.swift
+++ b/ios/PacketTunnelCore/Actor/State.swift
@@ -146,6 +146,9 @@ extension State {
/// The remote port that was chosen to connect to `connectedEndpoint`
public let remotePort: UInt16
+
+ /// True if post-quantum key exchange is enabled
+ public let isPostQuantum: Bool
}
/// Data associated with error state.