Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PacketTunnel: introduce proper state + blocked state #5183

Merged
merged 1 commit into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
336 changes: 262 additions & 74 deletions ios/MullvadVPN.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
guard tunnelManager.deviceState.isLoggedIn else { return false }

switch tunnelManager.tunnelStatus.state {
case .connected, .connecting, .reconnecting, .waitingForConnectivity(.noConnection):
case .connected, .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .error:
tunnelManager.reconnectTunnel(selectNewRelay: true)

case .disconnecting, .disconnected:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,13 @@ final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotific
// MARK: - Private

private func handleTunnelStatus(_ tunnelStatus: TunnelStatus) {
let invalidateForTunnelError = updateLastTunnelError(
tunnelStatus.packetTunnelStatus.lastErrors.first?.localizedDescription
)
let invalidateForTunnelError: Bool
if case let .error(blockStateReason) = tunnelStatus.state {
invalidateForTunnelError = updateLastTunnelError(blockStateReason.rawValue)
} else {
invalidateForTunnelError = updateLastTunnelError(nil)
}

let invalidateForManagerError = updateTunnelManagerError(tunnelStatus.state)
let invalidateForConnectivity = updateConnectivity(tunnelStatus.state)
let invalidateForNetwork = updateNetwork(tunnelStatus.state)
Expand Down
12 changes: 9 additions & 3 deletions ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ class MapConnectionStatusOperation: AsyncOperation {

case .reasserting:
fetchTunnelStatus(tunnel: tunnel) { packetTunnelStatus in
if packetTunnelStatus.isNetworkReachable {
if let blockedStateReason = packetTunnelStatus.blockedStateReason {
return .error(blockedStateReason)
} else if packetTunnelStatus.isNetworkReachable {
return packetTunnelStatus.tunnelRelay.map { .reconnecting($0) }
} else {
return .waitingForConnectivity(.noConnection)
Expand All @@ -62,7 +64,9 @@ class MapConnectionStatusOperation: AsyncOperation {

case .connected:
fetchTunnelStatus(tunnel: tunnel) { packetTunnelStatus in
if packetTunnelStatus.isNetworkReachable {
if let blockedStateReason = packetTunnelStatus.blockedStateReason {
return .error(blockedStateReason)
} else if packetTunnelStatus.isNetworkReachable {
return packetTunnelStatus.tunnelRelay.map { .connected($0) }
} else {
return .waitingForConnectivity(.noConnection)
Expand Down Expand Up @@ -102,7 +106,9 @@ class MapConnectionStatusOperation: AsyncOperation {
}

fetchTunnelStatus(tunnel: tunnel) { packetTunnelStatus in
if packetTunnelStatus.isNetworkReachable {
if let blockedStateReason = packetTunnelStatus.blockedStateReason {
return .error(blockedStateReason)
} else if packetTunnelStatus.isNetworkReachable {
return packetTunnelStatus.tunnelRelay.map { .connecting($0) }
} else {
return .waitingForConnectivity(.noConnection)
Expand Down
2 changes: 1 addition & 1 deletion ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class StopTunnelOperation: ResultOperation<Void> {

finish(result: .success(()))

case .connected, .connecting, .reconnecting, .waitingForConnectivity(.noConnection):
case .connected, .connecting, .reconnecting, .waitingForConnectivity(.noConnection), .error:
guard let tunnel = interactor.tunnel else {
finish(result: .failure(UnsetTunnelError()))
return
Expand Down
61 changes: 10 additions & 51 deletions ios/MullvadVPN/TunnelManager/TunnelManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ final class TunnelManager: StorePaymentObserver {
private var _tunnelStatus = TunnelStatus()

/// Last processed device check.
private var lastDeviceCheck: DeviceCheck?
private var lastPacketTunnelKeyRotation: Date?

// MARK: - Initialization

Expand Down Expand Up @@ -696,17 +696,23 @@ final class TunnelManager: StorePaymentObserver {

_tunnelStatus = newTunnelStatus

if let deviceCheck = newTunnelStatus.packetTunnelStatus.deviceCheck {
handleDeviceCheck(deviceCheck)
// Packet tunnel may have attempted or rotated the key.
// In that case we have to reload device state from Keychain as it's likely was modified by packet tunnel.
let newPacketTunnelKeyRotation = newTunnelStatus.packetTunnelStatus.lastKeyRotation
if lastPacketTunnelKeyRotation != newPacketTunnelKeyRotation {
lastPacketTunnelKeyRotation = newPacketTunnelKeyRotation
refreshDeviceState()
}

// TODO: handle blocked state (error state). See how handleRestError() manages invalid account or revoked device.

switch newTunnelStatus.state {
case .connecting, .reconnecting:
// Start polling tunnel status to keep the relay information up to date
// while the tunnel process is trying to connect.
startPollingTunnelStatus(interval: establishingTunnelStatusPollInterval)

case .connected, .waitingForConnectivity(.noConnection):
case .connected, .waitingForConnectivity(.noConnection), .error:
// Start polling tunnel status to keep connectivity status up to date.
startPollingTunnelStatus(interval: establishedTunnelStatusPollInterval)

Expand All @@ -724,53 +730,6 @@ final class TunnelManager: StorePaymentObserver {
return newTunnelStatus
}

private func handleDeviceCheck(_ deviceCheck: DeviceCheck) {
// Bail immediately when last device check is identical.
guard lastDeviceCheck != deviceCheck else { return }

// Packet tunnel may have attempted or rotated the key.
// In that case we have to reload device state from Keychain as it's likely was modified by packet tunnel.
if lastDeviceCheck?.keyRotationStatus != deviceCheck.keyRotationStatus {
switch deviceCheck.keyRotationStatus {
case .attempted, .succeeded:
refreshDeviceState()
case .noAction:
break
}
}

// Packet tunnel detected that device is revoked.
if lastDeviceCheck?.deviceVerdict != deviceCheck.deviceVerdict, deviceCheck.deviceVerdict == .revoked {
scheduleDeviceStateUpdate(taskName: "Set device revoked", reconnectTunnel: false) { deviceState in
deviceState = .revoked
}
}

// Packet tunnel received new account expiry.
if lastDeviceCheck?.accountVerdict != deviceCheck.accountVerdict {
switch deviceCheck.accountVerdict {
case let .expired(accountData), let .active(accountData):
scheduleDeviceStateUpdate(taskName: "Update account expiry", reconnectTunnel: false) { deviceState in
guard case .loggedIn(var storedAccountData, let storedDeviceData) = deviceState else {
return
}

if storedAccountData.identifier == accountData.id {
storedAccountData.expiry = accountData.expiry
}

deviceState = .loggedIn(storedAccountData, storedDeviceData)
}

case .invalid:
break
}
}

// Save last device check.
lastDeviceCheck = deviceCheck
}

fileprivate func setSettings(_ settings: LatestTunnelSettings, persist: Bool) {
nslock.lock()
defer { nslock.unlock() }
Expand Down
9 changes: 7 additions & 2 deletions ios/MullvadVPN/TunnelManager/TunnelState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ enum TunnelState: Equatable, CustomStringConvertible {
/// Waiting for connectivity to come back up.
case waitingForConnectivity(WaitingForConnectionReason)

/// Error state.
case error(BlockedStateReason)

var description: String {
switch self {
case .pendingReconnect:
Expand All @@ -85,14 +88,16 @@ enum TunnelState: Equatable, CustomStringConvertible {
return "reconnecting to \(tunnelRelay.hostname)"
case .waitingForConnectivity:
return "waiting for connectivity"
case let .error(blockedStateReason):
return "error state: \(blockedStateReason)"
}
}

var isSecured: Bool {
switch self {
case .reconnecting, .connecting, .connected, .waitingForConnectivity(.noConnection):
return true
case .pendingReconnect, .disconnecting, .disconnected, .waitingForConnectivity(.noNetwork):
case .pendingReconnect, .disconnecting, .disconnected, .waitingForConnectivity(.noNetwork), .error:
return false
}
}
Expand All @@ -103,7 +108,7 @@ enum TunnelState: Equatable, CustomStringConvertible {
return relay
case let .connecting(relay):
return relay
case .disconnecting, .disconnected, .waitingForConnectivity, .pendingReconnect:
case .disconnecting, .disconnected, .waitingForConnectivity, .pendingReconnect, .error:
return nil
}
}
Expand Down
18 changes: 15 additions & 3 deletions ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ private extension TunnelState {
case .connected:
return .successColor

case .disconnecting, .disconnected, .pendingReconnect, .waitingForConnectivity(.noNetwork):
case .disconnecting, .disconnected, .pendingReconnect, .waitingForConnectivity(.noNetwork), .error:
return .dangerColor
}
}
Expand Down Expand Up @@ -511,6 +511,10 @@ private extension TunnelState {
value: "No network",
comment: ""
)

case let .error(blockedStateReason):
// TODO: Fix me
return ""
}
}

Expand Down Expand Up @@ -538,6 +542,10 @@ private extension TunnelState {
value: "Switch location",
comment: ""
)

case let .error(blockedStateReason):
// TODO: Fix me
return ""
}
}

Expand Down Expand Up @@ -614,6 +622,10 @@ private extension TunnelState {
value: "Reconnecting",
comment: ""
)

case let .error(blockedStateReason):
// TODO: Fix me
return ""
}
}

Expand All @@ -628,7 +640,7 @@ private extension TunnelState {
.waitingForConnectivity(.noConnection):
return [.selectLocation, .cancel]

case .connected, .reconnecting:
case .connected, .reconnecting, .error:
return [.selectLocation, .disconnect]
}

Expand All @@ -641,7 +653,7 @@ private extension TunnelState {
.waitingForConnectivity(.noConnection):
return [.cancel]

case .connected, .reconnecting:
case .connected, .reconnecting, .error:
return [.disconnect]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ class TunnelViewController: UIViewController, RootContainment {
mapViewController.removeLocationMarker()
contentView.setAnimatingActivity(true)

case .waitingForConnectivity:
case .waitingForConnectivity, .error:
mapViewController.removeLocationMarker()
contentView.setAnimatingActivity(false)

Expand Down
70 changes: 70 additions & 0 deletions ios/PacketTunnel/DeviceCheck/DeviceCheck.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//
// DeviceCheck.swift
// PacketTunnel
//
// Created by pronebird on 13/09/2023.
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadTypes

/// The verdict of an account status check.
enum AccountVerdict: Equatable {
/// Account is no longer valid.
case invalid

/// Account is expired.
case expired(Account)

/// Account exists and has enough time left.
case active(Account)
}

/// The verdict of a device status check.
enum DeviceVerdict: Equatable {
/// Device is revoked.
case revoked

/// Device exists but the public key registered on server does not match any longer.
case keyMismatch

/// Device is in good standing and should work as normal.
case active
}

/// Type describing whether key rotation took place and the outcome of it.
enum KeyRotationStatus: Equatable {
/// No rotation took place yet.
case noAction

/// Rotation attempt took place but without success.
case attempted(Date)

/// Rotation attempt took place and succeeded.
case succeeded(Date)

/// Returns `true` if the status is `.succeeded`.
var isSucceeded: Bool {
if case .succeeded = self {
return true
} else {
return false
}
}
}

/**
Struct holding data associated with account and device diagnostics and also device key recovery performed by packet
tunnel process.
*/
struct DeviceCheck: Equatable {
/// The verdict of account status check.
var accountVerdict: AccountVerdict

/// The verdict of device status check.
var deviceVerdict: DeviceVerdict

// The status of the last performed key rotation.
var keyRotationStatus: KeyRotationStatus
}
2 changes: 1 addition & 1 deletion ios/PacketTunnel/DeviceCheck/DeviceCheckOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ final class DeviceCheckOperation: ResultOperation<DeviceCheck> {
remoteSevice: DeviceCheckRemoteServiceProtocol,
deviceStateAccessor: DeviceStateAccessorProtocol,
rotateImmediatelyOnKeyMismatch: Bool,
completionHandler: @escaping CompletionHandler
completionHandler: CompletionHandler? = nil
) {
self.remoteService = remoteSevice
self.deviceStateAccessor = deviceStateAccessor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ struct DeviceCheckRemoteService: DeviceCheckRemoteServiceProtocol {
accountNumber: String,
completion: @escaping (Result<Account, Error>) -> Void
) -> Cancellable {
accountsProxy.getAccountData(accountNumber: accountNumber, retryStrategy: .noRetry, completion: completion)
accountsProxy.getAccountData(accountNumber: accountNumber).execute(completionHandler: completion)
}

func getDevice(
Expand Down
23 changes: 0 additions & 23 deletions ios/PacketTunnel/MullvadEndpoint+WgEndpoint.swift

This file was deleted.

Loading
Loading