Skip to content

Commit

Permalink
PacketTunnel: introduce proper state and blocked state
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrej Mihajlov committed Sep 22, 2023
1 parent 990b5f8 commit b2965b9
Show file tree
Hide file tree
Showing 61 changed files with 3,169 additions and 1,244 deletions.
280 changes: 218 additions & 62 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

0 comments on commit b2965b9

Please sign in to comment.