diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-09-20 15:15:25 +0200 |
|---|---|---|
| committer | Bug Magnet <marco.nikic@mullvad.net> | 2023-09-21 13:42:23 +0200 |
| commit | e98d4400c9287c377ebb46301e34c4bdd165cc43 (patch) | |
| tree | 34756cf9a55ebf4e78705c3bc0b6e666f9ecfe9c /ios/PacketTunnelCore | |
| parent | db1ffbbfb157b419c303f8d87182f2acc3da9a0a (diff) | |
| download | mullvadvpn-e98d4400c9287c377ebb46301e34c4bdd165cc43.tar.xz mullvadvpn-e98d4400c9287c377ebb46301e34c4bdd165cc43.zip | |
Merge TunnelProviderMessaging into PacketTunnelCore
Diffstat (limited to 'ios/PacketTunnelCore')
10 files changed, 536 insertions, 0 deletions
diff --git a/ios/PacketTunnelCore/IPC/PacketTunnelErrorWrapper.swift b/ios/PacketTunnelCore/IPC/PacketTunnelErrorWrapper.swift new file mode 100644 index 0000000000..381b64898a --- /dev/null +++ b/ios/PacketTunnelCore/IPC/PacketTunnelErrorWrapper.swift @@ -0,0 +1,49 @@ +// +// PacketTunnelErrorWrapper.swift +// MullvadTypes +// +// Created by Sajad Vishkai on 2022-11-28. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +public enum PacketTunnelErrorWrapper: Codable, Equatable, LocalizedError { + public enum ConfigurationFailureCause: Codable, Equatable { + /// Device is locked. + case deviceLocked + + /// Settings schema is outdated. + case outdatedSchema + + /// No relay satisfying constraints. + case noRelaysSatisfyingConstraints + + /// Read error. + case readFailure + } + + /// Failure that indicates WireGuard errors. + case wireguard(String) + + /// Failure to read stored settings. + case configuration(ConfigurationFailureCause) + + public var errorDescription: String? { + switch self { + case let .wireguard(error): + return error + case let .configuration(cause): + switch cause { + case .deviceLocked: + return "Device is locked." + case .outdatedSchema: + return "Settings schema is outdated." + case .readFailure: + return "Failure to read VPN configuration." + case .noRelaysSatisfyingConstraints: + return "No relays satisfying constraints." + } + } + } +} diff --git a/ios/PacketTunnelCore/IPC/PacketTunnelOptions.swift b/ios/PacketTunnelCore/IPC/PacketTunnelOptions.swift new file mode 100644 index 0000000000..8290895294 --- /dev/null +++ b/ios/PacketTunnelCore/IPC/PacketTunnelOptions.swift @@ -0,0 +1,63 @@ +// +// PacketTunnelOptions.swift +// PacketTunnelCore +// +// Created by pronebird on 22/08/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import RelaySelector + +public struct PacketTunnelOptions { + /// Keys for options dictionary + private enum Keys: String { + /// Option key that holds the `NSData` value with `RelaySelectorResult` + /// encoded using `JSONEncoder`. + /// Used for passing the pre-selected relay in the GUI process to the Packet tunnel process. + case relaySelectorResult = "relay-selector-result" + + /// Option key that holds the `NSNumber` value, which is when set to `1` indicates that + /// the tunnel was started by the system. + /// System automatically provides that flag to the tunnel. + case isOnDemand = "is-on-demand" + } + + private var _rawOptions: [String: NSObject] + + public func rawOptions() -> [String: NSObject] { + _rawOptions + } + + public init() { + _rawOptions = [:] + } + + public init(rawOptions: [String: NSObject]) { + _rawOptions = rawOptions + } + + public func getSelectorResult() throws -> RelaySelectorResult? { + guard let data = _rawOptions[Keys.relaySelectorResult.rawValue] as? Data else { return nil } + + return try Self.decode(RelaySelectorResult.self, data) + } + + public mutating func setSelectorResult(_ value: RelaySelectorResult) throws { + _rawOptions[Keys.relaySelectorResult.rawValue] = try Self.encode(value) as NSData + } + + public func isOnDemand() -> Bool { + _rawOptions[Keys.isOnDemand.rawValue] as? Int == 1 + } + + /// Encode custom parameter value + private static func encode(_ value: some Codable) throws -> Data { + try JSONEncoder().encode(value) + } + + /// Decode custom parameter value + private static func decode<T: Codable>(_ type: T.Type, _ data: Data) throws -> T { + try JSONDecoder().decode(T.self, from: data) + } +} diff --git a/ios/PacketTunnelCore/IPC/PacketTunnelRelay.swift b/ios/PacketTunnelCore/IPC/PacketTunnelRelay.swift new file mode 100644 index 0000000000..4f69837240 --- /dev/null +++ b/ios/PacketTunnelCore/IPC/PacketTunnelRelay.swift @@ -0,0 +1,37 @@ +// +// PacketTunnelRelay.swift +// PacketTunnelCore +// +// Created by pronebird on 21/10/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadTypes + +/// Struct holding tunnel relay information. +public struct PacketTunnelRelay: Codable, Equatable { + /// IPv4 relay endpoint. + public let ipv4Relay: IPv4Endpoint + + /// IPv6 relay endpoint. + public let ipv6Relay: IPv6Endpoint? + + /// Relay hostname. + public let hostname: String + + /// Relay location. + public let location: Location + + public init( + ipv4Relay: IPv4Endpoint, + ipv6Relay: IPv6Endpoint? = nil, + hostname: String, + location: Location + ) { + self.ipv4Relay = ipv4Relay + self.ipv6Relay = ipv6Relay + self.hostname = hostname + self.location = location + } +} diff --git a/ios/PacketTunnelCore/IPC/PacketTunnelStatus.swift b/ios/PacketTunnelCore/IPC/PacketTunnelStatus.swift new file mode 100644 index 0000000000..5eece926a6 --- /dev/null +++ b/ios/PacketTunnelCore/IPC/PacketTunnelStatus.swift @@ -0,0 +1,112 @@ +// +// PacketTunnelStatus.swift +// PacketTunnelCore +// +// Created by pronebird on 27/07/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadTypes + +/// The verdict of an account status check. +public enum AccountVerdict: Equatable, Codable { + /// 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. +public enum DeviceVerdict: Equatable, Codable { + /// 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. +public enum KeyRotationStatus: Equatable, Codable { + /// 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`. + public 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. + */ +public struct DeviceCheck: Codable, Equatable { + /// The verdict of account status check. + public var accountVerdict: AccountVerdict + + /// The verdict of device status check. + public var deviceVerdict: DeviceVerdict + + // The status of the last performed key rotation. + public var keyRotationStatus: KeyRotationStatus + + public init( + accountVerdict: AccountVerdict, + deviceVerdict: DeviceVerdict, + keyRotationStatus: KeyRotationStatus + ) { + self.accountVerdict = accountVerdict + self.deviceVerdict = deviceVerdict + self.keyRotationStatus = keyRotationStatus + } +} + +/// Struct describing packet tunnel process status. +public struct PacketTunnelStatus: Codable, Equatable { + /// Last tunnel error. + public var lastErrors: [PacketTunnelErrorWrapper] + + /// Flag indicating whether network is reachable. + public var isNetworkReachable: Bool + + /// Last performed device check. + public var deviceCheck: DeviceCheck? + + /// Current relay. + public var tunnelRelay: PacketTunnelRelay? + + /// Number of consecutive connection failure attempts. + public var numberOfFailedAttempts: UInt + + public init( + lastErrors: [PacketTunnelErrorWrapper] = [], + isNetworkReachable: Bool = true, + deviceCheck: DeviceCheck? = nil, + tunnelRelay: PacketTunnelRelay? = nil, + numberOfFailedAttempts: UInt = 0 + ) { + self.lastErrors = lastErrors + self.isNetworkReachable = isNetworkReachable + self.deviceCheck = deviceCheck + self.tunnelRelay = tunnelRelay + self.numberOfFailedAttempts = numberOfFailedAttempts + } +} diff --git a/ios/PacketTunnelCore/IPC/RelaySelectorResult+PacketTunnelRelay.swift b/ios/PacketTunnelCore/IPC/RelaySelectorResult+PacketTunnelRelay.swift new file mode 100644 index 0000000000..4169f7bca8 --- /dev/null +++ b/ios/PacketTunnelCore/IPC/RelaySelectorResult+PacketTunnelRelay.swift @@ -0,0 +1,21 @@ +// +// RelaySelectorResult+PacketTunnelRelay.swift +// PacketTunnelCore +// +// Created by pronebird on 20/09/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import RelaySelector + +extension RelaySelectorResult { + public var packetTunnelRelay: PacketTunnelRelay { + PacketTunnelRelay( + ipv4Relay: endpoint.ipv4Relay, + ipv6Relay: endpoint.ipv6Relay, + hostname: relay.hostname, + location: location + ) + } +} diff --git a/ios/PacketTunnelCore/IPC/TunnelProviderMessage.swift b/ios/PacketTunnelCore/IPC/TunnelProviderMessage.swift new file mode 100644 index 0000000000..231c1cd858 --- /dev/null +++ b/ios/PacketTunnelCore/IPC/TunnelProviderMessage.swift @@ -0,0 +1,52 @@ +// +// TunnelProviderMessage.swift +// PacketTunnelCore +// +// Created by pronebird on 27/07/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import RelaySelector + +/// Enum describing supported app messages handled by packet tunnel provider. +public enum TunnelProviderMessage: Codable, CustomStringConvertible { + /// Request the tunnel to reconnect. + /// The packet tunnel reconnects to the current relay when selector result is `nil`. + case reconnectTunnel(RelaySelectorResult?) + + /// Request the tunnel status. + case getTunnelStatus + + /// Send HTTP request outside of VPN tunnel. + case sendURLRequest(ProxyURLRequest) + + /// Cancel HTTP request sent outside of VPN tunnel. + case cancelURLRequest(UUID) + + /// Notify tunnel about private key rotation. + case privateKeyRotation + + public var description: String { + switch self { + case .reconnectTunnel: + return "reconnect-tunnel" + case .getTunnelStatus: + return "get-tunnel-status" + case .sendURLRequest: + return "send-http-request" + case .cancelURLRequest: + return "cancel-http-request" + case .privateKeyRotation: + return "private-key-rotation" + } + } + + public init(messageData: Data) throws { + self = try JSONDecoder().decode(Self.self, from: messageData) + } + + public func encode() throws -> Data { + try JSONEncoder().encode(self) + } +} diff --git a/ios/PacketTunnelCore/IPC/TunnelProviderReply.swift b/ios/PacketTunnelCore/IPC/TunnelProviderReply.swift new file mode 100644 index 0000000000..d0c0c80b1d --- /dev/null +++ b/ios/PacketTunnelCore/IPC/TunnelProviderReply.swift @@ -0,0 +1,26 @@ +// +// TunnelProviderReply.swift +// PacketTunnelCore +// +// Created by pronebird on 20/10/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Container type for tunnel provider reply. +public struct TunnelProviderReply<T: Codable>: Codable { + public var value: T + + public init(_ value: T) { + self.value = value + } + + public init(messageData: Data) throws { + self = try JSONDecoder().decode(Self.self, from: messageData) + } + + public func encode() throws -> Data { + try JSONEncoder().encode(self) + } +} diff --git a/ios/PacketTunnelCore/URLRequestProxy/ProxyURLRequest.swift b/ios/PacketTunnelCore/URLRequestProxy/ProxyURLRequest.swift new file mode 100644 index 0000000000..e7b52c36c2 --- /dev/null +++ b/ios/PacketTunnelCore/URLRequestProxy/ProxyURLRequest.swift @@ -0,0 +1,36 @@ +// +// ProxyURLRequest.swift +// PacketTunnelCore +// +// Created by Sajad Vishkai on 2022-10-03. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Struct describing serializable URLRequest data. +public struct ProxyURLRequest: Codable { + public let id: UUID + public let url: URL + public let method: String? + public let httpBody: Data? + public let httpHeaders: [String: String]? + + public var urlRequest: URLRequest { + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = method + urlRequest.httpBody = httpBody + urlRequest.allHTTPHeaderFields = httpHeaders + return urlRequest + } + + public init?(id: UUID, urlRequest: URLRequest) { + guard let urlRequestUrl = urlRequest.url else { return nil } + + self.id = id + url = urlRequestUrl + method = urlRequest.httpMethod + httpBody = urlRequest.httpBody + httpHeaders = urlRequest.allHTTPHeaderFields + } +} diff --git a/ios/PacketTunnelCore/URLRequestProxy/ProxyURLResponse.swift b/ios/PacketTunnelCore/URLRequestProxy/ProxyURLResponse.swift new file mode 100644 index 0000000000..1e13bf9b83 --- /dev/null +++ b/ios/PacketTunnelCore/URLRequestProxy/ProxyURLResponse.swift @@ -0,0 +1,65 @@ +// +// ProxyURLResponse.swift +// PacketTunnelCore +// +// Created by pronebird on 20/10/2022. +// Copyright © 2022 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/// Struct describing serializable URLResponse data. +public struct ProxyURLResponse: Codable { + public let data: Data? + public let response: HTTPURLResponseWrapper? + public let error: URLErrorWrapper? + + public init(data: Data?, response: URLResponse?, error: Error?) { + self.data = data + self.response = response.flatMap { HTTPURLResponseWrapper($0) } + self.error = error.flatMap { URLErrorWrapper($0) } + } +} + +public struct URLErrorWrapper: Codable { + public let code: Int? + public let localizedDescription: String + + public init?(_ error: Error) { + localizedDescription = error.localizedDescription + code = (error as? URLError)?.errorCode + } + + public var originalError: Error? { + guard let code else { return nil } + + return URLError(URLError.Code(rawValue: code)) + } +} + +public struct HTTPURLResponseWrapper: Codable { + public let url: URL? + public let statusCode: Int + public let headerFields: [String: String]? + + public init?(_ response: URLResponse) { + guard let response = response as? HTTPURLResponse else { return nil } + + url = response.url + statusCode = response.statusCode + headerFields = Dictionary( + uniqueKeysWithValues: response.allHeaderFields.map { ("\($0)", "\($1)") } + ) + } + + public var originalResponse: HTTPURLResponse? { + guard let url else { return nil } + + return HTTPURLResponse( + url: url, + statusCode: statusCode, + httpVersion: nil, + headerFields: headerFields + ) + } +} diff --git a/ios/PacketTunnelCore/URLRequestProxy/URLRequestProxy.swift b/ios/PacketTunnelCore/URLRequestProxy/URLRequestProxy.swift new file mode 100644 index 0000000000..a5b3be3bc2 --- /dev/null +++ b/ios/PacketTunnelCore/URLRequestProxy/URLRequestProxy.swift @@ -0,0 +1,75 @@ +// +// URLRequestProxy.swift +// PacketTunnelCore +// +// Created by pronebird on 03/02/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadREST +import MullvadTransport +import MullvadTypes + +public final class URLRequestProxy { + /// Serial queue used for synchronizing access to class members. + private let dispatchQueue: DispatchQueue + + private let transportProvider: RESTTransportProvider + + /// List of all proxied network requests bypassing VPN. + private var proxiedRequests: [UUID: Cancellable] = [:] + + public init( + dispatchQueue: DispatchQueue, + transportProvider: RESTTransportProvider + ) { + self.dispatchQueue = dispatchQueue + self.transportProvider = transportProvider + } + + public func sendRequest( + _ proxyRequest: ProxyURLRequest, + completionHandler: @escaping (ProxyURLResponse) -> Void + ) { + dispatchQueue.async { + guard let transportProvider = self.transportProvider.makeTransport() else { return } + + // The task sent by `transport.sendRequest` comes in an already resumed state + let task = transportProvider.sendRequest(proxyRequest.urlRequest) { [weak self] data, response, error in + guard let self else { return } + // However there is no guarantee about which queue the execution resumes on + // Use `dispatchQueue` to guarantee thread safe access to `proxiedRequests` + dispatchQueue.async { + let response = ProxyURLResponse(data: data, response: response, error: error) + _ = self.removeRequest(identifier: proxyRequest.id) + + completionHandler(response) + } + } + + // All tasks should have unique identifiers, but if not, cancel the task scheduled + // earlier. + let oldTask = self.addRequest(identifier: proxyRequest.id, task: task) + oldTask?.cancel() + } + } + + public func cancelRequest(identifier: UUID) { + dispatchQueue.async { + let task = self.removeRequest(identifier: identifier) + + task?.cancel() + } + } + + private func addRequest(identifier: UUID, task: Cancellable) -> Cancellable? { + dispatchPrecondition(condition: .onQueue(dispatchQueue)) + return proxiedRequests.updateValue(task, forKey: identifier) + } + + private func removeRequest(identifier: UUID) -> Cancellable? { + dispatchPrecondition(condition: .onQueue(dispatchQueue)) + return proxiedRequests.removeValue(forKey: identifier) + } +} |
