diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index 19db4c72975d..d0fc0c6bc379 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -980,6 +980,11 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo case .connected, .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .error: tunnelManager.reconnectTunnel(selectNewRelay: true) + #if DEBUG + case .negotiatingKey: + tunnelManager.reconnectTunnel(selectNewRelay: true) + #endif + case .disconnecting, .disconnected: tunnelManager.startTunnel() diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift index 5d61428497a5..9c133984223e 100644 --- a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift +++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift @@ -50,21 +50,27 @@ class MapConnectionStatusOperation: AsyncOperation { fetchTunnelStatus(tunnel: tunnel) { observedState in switch observedState { case let .connected(connectionState): - return connectionState.isNetworkReachable + connectionState.isNetworkReachable ? .connected(connectionState.selectedRelay) : .waitingForConnectivity(.noConnection) case let .connecting(connectionState): - return connectionState.isNetworkReachable + connectionState.isNetworkReachable ? .connecting(connectionState.selectedRelay) : .waitingForConnectivity(.noConnection) + #if DEBUG + case let .negotiatingKey(connectionState): + connectionState.isNetworkReachable + ? .negotiatingKey(connectionState.selectedRelay) + : .waitingForConnectivity(.noConnection) + #endif case let .reconnecting(connectionState): - return connectionState.isNetworkReachable + connectionState.isNetworkReachable ? .reconnecting(connectionState.selectedRelay) : .waitingForConnectivity(.noConnection) case let .error(blockedState): - return .error(blockedState.reason) + .error(blockedState.reason) case .initial, .disconnecting, .disconnected: - return .none + .none } } return diff --git a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift index 7bcce98a734a..4701a4238ca4 100644 --- a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift @@ -36,27 +36,36 @@ class StopTunnelOperation: ResultOperation { finish(result: .success(())) case .connected, .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .error: - guard let tunnel = interactor.tunnel else { - finish(result: .failure(UnsetTunnelError())) - return - } + doShutDownTunnel() - // Disable on-demand when stopping the tunnel to prevent it from coming back up - tunnel.isOnDemandEnabled = false - - tunnel.saveToPreferences { error in - self.dispatchQueue.async { - if let error { - self.finish(result: .failure(error)) - } else { - tunnel.stop() - self.finish(result: .success(())) - } - } - } + #if DEBUG + case .negotiatingKey: + doShutDownTunnel() + #endif case .disconnected, .disconnecting, .pendingReconnect, .waitingForConnectivity(.noNetwork): finish(result: .success(())) } } + + private func doShutDownTunnel() { + guard let tunnel = interactor.tunnel else { + finish(result: .failure(UnsetTunnelError())) + return + } + + // Disable on-demand when stopping the tunnel to prevent it from coming back up + tunnel.isOnDemandEnabled = false + + tunnel.saveToPreferences { error in + self.dispatchQueue.async { + if let error { + self.finish(result: .failure(error)) + } else { + tunnel.stop() + self.finish(result: .success(())) + } + } + } + } } diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index b3d53b56133b..3f4c4c67d582 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -675,6 +675,11 @@ final class TunnelManager: StorePaymentObserver { // while the tunnel process is trying to connect. startPollingTunnelStatus(interval: establishingTunnelStatusPollInterval) + #if DEBUG + case .negotiatingKey: + startPollingTunnelStatus(interval: establishingTunnelStatusPollInterval) + #endif + case .connected, .waitingForConnectivity(.noConnection): // Start polling tunnel status to keep connectivity status up to date. startPollingTunnelStatus(interval: establishedTunnelStatusPollInterval) diff --git a/ios/MullvadVPN/TunnelManager/TunnelState.swift b/ios/MullvadVPN/TunnelManager/TunnelState.swift index fad29775681c..08b889899ef7 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelState.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelState.swift @@ -50,6 +50,11 @@ enum TunnelState: Equatable, CustomStringConvertible { /// Connecting the tunnel. case connecting(SelectedRelay?) + #if DEBUG + /// Negotiating a key for post-quantum resistance + case negotiatingKey(SelectedRelay) + #endif + /// Connected the tunnel case connected(SelectedRelay) @@ -75,25 +80,29 @@ enum TunnelState: Equatable, CustomStringConvertible { var description: String { switch self { case .pendingReconnect: - return "pending reconnect after disconnect" + "pending reconnect after disconnect" case let .connecting(tunnelRelay): if let tunnelRelay { - return "connecting to \(tunnelRelay.hostname)" + "connecting to \(tunnelRelay.hostname)" } else { - return "connecting, fetching relay" + "connecting, fetching relay" } case let .connected(tunnelRelay): - return "connected to \(tunnelRelay.hostname)" + "connected to \(tunnelRelay.hostname)" case let .disconnecting(actionAfterDisconnect): - return "disconnecting and then \(actionAfterDisconnect)" + "disconnecting and then \(actionAfterDisconnect)" case .disconnected: - return "disconnected" + "disconnected" case let .reconnecting(tunnelRelay): - return "reconnecting to \(tunnelRelay.hostname)" + "reconnecting to \(tunnelRelay.hostname)" case .waitingForConnectivity: - return "waiting for connectivity" + "waiting for connectivity" case let .error(blockedStateReason): - return "error state: \(blockedStateReason)" + "error state: \(blockedStateReason)" + #if DEBUG + case let .negotiatingKey(tunnelRelay): + "negotiating key with \(tunnelRelay.hostname)" + #endif } } @@ -101,20 +110,28 @@ enum TunnelState: Equatable, CustomStringConvertible { switch self { case .reconnecting, .connecting, .connected, .waitingForConnectivity(.noConnection), .error(.accountExpired), .error(.deviceRevoked): - return true + true case .pendingReconnect, .disconnecting, .disconnected, .waitingForConnectivity(.noNetwork), .error: - return false + false + #if DEBUG + case .negotiatingKey: + false + #endif } } var relay: SelectedRelay? { switch self { case let .connected(relay), let .reconnecting(relay): - return relay + relay case let .connecting(relay): - return relay + relay + #if DEBUG + case let .negotiatingKey(relay): + relay + #endif case .disconnecting, .disconnected, .waitingForConnectivity, .pendingReconnect, .error: - return nil + nil } } } @@ -130,9 +147,9 @@ enum ActionAfterDisconnect: CustomStringConvertible { var description: String { switch self { case .nothing: - return "do nothing" + "do nothing" case .reconnect: - return "reconnect" + "reconnect" } } } diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift index bd991142e662..14725159277c 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift @@ -450,13 +450,18 @@ private extension TunnelState { var textColorForSecureLabel: UIColor { switch self { case .connecting, .reconnecting, .waitingForConnectivity(.noConnection): - return .white + .white + + #if DEBUG + case .negotiatingKey: + .white + #endif case .connected: - return .successColor + .successColor case .disconnecting, .disconnected, .pendingReconnect, .waitingForConnectivity(.noNetwork), .error: - return .dangerColor + .dangerColor } } @@ -471,15 +476,25 @@ private extension TunnelState { var localizedTitleForSecureLabel: String { switch self { case .connecting, .reconnecting: - return NSLocalizedString( + NSLocalizedString( "TUNNEL_STATE_CONNECTING", tableName: "Main", value: "Creating secure connection", comment: "" ) + #if DEBUG + case .negotiatingKey: + NSLocalizedString( + "TUNNEL_STATE_NEGOTIATING_KEY", + tableName: "Main", + value: "Negotiating key", + comment: "" + ) + #endif + case .connected: - return NSLocalizedString( + NSLocalizedString( "TUNNEL_STATE_CONNECTED", tableName: "Main", value: "Secure connection", @@ -487,14 +502,14 @@ private extension TunnelState { ) case .disconnecting(.nothing): - return NSLocalizedString( + NSLocalizedString( "TUNNEL_STATE_DISCONNECTING", tableName: "Main", value: "Disconnecting", comment: "" ) case .disconnecting(.reconnect), .pendingReconnect: - return NSLocalizedString( + NSLocalizedString( "TUNNEL_STATE_PENDING_RECONNECT", tableName: "Main", value: "Reconnecting", @@ -502,7 +517,7 @@ private extension TunnelState { ) case .disconnected: - return NSLocalizedString( + NSLocalizedString( "TUNNEL_STATE_DISCONNECTED", tableName: "Main", value: "Unsecured connection", @@ -510,7 +525,7 @@ private extension TunnelState { ) case .waitingForConnectivity(.noConnection), .error: - return NSLocalizedString( + NSLocalizedString( "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY", tableName: "Main", value: "Blocked connection", @@ -518,7 +533,7 @@ private extension TunnelState { ) case .waitingForConnectivity(.noNetwork): - return NSLocalizedString( + NSLocalizedString( "TUNNEL_STATE_NO_NETWORK", tableName: "Main", value: "No network", @@ -530,7 +545,7 @@ private extension TunnelState { var localizedTitleForSelectLocationButton: String? { switch self { case .disconnecting(.reconnect), .pendingReconnect: - return NSLocalizedString( + NSLocalizedString( "SWITCH_LOCATION_BUTTON_TITLE", tableName: "Main", value: "Select location", @@ -538,7 +553,7 @@ private extension TunnelState { ) case .disconnected, .disconnecting(.nothing): - return NSLocalizedString( + NSLocalizedString( "SELECT_LOCATION_BUTTON_TITLE", tableName: "Main", value: "Select location", @@ -546,27 +561,47 @@ private extension TunnelState { ) case .connecting, .connected, .reconnecting, .waitingForConnectivity, .error: - return NSLocalizedString( + NSLocalizedString( "SWITCH_LOCATION_BUTTON_TITLE", tableName: "Main", value: "Switch location", comment: "" ) + + #if DEBUG + case .negotiatingKey: + NSLocalizedString( + "SWITCH_LOCATION_BUTTON_TITLE", + tableName: "Main", + value: "Switch location", + comment: "" + ) + #endif } } var localizedAccessibilityLabel: String { switch self { case .connecting: - return NSLocalizedString( + NSLocalizedString( + "TUNNEL_STATE_CONNECTING_ACCESSIBILITY_LABEL", + tableName: "Main", + value: "Creating secure connection", + comment: "" + ) + + #if DEBUG + case .negotiatingKey: + NSLocalizedString( "TUNNEL_STATE_CONNECTING_ACCESSIBILITY_LABEL", tableName: "Main", value: "Creating secure connection", comment: "" ) + #endif case let .connected(tunnelInfo): - return String( + String( format: NSLocalizedString( "TUNNEL_STATE_CONNECTED_ACCESSIBILITY_LABEL", tableName: "Main", @@ -578,7 +613,7 @@ private extension TunnelState { ) case .disconnected: - return NSLocalizedString( + NSLocalizedString( "TUNNEL_STATE_DISCONNECTED_ACCESSIBILITY_LABEL", tableName: "Main", value: "Unsecured connection", @@ -586,7 +621,7 @@ private extension TunnelState { ) case let .reconnecting(tunnelInfo): - return String( + String( format: NSLocalizedString( "TUNNEL_STATE_RECONNECTING_ACCESSIBILITY_LABEL", tableName: "Main", @@ -598,7 +633,7 @@ private extension TunnelState { ) case .waitingForConnectivity(.noConnection), .error: - return NSLocalizedString( + NSLocalizedString( "TUNNEL_STATE_WAITING_FOR_CONNECTIVITY_ACCESSIBILITY_LABEL", tableName: "Main", value: "Blocked connection", @@ -606,7 +641,7 @@ private extension TunnelState { ) case .waitingForConnectivity(.noNetwork): - return NSLocalizedString( + NSLocalizedString( "TUNNEL_STATE_NO_NETWORK_ACCESSIBILITY_LABEL", tableName: "Main", value: "No network", @@ -614,7 +649,7 @@ private extension TunnelState { ) case .disconnecting(.nothing): - return NSLocalizedString( + NSLocalizedString( "TUNNEL_STATE_DISCONNECTING_ACCESSIBILITY_LABEL", tableName: "Main", value: "Disconnecting", @@ -622,7 +657,7 @@ private extension TunnelState { ) case .disconnecting(.reconnect), .pendingReconnect: - return NSLocalizedString( + NSLocalizedString( "TUNNEL_STATE_PENDING_RECONNECT_ACCESSIBILITY_LABEL", tableName: "Main", value: "Reconnecting", @@ -636,31 +671,40 @@ private extension TunnelState { case (.phone, _), (.pad, .compact): switch self { case .disconnected, .disconnecting(.nothing), .waitingForConnectivity(.noNetwork): - return [.selectLocation, .connect] + [.selectLocation, .connect] case .connecting, .pendingReconnect, .disconnecting(.reconnect), .waitingForConnectivity(.noConnection): - return [.selectLocation, .cancel] + [.selectLocation, .cancel] + + #if DEBUG + case .negotiatingKey: + [.selectLocation, .cancel] + #endif case .connected, .reconnecting, .error: - return [.selectLocation, .disconnect] + [.selectLocation, .disconnect] } case (.pad, .regular): switch self { case .disconnected, .disconnecting(.nothing), .waitingForConnectivity(.noNetwork): - return [.connect] + [.connect] case .connecting, .pendingReconnect, .disconnecting(.reconnect), .waitingForConnectivity(.noConnection): - return [.cancel] + [.cancel] + #if DEBUG + case .negotiatingKey: + [.cancel] + #endif case .connected, .reconnecting, .error: - return [.disconnect] + [.disconnect] } default: - return [] + [] } } diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift index d01b3b3133df..7d68b3dd4136 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift @@ -157,6 +157,13 @@ class TunnelViewController: UIViewController, RootContainment { contentView.setAnimatingActivity(true) mapViewController.setCenter(tunnelRelay.location.geoCoordinate, animated: animated) + #if DEBUG + case let .negotiatingKey(tunnelRelay): + mapViewController.removeLocationMarker() + contentView.setAnimatingActivity(true) + mapViewController.setCenter(tunnelRelay.location.geoCoordinate, animated: animated) + #endif + case let .connected(tunnelRelay): let center = tunnelRelay.location.geoCoordinate mapViewController.setCenter(center, animated: animated) { diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift index d2aba984ca81..ed9c700af825 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift @@ -229,6 +229,11 @@ extension PacketTunnelProvider { // Cache last connection attempt to filter out repeating calls. lastConnectionAttempt = connectionAttempt + #if DEBUG + case .negotiatingKey: + break + #endif + case .initial, .connected, .disconnecting, .disconnected, .error: break } diff --git a/ios/PacketTunnelCore/Actor/ObservedState+Extensions.swift b/ios/PacketTunnelCore/Actor/ObservedState+Extensions.swift index 05624a6565de..1504f3e47ddf 100644 --- a/ios/PacketTunnelCore/Actor/ObservedState+Extensions.swift +++ b/ios/PacketTunnelCore/Actor/ObservedState+Extensions.swift @@ -13,32 +13,40 @@ extension ObservedState { public var relayConstraints: RelayConstraints? { switch self { case let .connecting(connState), let .connected(connState), let .reconnecting(connState): - return connState.relayConstraints + connState.relayConstraints + #if DEBUG + case let .negotiatingKey(connState): + connState.relayConstraints + #endif case let .error(blockedState): - return blockedState.relayConstraints + blockedState.relayConstraints case .initial, .disconnecting, .disconnected: - return nil + nil } } public var name: String { switch self { case .connected: - return "Connected" + "Connected" case .connecting: - return "Connecting" + "Connecting" + #if DEBUG + case .negotiatingKey: + "Negotiating key" + #endif case .reconnecting: - return "Reconnecting" + "Reconnecting" case .disconnecting: - return "Disconnecting" + "Disconnecting" case .disconnected: - return "Disconnected" + "Disconnected" case .initial: - return "Initial" + "Initial" case .error: - return "Error" + "Error" } } diff --git a/ios/PacketTunnelCore/Actor/ObservedState.swift b/ios/PacketTunnelCore/Actor/ObservedState.swift index a6b7d741bc72..94a1aa19c8f7 100644 --- a/ios/PacketTunnelCore/Actor/ObservedState.swift +++ b/ios/PacketTunnelCore/Actor/ObservedState.swift @@ -16,6 +16,9 @@ public enum ObservedState: Equatable, Codable { case initial case connecting(ObservedConnectionState) case reconnecting(ObservedConnectionState) + #if DEBUG + case negotiatingKey(ObservedConnectionState) + #endif case connected(ObservedConnectionState) case disconnecting(ObservedConnectionState) case disconnected