summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--ios/CHANGELOG.md2
-rw-r--r--ios/MullvadTypes/PacketTunnelStatus.swift28
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj2
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift32
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider.swift200
5 files changed, 261 insertions, 3 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md
index 3a60a96ab9..0cb7b673df 100644
--- a/ios/CHANGELOG.md
+++ b/ios/CHANGELOG.md
@@ -35,6 +35,8 @@ Line wrap the file at 100 chars. Th
- Add menu item to control shortcuts.
- Add continuous monitoring of tunnel connection. Verify ping replies to detect whether traffic is
really flowing.
+- Check if device is revoked or account has expired when the tunnel fails to connect on each second
+ failed attempt.
### Changed
- When logged into an account with no time left, a new view is shown instead of account settings,
diff --git a/ios/MullvadTypes/PacketTunnelStatus.swift b/ios/MullvadTypes/PacketTunnelStatus.swift
index a5844fddda..1e4cbf18ba 100644
--- a/ios/MullvadTypes/PacketTunnelStatus.swift
+++ b/ios/MullvadTypes/PacketTunnelStatus.swift
@@ -8,6 +8,30 @@
import Foundation
+public struct DeviceCheck: Codable, Equatable {
+ /// Unique identifier for the device check.
+ /// Should only change when other fields in the struct are being changed.
+ public var identifier: UUID
+
+ /// Flag indicating whether device is revoked.
+ /// Set to `nil` when the device status is unknown yet.
+ public var isDeviceRevoked: Bool?
+
+ /// Last known account expiry.
+ /// Set to `nil` when account expiry is unknown yet.
+ public var accountExpiry: Date?
+
+ public init(
+ identifier: UUID = UUID(),
+ isDeviceRevoked: Bool? = nil,
+ accountExpiry: Date? = nil
+ ) {
+ self.identifier = identifier
+ self.isDeviceRevoked = isDeviceRevoked
+ self.accountExpiry = accountExpiry
+ }
+}
+
/// Struct describing packet tunnel process status.
public struct PacketTunnelStatus: Codable, Equatable {
/// Last tunnel error.
@@ -16,16 +40,20 @@ public struct PacketTunnelStatus: Codable, Equatable {
/// Flag indicating whether network is reachable.
public var isNetworkReachable: Bool
+ /// Last performed device check.
+ public var deviceCheck: DeviceCheck?
/// Current relay.
public var tunnelRelay: PacketTunnelRelay?
public init(
lastError: String? = nil,
isNetworkReachable: Bool = true,
+ deviceCheck: DeviceCheck? = nil,
tunnelRelay: PacketTunnelRelay? = nil
) {
self.lastError = lastError
self.isNetworkReachable = isNetworkReachable
+ self.deviceCheck = deviceCheck
self.tunnelRelay = tunnelRelay
}
}
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index a1cea3ff0a..c73d0c820c 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
+ 061A2B67291121410084590B /* libOperations.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58E5126528DDF04200B0BCDE /* libOperations.a */; };
062B45A328FD4CA700746E77 /* le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 06799AB428F98CE700ACD94E /* le_root_cert.cer */; };
062B45AE28FD503000746E77 /* WireGuardKit in Frameworks */ = {isa = PBXBuildFile; productRef = 062B45AD28FD503000746E77 /* WireGuardKit */; };
062B45B428FD508C00746E77 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 062B45B328FD508C00746E77 /* Logging */; };
@@ -910,6 +911,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 061A2B67291121410084590B /* libOperations.a in Frameworks */,
5898D2AB2901845400EB5EBA /* libRelaySelector.a in Frameworks */,
5898D2AC2901845400EB5EBA /* libTunnelProviderMessaging.a in Frameworks */,
062B45B428FD508C00746E77 /* Logging in Frameworks */,
diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
index 5061230e17..8d25f31ff1 100644
--- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift
+++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift
@@ -82,6 +82,9 @@ final class TunnelManager {
private var _tunnel: Tunnel?
private var _tunnelStatus = TunnelStatus()
+ /// Last processed device check identifier.
+ private var lastDeviceCheckIdentifier: UUID?
+
// MARK: - Initialization
private init(accountsProxy: REST.AccountsProxy, devicesProxy: REST.DevicesProxy) {
@@ -665,6 +668,27 @@ final class TunnelManager {
_tunnelStatus = newTunnelStatus
+ if let deviceCheck = newTunnelStatus.packetTunnelStatus.deviceCheck,
+ deviceCheck.identifier != lastDeviceCheckIdentifier
+ {
+ if deviceCheck.isDeviceRevoked ?? false {
+ didDetectDeviceRevoked()
+
+ } else if let accountExpiry = deviceCheck.accountExpiry {
+ scheduleDeviceStateUpdate(
+ taskName: "Update account expiry",
+ reconnectTunnel: false
+ ) { deviceState in
+ if case .loggedIn(var accountData, let deviceData) = deviceState {
+ accountData.expiry = accountExpiry
+ deviceState = .loggedIn(accountData, deviceData)
+ }
+ }
+ }
+
+ lastDeviceCheckIdentifier = deviceCheck.identifier
+ }
+
switch newTunnelStatus.state {
case .connecting, .reconnecting:
// Start polling tunnel status to keep the relay information up to date
@@ -895,8 +919,9 @@ final class TunnelManager {
private func scheduleDeviceStateUpdate(
taskName: String,
+ reconnectTunnel: Bool = true,
modificationBlock: @escaping (inout DeviceState) -> Void,
- completionHandler: (() -> Void)?
+ completionHandler: (() -> Void)? = nil
) {
let operation = AsyncBlockOperation(dispatchQueue: internalQueue) {
var deviceState = self.deviceState
@@ -904,7 +929,10 @@ final class TunnelManager {
modificationBlock(&deviceState)
self.setDeviceState(deviceState, persist: true)
- self.reconnectTunnel(selectNewRelay: false, completionHandler: nil)
+
+ if reconnectTunnel {
+ self.reconnectTunnel(selectNewRelay: false, completionHandler: nil)
+ }
}
operation.completionBlock = {
diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift
index 1ec04c0afe..b222e81720 100644
--- a/ios/PacketTunnel/PacketTunnelProvider.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider.swift
@@ -12,6 +12,7 @@ import MullvadREST
import MullvadTypes
import Network
import NetworkExtension
+import Operations
import RelayCache
import RelaySelector
import TunnelProviderMessaging
@@ -37,6 +38,12 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
/// Flag indicating whether network is reachable.
private var isNetworkReachable = true
+ /// Struct holding result of the last device check.
+ private var deviceCheck: DeviceCheck?
+
+ /// Number of consecutive connection failure attempts.
+ private var numberOfFailedAttempts: UInt = 0
+
/// Last runtime error.
private var lastError: Error?
@@ -65,11 +72,23 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
/// Tunnel monitor.
private var tunnelMonitor: TunnelMonitor!
+ /// Account data request proxy
+ private lazy var accountProxy = REST.ProxyFactory.shared.createAccountsProxy()
+
+ /// Device data request proxy
+ private lazy var deviceProxy = REST.ProxyFactory.shared.createDevicesProxy()
+
+ private lazy var checkDeviceStateTask: Cancellable? = nil
+
+ /// Internal operation queue.
+ private let operationQueue = AsyncOperationQueue()
+
/// Returns `PacketTunnelStatus` used for sharing with main bundle process.
private var packetTunnelStatus: PacketTunnelStatus {
return PacketTunnelStatus(
lastError: lastError?.localizedDescription,
isNetworkReachable: isNetworkReachable,
+ deviceCheck: deviceCheck,
tunnelRelay: selectorResult?.packetTunnelRelay
)
}
@@ -91,6 +110,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
super.init()
+ REST.TransportRegistry.shared.setTransport(
+ REST.URLSessionTransport(urlSession: urlSession)
+ )
+
adapter = WireGuardAdapter(
with: self,
shouldHandleReasserting: false,
@@ -197,7 +220,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
dispatchQueue.async {
self.tunnelMonitor.stop()
-
+ self.checkDeviceStateTask?.cancel()
+ self.checkDeviceStateTask = nil
self.startTunnelCompletionHandler = nil
self.reassertTunnelCompletionHandler = nil
}
@@ -320,6 +344,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
reassertTunnelCompletionHandler?()
reassertTunnelCompletionHandler = nil
+ numberOfFailedAttempts = 0
+
+ checkDeviceStateTask?.cancel()
+ checkDeviceStateTask = nil
+
+ deviceCheck = nil
+
setReconnecting(false)
}
@@ -329,6 +360,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
) {
dispatchPrecondition(condition: .onQueue(dispatchQueue))
+ let (value, isOverflow) = numberOfFailedAttempts.addingReportingOverflow(1)
+ numberOfFailedAttempts = isOverflow ? 0 : value
+
+ if numberOfFailedAttempts.isMultiple(of: 2) {
+ startDeviceCheck()
+ }
+
providerLogger.debug("Recover connection. Picking next relay...")
reconnectTunnel(to: .automatic, completionHandler: completionHandler)
@@ -451,6 +489,146 @@ class PacketTunnelProvider: NEPacketTunnelProvider, TunnelMonitorDelegate {
constraints: relayConstraints
)
}
+
+ // MARK: - Device check
+
+ /// Fetch account and device data to verify account expiry and device status.
+ /// Saves the result into deviceCheck
+ private func startDeviceCheck() {
+ let deviceState: DeviceState
+ do {
+ deviceState = try SettingsManager.readDeviceState()
+ } catch {
+ providerLogger.error(
+ error: error,
+ message: "Failed to read device states"
+ )
+ return
+ }
+
+ guard case let .loggedIn(storedAccountData, storedDeviceData) = deviceState else {
+ return
+ }
+
+ providerLogger.debug("Start device check")
+
+ let accountOperation = createGetAccountDataOperation(
+ accountNumber: storedAccountData.number
+ )
+ let deviceOperation = createGetDeviceDataOperation(
+ accountNumber: storedAccountData.number,
+ identifier: storedDeviceData.identifier
+ )
+
+ let completionOperation = AsyncBlockOperation(dispatchQueue: dispatchQueue) { [weak self] in
+ guard let self = self else { return }
+
+ var newAccountExpiry: Date?
+ var newDeviceRevoked: Bool?
+
+ switch accountOperation.completion {
+ case let .failure(error):
+ self.providerLogger.error(
+ error: error,
+ message: "Failed to fetch account data."
+ )
+
+ case let .success(accountData):
+ newAccountExpiry = accountData.expiry
+
+ case .none, .cancelled: break
+ }
+
+ switch deviceOperation.completion {
+ case let .failure(error):
+ if error.compareErrorCode(.deviceNotFound) {
+ newDeviceRevoked = true
+ } else {
+ self.providerLogger.error(
+ error: error,
+ message: "Failed to fetch device data."
+ )
+ }
+
+ case .none, .cancelled, .success: break
+ }
+
+ if var deviceCheck = self.deviceCheck {
+ deviceCheck.update(
+ accountExpiry: newAccountExpiry,
+ isDeviceRevoked: newDeviceRevoked
+ )
+
+ self.deviceCheck = deviceCheck
+ } else {
+ self.deviceCheck = DeviceCheck(
+ identifier: UUID(),
+ isDeviceRevoked: newDeviceRevoked,
+ accountExpiry: newAccountExpiry
+ )
+ }
+
+ if newDeviceRevoked ?? false {
+ self.tunnelMonitor.stop()
+ }
+ }
+
+ completionOperation.addDependencies([accountOperation, deviceOperation])
+
+ let groupOperation = GroupOperation(operations: [
+ accountOperation,
+ deviceOperation,
+ completionOperation,
+ ])
+ checkDeviceStateTask = groupOperation
+
+ operationQueue.addOperation(groupOperation)
+ }
+
+ private func createGetAccountDataOperation(accountNumber: String)
+ -> ResultOperation<REST.AccountData, REST.Error>
+ {
+ let operation = ResultBlockOperation<REST.AccountData, REST.Error>(
+ dispatchQueue: dispatchQueue
+ )
+
+ operation.setExecutionBlock { operation in
+ let task = self.accountProxy.getAccountData(
+ accountNumber: accountNumber,
+ retryStrategy: .noRetry
+ ) { completion in
+ operation.finish(completion: completion)
+ }
+
+ operation.addCancellationBlock {
+ task.cancel()
+ }
+ }
+
+ return operation
+ }
+
+ private func createGetDeviceDataOperation(accountNumber: String, identifier: String)
+ -> ResultOperation<REST.Device, REST.Error>
+ {
+ let operation = ResultBlockOperation<REST.Device, REST.Error>(dispatchQueue: dispatchQueue)
+
+ operation.setExecutionBlock { operation in
+ let task = self.deviceProxy.getDevice(
+ accountNumber: accountNumber,
+ identifier: identifier,
+ retryStrategy: .noRetry
+ ) { completion in
+ operation.finish(completion: completion)
+ }
+
+ operation.addCancellationBlock {
+ task.cancel()
+ }
+ }
+
+ return operation
+ }
}
/// Enum describing the next relay to connect to.
@@ -461,3 +639,23 @@ private enum NextRelay {
/// Determine next relay using relay selector.
case automatic
}
+
+extension DeviceCheck {
+ mutating func update(accountExpiry: Date?, isDeviceRevoked: Bool?) {
+ var shouldChangeIdentifier = false
+
+ if let accountExpiry = accountExpiry, self.accountExpiry != accountExpiry {
+ shouldChangeIdentifier = true
+ self.accountExpiry = accountExpiry
+ }
+
+ if let isDeviceRevoked = isDeviceRevoked, self.isDeviceRevoked != isDeviceRevoked {
+ shouldChangeIdentifier = true
+ self.isDeviceRevoked = isDeviceRevoked
+ }
+
+ if shouldChangeIdentifier {
+ identifier = UUID()
+ }
+ }
+}