diff options
| author | Jon Petersson <jon.petersson@mullvad.net> | 2025-01-16 15:52:16 +0100 |
|---|---|---|
| committer | Jon Petersson <jon.petersson@mullvad.net> | 2025-02-21 15:08:01 +0100 |
| commit | 041bf3f20f3cb0ed6703065eee4d09b5f938f730 (patch) | |
| tree | 13ed3e3895269a8bb6980b5d6eab3238f1e09252 | |
| parent | 9a8535ef787d784d2d75dbd1474e1a1846413e83 (diff) | |
| download | mullvadvpn-041bf3f20f3cb0ed6703065eee4d09b5f938f730.tar.xz mullvadvpn-041bf3f20f3cb0ed6703065eee4d09b5f938f730.zip | |
Implement an FFI to fetch API IP addresses using mullvad-api
29 files changed, 1039 insertions, 31 deletions
diff --git a/Cargo.lock b/Cargo.lock index c55f31cec7..3e0c42ae83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2520,11 +2520,14 @@ name = "mullvad-ios" version = "0.0.0" dependencies = [ "cbindgen 0.28.0", + "hyper", "hyper-util", "libc", "log", + "mullvad-api", "mullvad-encrypted-dns-proxy", "oslog", + "serde_json", "shadowsocks-service", "talpid-tunnel-config-client", "talpid-types", diff --git a/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift b/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift index 1330be345f..3c8350b8fb 100644 --- a/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift +++ b/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift @@ -12,6 +12,13 @@ import MullvadTypes import WireGuardKitTypes struct APIProxyStub: APIQuerying { + func mullvadApiGetAddressList( + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping ProxyCompletionHandler<[AnyIPEndpoint]> + ) -> Cancellable { + AnyCancellable() + } + func getAddressList( retryStrategy: REST.RetryStrategy, completionHandler: @escaping ProxyCompletionHandler<[AnyIPEndpoint]> diff --git a/ios/MullvadMockData/MullvadREST/MockProxyFactory.swift b/ios/MullvadMockData/MullvadREST/MockProxyFactory.swift index ad8192963b..f6a101808c 100644 --- a/ios/MullvadMockData/MullvadREST/MockProxyFactory.swift +++ b/ios/MullvadMockData/MullvadREST/MockProxyFactory.swift @@ -8,6 +8,7 @@ import Foundation import MullvadREST +import MullvadRustRuntime import MullvadTypes import WireGuardKitTypes @@ -28,11 +29,13 @@ public struct MockProxyFactory: ProxyFactoryProtocol { public static func makeProxyFactory( transportProvider: any RESTTransportProvider, - addressCache: REST.AddressCache + addressCache: REST.AddressCache, + apiContext: MullvadApiContext ) -> any ProxyFactoryProtocol { let basicConfiguration = REST.ProxyConfiguration( transportProvider: transportProvider, - addressCacheStore: addressCache + addressCacheStore: addressCache, + apiContext: apiContext ) let authenticationProxy = REST.AuthenticationProxy( @@ -44,7 +47,8 @@ public struct MockProxyFactory: ProxyFactoryProtocol { let authConfiguration = REST.AuthProxyConfiguration( proxyConfiguration: basicConfiguration, - accessTokenManager: accessTokenManager + accessTokenManager: accessTokenManager, + apiContext: apiContext ) return MockProxyFactory(configuration: authConfiguration) diff --git a/ios/MullvadREST/ApiHandlers/MullvadApiRequestFactory.swift b/ios/MullvadREST/ApiHandlers/MullvadApiRequestFactory.swift new file mode 100644 index 0000000000..89bf2dd725 --- /dev/null +++ b/ios/MullvadREST/ApiHandlers/MullvadApiRequestFactory.swift @@ -0,0 +1,37 @@ +// +// MullvadApiRequestFactory.swift +// MullvadVPN +// +// Created by Jon Petersson on 2025-02-07. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import MullvadRustRuntime +import MullvadTypes + +enum MullvadApiRequest { + case getAddressList +} + +struct MullvadApiRequestFactory { + let apiContext: MullvadApiContext + + func makeRequest(_ request: MullvadApiRequest) -> REST.MullvadApiRequestHandler { + { completion in + let pointerClass = MullvadApiCompletion { apiResponse in + try? completion?(apiResponse) + } + + let rawPointer = Unmanaged.passRetained(pointerClass).toOpaque() + + return switch request { + case .getAddressList: + MullvadApiCancellable(handle: mullvad_api_get_addresses(apiContext.context, rawPointer)) + } + } + } +} + +extension REST { + typealias MullvadApiRequestHandler = (((MullvadApiResponse) throws -> Void)?) -> MullvadApiCancellable +} diff --git a/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift index 5ac58668c0..4c8e144d6d 100644 --- a/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift +++ b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift @@ -7,10 +7,17 @@ // import Foundation +import MullvadRustRuntime import MullvadTypes +import Operations import WireGuardKitTypes public protocol APIQuerying: Sendable { + func mullvadApiGetAddressList( + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]> + ) -> Cancellable + func getAddressList( retryStrategy: REST.RetryStrategy, completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]> @@ -55,6 +62,32 @@ extension REST { ) } + public func mullvadApiGetAddressList( + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]> + ) -> Cancellable { + let requestHandler = mullvadApiRequestFactory.makeRequest(.getAddressList) + + let responseHandler = rustResponseHandler( + decoding: [AnyIPEndpoint].self, + with: responseDecoder + ) + + let networkOperation = MullvadApiNetworkOperation( + name: "get-api-addrs", + dispatchQueue: dispatchQueue, + retryStrategy: retryStrategy, + requestHandler: requestHandler, + responseDecoder: responseDecoder, + responseHandler: responseHandler, + completionHandler: completionHandler + ) + + operationQueue.addOperation(networkOperation) + + return networkOperation + } + public func getAddressList( retryStrategy: REST.RetryStrategy, completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]> diff --git a/ios/MullvadREST/ApiHandlers/RESTDefaults.swift b/ios/MullvadREST/ApiHandlers/RESTDefaults.swift index 250401b019..d115abc37a 100644 --- a/ios/MullvadREST/ApiHandlers/RESTDefaults.swift +++ b/ios/MullvadREST/ApiHandlers/RESTDefaults.swift @@ -7,6 +7,7 @@ // import Foundation +import MullvadRustRuntime import MullvadTypes // swiftlint:disable force_cast @@ -28,6 +29,13 @@ extension REST { /// Default network timeout for API requests. public static let defaultAPINetworkTimeout: Duration = .seconds(10) + + /// API context used for API requests via Rust runtime. + // swiftlint:disable:next force_try + public static let apiContext = try! MullvadApiContext( + host: defaultAPIHostname, + address: defaultAPIEndpoint + ) } // swiftlint:enable force_cast diff --git a/ios/MullvadREST/ApiHandlers/RESTProxy.swift b/ios/MullvadREST/ApiHandlers/RESTProxy.swift index 0da8ab546c..75d1e31fd8 100644 --- a/ios/MullvadREST/ApiHandlers/RESTProxy.swift +++ b/ios/MullvadREST/ApiHandlers/RESTProxy.swift @@ -7,6 +7,7 @@ // import Foundation +import MullvadRustRuntime import MullvadTypes import Operations @@ -26,6 +27,8 @@ extension REST { /// URL request factory. let requestFactory: REST.RequestFactory + let mullvadApiRequestFactory: MullvadApiRequestFactory + /// URL response decoder. let responseDecoder: JSONDecoder @@ -40,6 +43,7 @@ extension REST { self.configuration = configuration self.requestFactory = requestFactory + self.mullvadApiRequestFactory = MullvadApiRequestFactory(apiContext: configuration.apiContext) self.responseDecoder = responseDecoder } @@ -132,13 +136,16 @@ extension REST { public class ProxyConfiguration: @unchecked Sendable { public let transportProvider: RESTTransportProvider public let addressCacheStore: AddressCache + public let apiContext: MullvadApiContext public init( transportProvider: RESTTransportProvider, - addressCacheStore: AddressCache + addressCacheStore: AddressCache, + apiContext: MullvadApiContext ) { self.transportProvider = transportProvider self.addressCacheStore = addressCacheStore + self.apiContext = apiContext } } @@ -147,13 +154,15 @@ extension REST { public init( proxyConfiguration: ProxyConfiguration, - accessTokenManager: RESTAccessTokenManagement + accessTokenManager: RESTAccessTokenManagement, + apiContext: MullvadApiContext ) { self.accessTokenManager = accessTokenManager super.init( transportProvider: proxyConfiguration.transportProvider, - addressCacheStore: proxyConfiguration.addressCacheStore + addressCacheStore: proxyConfiguration.addressCacheStore, + apiContext: apiContext ) } } diff --git a/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift b/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift index 331fb49030..46acaa94bf 100644 --- a/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift +++ b/ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift @@ -7,6 +7,8 @@ // import Foundation +import MullvadRustRuntime + public protocol ProxyFactoryProtocol { var configuration: REST.AuthProxyConfiguration { get } @@ -16,7 +18,8 @@ public protocol ProxyFactoryProtocol { static func makeProxyFactory( transportProvider: RESTTransportProvider, - addressCache: REST.AddressCache + addressCache: REST.AddressCache, + apiContext: MullvadApiContext ) -> ProxyFactoryProtocol } @@ -26,11 +29,13 @@ extension REST { public static func makeProxyFactory( transportProvider: any RESTTransportProvider, - addressCache: REST.AddressCache + addressCache: REST.AddressCache, + apiContext: MullvadApiContext ) -> any ProxyFactoryProtocol { let basicConfiguration = REST.ProxyConfiguration( transportProvider: transportProvider, - addressCacheStore: addressCache + addressCacheStore: addressCache, + apiContext: apiContext ) let authenticationProxy = REST.AuthenticationProxy( @@ -42,7 +47,8 @@ extension REST { let authConfiguration = REST.AuthProxyConfiguration( proxyConfiguration: basicConfiguration, - accessTokenManager: accessTokenManager + accessTokenManager: accessTokenManager, + apiContext: apiContext ) return ProxyFactory(configuration: authConfiguration) diff --git a/ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift b/ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift index 9790514507..1b6d7f950b 100644 --- a/ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift +++ b/ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift @@ -7,6 +7,7 @@ // import Foundation +import MullvadRustRuntime import MullvadTypes protocol RESTResponseHandler<Success> { @@ -15,7 +16,14 @@ protocol RESTResponseHandler<Success> { func handleURLResponse(_ response: HTTPURLResponse, data: Data) -> REST.ResponseHandlerResult<Success> } +protocol RESTRustResponseHandler<Success> { + associatedtype Success + + func handleResponse(_ response: MullvadApiResponse) -> REST.ResponseHandlerResult<Success> +} + extension REST { + // TODO: We could probably remove the `decoding` case when network requests are fully merged to Mullvad API. /// Responser handler result type. enum ResponseHandlerResult<Success> { /// Response handler succeeded and produced a value. @@ -66,4 +74,53 @@ extension REST { } } } + + final class RustResponseHandler<Success>: RESTRustResponseHandler { + typealias HandlerBlock = (MullvadApiResponse) -> REST.ResponseHandlerResult<Success> + + private let handlerBlock: HandlerBlock + + init(_ block: @escaping HandlerBlock) { + handlerBlock = block + } + + func handleResponse(_ response: MullvadApiResponse) -> REST.ResponseHandlerResult<Success> { + handlerBlock(response) + } + } + + /// Returns default response handler that parses JSON response into the + /// given `Decodable` type if possible, otherwise attempts to decode + /// the server error. + static func rustResponseHandler<T: Decodable>( + decoding type: T.Type, + with decoder: JSONDecoder + ) -> RustResponseHandler<T> { + RustResponseHandler { response in + guard let body = response.body else { + return .unhandledResponse(nil) + } + + do { + let decoded = try decoder.decode(type, from: body) + return .decoding { decoded } + } catch { + return .unhandledResponse( + try? decoder.decode( + ServerErrorResponse.self, + from: body + ) + ) + } + } + } + + /// Returns default response handler that parses JSON response into the + /// given `Decodable` type if possible, otherwise attempts to decode + /// the server error. + static func rustEmptyResponseHandler() -> RustResponseHandler<Void> { + RustResponseHandler { _ in + .success(()) + } + } } diff --git a/ios/MullvadREST/ApiHandlers/RESTRustNetworkOperation.swift b/ios/MullvadREST/ApiHandlers/RESTRustNetworkOperation.swift new file mode 100644 index 0000000000..1bcd218444 --- /dev/null +++ b/ios/MullvadREST/ApiHandlers/RESTRustNetworkOperation.swift @@ -0,0 +1,168 @@ +// +// RESTRustNetworkOperation.swift +// MullvadREST +// +// Created by Jon Petersson on 2025-01-29. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadLogging +import MullvadRustRuntime +import MullvadTypes +import Operations + +extension REST { + class MullvadApiNetworkOperation<Success: Sendable>: ResultOperation<Success>, @unchecked Sendable { + private let logger: Logger + + private let requestHandler: MullvadApiRequestHandler + private var responseDecoder: JSONDecoder + private let responseHandler: any RESTRustResponseHandler<Success> + private var networkTask: MullvadApiCancellable? + + private let retryStrategy: RetryStrategy + private var retryDelayIterator: AnyIterator<Duration> + private var retryTimer: DispatchSourceTimer? + private var retryCount = 0 + + init( + name: String, + dispatchQueue: DispatchQueue, + retryStrategy: RetryStrategy, + requestHandler: @escaping MullvadApiRequestHandler, + responseDecoder: JSONDecoder, + responseHandler: some RESTRustResponseHandler<Success>, + completionHandler: CompletionHandler? = nil + ) { + self.retryStrategy = retryStrategy + retryDelayIterator = retryStrategy.makeDelayIterator() + self.responseDecoder = responseDecoder + self.requestHandler = requestHandler + self.responseHandler = responseHandler + + var logger = Logger(label: "REST.RustNetworkOperation") + logger[metadataKey: "name"] = .string(name) + self.logger = logger + + super.init( + dispatchQueue: dispatchQueue, + completionQueue: .main, + completionHandler: completionHandler + ) + } + + override public func operationDidCancel() { + retryTimer?.cancel() + networkTask?.cancel() + + retryTimer = nil + networkTask = nil + } + + override public func main() { + startRequest() + } + + func startRequest() { + dispatchPrecondition(condition: .onQueue(dispatchQueue)) + + guard !isCancelled else { + finish(result: .failure(OperationError.cancelled)) + return + } + + networkTask = requestHandler { [weak self] response in + guard let self else { return } + + if let error = response.restError() { + if response.shouldRetry { + retryRequest(with: error) + } else { + finish(result: .failure(error)) + } + + return + } + + let decodedResponse = responseHandler.handleResponse(response) + + switch decodedResponse { + case let .success(value): + finish(result: .success(value)) + case let .decoding(block): + finish(result: .success(try block())) + case let .unhandledResponse(error): + finish(result: .failure(REST.Error.unhandledResponse(Int(response.statusCode), error))) + } + } + } + + private func retryRequest(with error: REST.Error) { + // Check if retry count is not exceeded. + guard retryCount < retryStrategy.maxRetryCount else { + if retryStrategy.maxRetryCount > 0 { + logger.debug("Ran out of retry attempts (\(retryStrategy.maxRetryCount))") + } + finish(result: .failure(error)) + return + } + + // Increment retry count. + retryCount += 1 + + // Retry immediately if retry delay is set to never. + guard retryStrategy.delay != .never else { + startRequest() + return + } + + guard let waitDelay = retryDelayIterator.next() else { + logger.debug("Retry delay iterator failed to produce next value.") + + finish(result: .failure(error)) + return + } + + logger.debug("Retry in \(waitDelay.logFormat()).") + + // Create timer to delay retry. + let timer = DispatchSource.makeTimerSource(queue: dispatchQueue) + + timer.setEventHandler { [weak self] in + self?.startRequest() + } + + timer.setCancelHandler { [weak self] in + self?.finish(result: .failure(OperationError.cancelled)) + } + + timer.schedule(wallDeadline: .now() + waitDelay.timeInterval) + timer.activate() + + retryTimer = timer + } + } +} + +extension MullvadApiResponse { + public func restError() -> REST.Error? { + guard !success else { + return nil + } + + guard let serverResponseCode else { + return .transport(MullvadApiTransportError.connectionFailed(description: errorDescription)) + } + + let response = REST.ServerErrorResponse( + code: REST.ServerResponseCode(rawValue: serverResponseCode), + detail: errorDescription + ) + return .unhandledResponse(Int(statusCode), response) + } +} + +enum MullvadApiTransportError: Error { + case connectionFailed(description: String?) +} diff --git a/ios/MullvadRESTTests/RequestExecutorTests.swift b/ios/MullvadRESTTests/RequestExecutorTests.swift index e4ffa58827..36b3ca2b3c 100644 --- a/ios/MullvadRESTTests/RequestExecutorTests.swift +++ b/ios/MullvadRESTTests/RequestExecutorTests.swift @@ -27,7 +27,8 @@ final class RequestExecutorTests: XCTestCase { let proxyFactory = REST.ProxyFactory.makeProxyFactory( transportProvider: transportProvider, - addressCache: addressCache + addressCache: addressCache, + apiContext: REST.apiContext ) timerServerProxy = TimeServerProxy(configuration: proxyFactory.configuration) } diff --git a/ios/MullvadRustRuntime/MullvadApiCancellable.swift b/ios/MullvadRustRuntime/MullvadApiCancellable.swift new file mode 100644 index 0000000000..0f0e0fe6e4 --- /dev/null +++ b/ios/MullvadRustRuntime/MullvadApiCancellable.swift @@ -0,0 +1,23 @@ +// +// MullvadApiCancellable.swift +// MullvadVPN +// +// Created by Jon Petersson on 2025-02-07. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +public class MullvadApiCancellable { + private let handle: SwiftCancelHandle + + public init(handle: consuming SwiftCancelHandle) { + self.handle = handle + } + + deinit { + mullvad_api_cancel_task_drop(handle) + } + + public func cancel() { + mullvad_api_cancel_task(handle) + } +} diff --git a/ios/MullvadRustRuntime/MullvadApiCompletion.swift b/ios/MullvadRustRuntime/MullvadApiCompletion.swift new file mode 100644 index 0000000000..ca61c6791f --- /dev/null +++ b/ios/MullvadRustRuntime/MullvadApiCompletion.swift @@ -0,0 +1,28 @@ +// +// MullvadApiCompletion.swift +// MullvadVPN +// +// Created by Jon Petersson on 2025-01-16. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +@_silgen_name("mullvad_api_completion_finish") +func mullvadApiCompletionFinish( + response: SwiftMullvadApiResponse, + completionCookie: UnsafeMutableRawPointer +) { + let completionBridge = Unmanaged<MullvadApiCompletion> + .fromOpaque(completionCookie) + .takeRetainedValue() + let apiResponse = MullvadApiResponse(response: response) + + completionBridge.completion(apiResponse) +} + +public class MullvadApiCompletion { + public var completion: (MullvadApiResponse) -> Void + + public init(completion: @escaping ((MullvadApiResponse) -> Void)) { + self.completion = completion + } +} diff --git a/ios/MullvadRustRuntime/MullvadApiContext.swift b/ios/MullvadRustRuntime/MullvadApiContext.swift new file mode 100644 index 0000000000..f637590612 --- /dev/null +++ b/ios/MullvadRustRuntime/MullvadApiContext.swift @@ -0,0 +1,27 @@ +// +// MullvadApiContext.swift +// MullvadVPN +// +// Created by Jon Petersson on 2025-01-24. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import MullvadTypes + +public struct MullvadApiContext: Sendable { + enum MullvadApiContextError: Error { + case failedToConstructApiClient + } + + public let context: SwiftApiContext + + public init(host: String, address: AnyIPEndpoint) throws { + context = mullvad_api_init_new(host, address.description) + + if context._0 == nil { + throw MullvadApiContextError.failedToConstructApiClient + } + } +} + +extension SwiftApiContext: @unchecked @retroactive Sendable {} diff --git a/ios/MullvadRustRuntime/MullvadApiResponse.swift b/ios/MullvadRustRuntime/MullvadApiResponse.swift new file mode 100644 index 0000000000..a709342b4e --- /dev/null +++ b/ios/MullvadRustRuntime/MullvadApiResponse.swift @@ -0,0 +1,55 @@ +// +// MullvadApiResponse.swift +// MullvadVPN +// +// Created by Jon Petersson on 2025-01-24. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +public class MullvadApiResponse { + private let response: SwiftMullvadApiResponse + + public init(response: consuming SwiftMullvadApiResponse) { + self.response = response + } + + deinit { + mullvad_response_drop(response) + } + + public var body: Data? { + guard let body = response.body else { + return nil + } + + return Data(UnsafeBufferPointer(start: body, count: Int(response.body_size))) + } + + public var errorDescription: String? { + return if response.error_description == nil { + nil + } else { + String(cString: response.error_description) + } + } + + public var statusCode: UInt16 { + response.status_code + } + + public var serverResponseCode: String? { + return if response.server_response_code == nil { + nil + } else { + String(cString: response.server_response_code) + } + } + + public var shouldRetry: Bool { + response.should_retry + } + + public var success: Bool { + response.success + } +} diff --git a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h index b10f4f81f2..faae315d6d 100644 --- a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h +++ b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h @@ -14,6 +14,8 @@ enum TunnelObfuscatorProtocol { }; typedef uint8_t TunnelObfuscatorProtocol; +typedef struct ApiContext ApiContext; + /** * A thin wrapper around [`mullvad_encrypted_dns_proxy::state::EncryptedDnsProxyState`] that * can start a local forwarder (see [`Self::start`]). @@ -22,6 +24,31 @@ typedef struct EncryptedDnsProxyState EncryptedDnsProxyState; typedef struct ExchangeCancelToken ExchangeCancelToken; +typedef struct RequestCancelHandle RequestCancelHandle; + +typedef struct SwiftApiContext { + const struct ApiContext *_0; +} SwiftApiContext; + +typedef struct SwiftCancelHandle { + struct RequestCancelHandle *ptr; +} SwiftCancelHandle; + +typedef struct SwiftMullvadApiResponse { + uint8_t *body; + uintptr_t body_size; + uint16_t status_code; + uint8_t *error_description; + uint8_t *server_response_code; + bool success; + bool should_retry; + uint64_t retry_after; +} SwiftMullvadApiResponse; + +typedef struct CompletionCookie { + void *_0; +} CompletionCookie; + typedef struct ProxyHandle { void *context; uint16_t port; @@ -50,6 +77,86 @@ typedef struct EphemeralPeerParameters { extern const uint16_t CONFIG_SERVICE_PORT; /** + * # Safety + * + * `host` must be a pointer to a null terminated string representing a hostname for Mullvad API host. + * This hostname will be used for TLS validation but not used for domain name resolution. + * + * `address` must be a pointer to a null terminated string representing a socket address through which + * the Mullvad API can be reached directly. + * + * If a context cannot be constructed this function will panic since the call site would not be able + * to proceed in a meaningful way anyway. + * + * This function is safe. + */ +struct SwiftApiContext mullvad_api_init_new(const uint8_t *host, + const uint8_t *address); + +/** + * # Safety + * + * `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created + * by calling `mullvad_api_init_new`. + * + * `completion_cookie` must be pointing to a valid instance of `CompletionCookie`. `CompletionCookie` is + * safe because the pointer in `MullvadApiCompletion` is valid for the lifetime of the process where this + * type is intended to be used. + * + * This function is not safe to call multiple times with the same `CompletionCookie`. + */ +struct SwiftCancelHandle mullvad_api_get_addresses(struct SwiftApiContext api_context, + void *completion_cookie); + +/** + * Called by the Swift side to signal that a Mullvad API call should be cancelled. + * After this call, the cancel token is no longer valid. + * + * # Safety + * + * `handle_ptr` must be pointing to a valid instance of `SwiftCancelHandle`. This function + * is not safe to call multiple times with the same `SwiftCancelHandle`. + */ +void mullvad_api_cancel_task(struct SwiftCancelHandle handle_ptr); + +/** + * Called by the Swift side to signal that the Rust `SwiftCancelHandle` can be safely + * dropped from memory. + * + * # Safety + * + * `handle_ptr` must be pointing to a valid instance of `SwiftCancelHandle`. This function + * is not safe to call multiple times with the same `SwiftCancelHandle`. + */ +void mullvad_api_cancel_task_drop(struct SwiftCancelHandle handle_ptr); + +/** + * Maps to `mullvadApiCompletionFinish` on Swift side to facilitate callback based completion flow when doing + * network calls through Mullvad API on Rust side. + * + * # Safety + * + * `response` must be pointing to a valid instance of `SwiftMullvadApiResponse`. + * + * `completion_cookie` must be pointing to a valid instance of `CompletionCookie`. `CompletionCookie` is safe + * because the pointer in `MullvadApiCompletion` is valid for the lifetime of the process where this type is + * intended to be used. + */ +extern void mullvad_api_completion_finish(struct SwiftMullvadApiResponse response, + struct CompletionCookie completion_cookie); + +/** + * Called by the Swift side to signal that the Rust `SwiftMullvadApiResponse` can be safely + * dropped from memory. + * + * # Safety + * + * `response` must be pointing to a valid instance of `SwiftMullvadApiResponse`. This function + * is not safe to call multiple times with the same `SwiftMullvadApiResponse`. + */ +void mullvad_response_drop(struct SwiftMullvadApiResponse response); + +/** * Initializes a valid pointer to an instance of `EncryptedDnsProxyState`. * * # Safety diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index ecb371e025..2b1036fadf 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -504,7 +504,7 @@ 7A2960FD2A964BB700389B82 /* AlertPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2960FC2A964BB700389B82 /* AlertPresentation.swift */; }; 7A307AD92A8CD8DA0017618B /* Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A307AD82A8CD8DA0017618B /* Duration.swift */; }; 7A307ADB2A8F56DF0017618B /* Duration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A307ADA2A8F56DF0017618B /* Duration+Extensions.swift */; }; - 7A3215742D3E5A85005DF395 /* DAITASettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3215732D3E5A7B005DF395 /* DAITASettingsCoordinator.swift */; }; + 7A3215722D3934E6005DF395 /* MullvadApiCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3215702D392F0B005DF395 /* MullvadApiCompletion.swift */; }; 7A33538F2AA9FF1600F0A71C /* SimulatorTunnelProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A33538E2AA9FF1600F0A71C /* SimulatorTunnelProviderManager.swift */; }; 7A3353912AAA014400F0A71C /* SimulatorVPNConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */; }; 7A3353932AAA089000F0A71C /* SimulatorTunnelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */; }; @@ -598,7 +598,10 @@ 7A8A19242CF4C9BF000BCB5B /* MultihopPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A19232CF4C9B8000BCB5B /* MultihopPage.swift */; }; 7A8A19262CF4D37B000BCB5B /* DAITAPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A19252CF4D373000BCB5B /* DAITAPage.swift */; }; 7A8A19282CF603EB000BCB5B /* SettingsViewControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A19272CF603E3000BCB5B /* SettingsViewControllerFactory.swift */; }; - 7A99D36D2D54FCC400891FF7 /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 7A99D36C2D54FCC400891FF7 /* relays.json */; }; + 7A95B6792D5F729300687524 /* DAITASettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A95B6782D5F729300687524 /* DAITASettingsCoordinator.swift */; }; + 7A95B67B2D5F758300687524 /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 7A95B67A2D5F758300687524 /* relays.json */; }; + 7A99D36F2D56070400891FF7 /* MullvadApiRequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A99D36E2D5606F900891FF7 /* MullvadApiRequestFactory.swift */; }; + 7A99D3712D56222000891FF7 /* MullvadApiCancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A99D3702D56220E00891FF7 /* MullvadApiCancellable.swift */; }; 7A9BE5A22B8F88C500E2A7D0 /* LocationNodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9BE5A12B8F88C500E2A7D0 /* LocationNodeTests.swift */; }; 7A9BE5A32B8F89B900E2A7D0 /* LocationNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6389F72B864CDF008E77E1 /* LocationNode.swift */; }; 7A9BE5A52B90760C00E2A7D0 /* CustomListsDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9BE5A42B90760C00E2A7D0 /* CustomListsDataSourceTests.swift */; }; @@ -639,6 +642,9 @@ 7AB3BEB52BD7A6CB00E34384 /* LocationViewControllerWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB3BEB42BD7A6CB00E34384 /* LocationViewControllerWrapper.swift */; }; 7AB4CCB92B69097E006037F5 /* IPOverrideTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4CCB82B69097E006037F5 /* IPOverrideTests.swift */; }; 7AB4CCBB2B691BBB006037F5 /* IPOverrideInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4CCBA2B691BBB006037F5 /* IPOverrideInteractor.swift */; }; + 7AB931242D43C2CA005FCEBA /* MullvadApiContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB931232D43C2C2005FCEBA /* MullvadApiContext.swift */; }; + 7AB931262D43D22F005FCEBA /* MullvadApiResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB931252D43D222005FCEBA /* MullvadApiResponse.swift */; }; + 7AB9312F2D4A5D0A005FCEBA /* RESTRustNetworkOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB9312D2D4A5D0A005FCEBA /* RESTRustNetworkOperation.swift */; }; 7ABCA5B32A9349F20044A708 /* Routing.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A88DCCE2A8FABBE00D2FF0E /* Routing.framework */; }; 7ABCA5B42A9349F20044A708 /* Routing.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7A88DCCE2A8FABBE00D2FF0E /* Routing.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 7ABCA5B72A9353C60044A708 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CAF9F72983D36800BE19F7 /* Coordinator.swift */; }; @@ -1072,11 +1078,10 @@ F0F56B092C0E058A009D676B /* ObserverList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CC40EE24A601900019D96E /* ObserverList.swift */; }; F0FADDEA2BE90AAA000D0B02 /* LaunchArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */; }; F0FADDEC2BE90AB0000D0B02 /* LaunchArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F1EF8C2BE8FF0A00CED01D /* LaunchArguments.swift */; }; + F910A4012D3FF23A002FF3BB /* View+Modifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A4002D3FF22E002FF3BB /* View+Modifier.swift */; }; F910A4312D4A1B41002FF3BB /* InAppPurchaseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A4302D4A1B3B002FF3BB /* InAppPurchaseCoordinator.swift */; }; F910A43A2D4A283D002FF3BB /* InAppPurchaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A4392D4A2839002FF3BB /* InAppPurchaseViewController.swift */; }; F910A8572D523812002FF3BB /* TunnelSettingsV7.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A8562D523812002FF3BB /* TunnelSettingsV7.swift */; }; - F95C1C252D3E5E8E00EBE769 /* UIAlertController+InAppPurchase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F95C1C242D3E5E7A00EBE769 /* UIAlertController+InAppPurchase.swift */; }; - F910A4012D3FF23A002FF3BB /* View+Modifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F910A4002D3FF22E002FF3BB /* View+Modifier.swift */; }; F998EFF82D359C4600D88D01 /* SKProduct+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */; }; F998EFFA2D3656BA00D88D01 /* SKProduct+Sorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = F998EFF92D3656B100D88D01 /* SKProduct+Sorting.swift */; }; /* End PBXBuildFile section */ @@ -2024,7 +2029,7 @@ 7A2960FC2A964BB700389B82 /* AlertPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPresentation.swift; sourceTree = "<group>"; }; 7A307AD82A8CD8DA0017618B /* Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Duration.swift; sourceTree = "<group>"; }; 7A307ADA2A8F56DF0017618B /* Duration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duration+Extensions.swift"; sourceTree = "<group>"; }; - 7A3215732D3E5A7B005DF395 /* DAITASettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAITASettingsCoordinator.swift; sourceTree = "<group>"; }; + 7A3215702D392F0B005DF395 /* MullvadApiCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiCompletion.swift; sourceTree = "<group>"; }; 7A33538E2AA9FF1600F0A71C /* SimulatorTunnelProviderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelProviderManager.swift; sourceTree = "<group>"; }; 7A3353902AAA014400F0A71C /* SimulatorVPNConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorVPNConnection.swift; sourceTree = "<group>"; }; 7A3353922AAA089000F0A71C /* SimulatorTunnelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelInfo.swift; sourceTree = "<group>"; }; @@ -2107,7 +2112,10 @@ 7A8A19232CF4C9B8000BCB5B /* MultihopPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopPage.swift; sourceTree = "<group>"; }; 7A8A19252CF4D373000BCB5B /* DAITAPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAITAPage.swift; sourceTree = "<group>"; }; 7A8A19272CF603E3000BCB5B /* SettingsViewControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewControllerFactory.swift; sourceTree = "<group>"; }; - 7A99D36C2D54FCC400891FF7 /* relays.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = relays.json; sourceTree = "<group>"; }; + 7A95B6782D5F729300687524 /* DAITASettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAITASettingsCoordinator.swift; sourceTree = "<group>"; }; + 7A95B67A2D5F758300687524 /* relays.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = relays.json; sourceTree = "<group>"; }; + 7A99D36E2D5606F900891FF7 /* MullvadApiRequestFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiRequestFactory.swift; sourceTree = "<group>"; }; + 7A99D3702D56220E00891FF7 /* MullvadApiCancellable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiCancellable.swift; sourceTree = "<group>"; }; 7A9BE5A12B8F88C500E2A7D0 /* LocationNodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationNodeTests.swift; sourceTree = "<group>"; }; 7A9BE5A42B90760C00E2A7D0 /* CustomListsDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListsDataSourceTests.swift; sourceTree = "<group>"; }; 7A9BE5A82B90806800E2A7D0 /* CustomListsRepositoryStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListsRepositoryStub.swift; sourceTree = "<group>"; }; @@ -2145,6 +2153,9 @@ 7AB3BEB42BD7A6CB00E34384 /* LocationViewControllerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationViewControllerWrapper.swift; sourceTree = "<group>"; }; 7AB4CCB82B69097E006037F5 /* IPOverrideTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideTests.swift; sourceTree = "<group>"; }; 7AB4CCBA2B691BBB006037F5 /* IPOverrideInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideInteractor.swift; sourceTree = "<group>"; }; + 7AB931232D43C2C2005FCEBA /* MullvadApiContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiContext.swift; sourceTree = "<group>"; }; + 7AB931252D43D222005FCEBA /* MullvadApiResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiResponse.swift; sourceTree = "<group>"; }; + 7AB9312D2D4A5D0A005FCEBA /* RESTRustNetworkOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTRustNetworkOperation.swift; sourceTree = "<group>"; }; 7ABE318C2A1CDD4500DF4963 /* UIFont+Weight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Weight.swift"; sourceTree = "<group>"; }; 7ABFB09D2BA316220074A49E /* RelayConstraintsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConstraintsTests.swift; sourceTree = "<group>"; }; 7AC8A3AD2ABC6FBB00DC4939 /* SettingsHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsHeaderView.swift; sourceTree = "<group>"; }; @@ -2450,11 +2461,10 @@ F0F316182BF3572B0078DBCF /* RelaySelectorResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorResult.swift; sourceTree = "<group>"; }; F0F3161A2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoRelaysSatisfyingConstraintsError.swift; sourceTree = "<group>"; }; F0FBD98E2C4A60CC00EE5323 /* KeyExchangingResultStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyExchangingResultStub.swift; sourceTree = "<group>"; }; - F910A8562D523812002FF3BB /* TunnelSettingsV7.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV7.swift; sourceTree = "<group>"; }; F910A4002D3FF22E002FF3BB /* View+Modifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Modifier.swift"; sourceTree = "<group>"; }; - F95C1C242D3E5E7A00EBE769 /* UIAlertController+InAppPurchase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+InAppPurchase.swift"; sourceTree = "<group>"; }; F910A4302D4A1B3B002FF3BB /* InAppPurchaseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseCoordinator.swift; sourceTree = "<group>"; }; F910A4392D4A2839002FF3BB /* InAppPurchaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseViewController.swift; sourceTree = "<group>"; }; + F910A8562D523812002FF3BB /* TunnelSettingsV7.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV7.swift; sourceTree = "<group>"; }; F998EFF92D3656B100D88D01 /* SKProduct+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SKProduct+Sorting.swift"; sourceTree = "<group>"; }; /* End PBXFileReference section */ @@ -2643,7 +2653,7 @@ isa = PBXGroup; children = ( 06799AB428F98CE700ACD94E /* le_root_cert.cer */, - 7A99D36C2D54FCC400891FF7 /* relays.json */, + 7A95B67A2D5F758300687524 /* relays.json */, ); path = Assets; sourceTree = "<group>"; @@ -4232,7 +4242,7 @@ 7A8A19082CE5FFD7000BCB5B /* DAITA */ = { isa = PBXGroup; children = ( - 7A3215732D3E5A7B005DF395 /* DAITASettingsCoordinator.swift */, + 7A95B6782D5F729300687524 /* DAITASettingsCoordinator.swift */, F041BE4E2C983C2B0083EC28 /* DAITASettingsPromptItem.swift */, 7A8A19132CEF2527000BCB5B /* DAITATunnelSettingsViewModel.swift */, 7A8A19092CE5FFDF000BCB5B /* SettingsDAITAView.swift */, @@ -4438,12 +4448,16 @@ children = ( A9D9A4D32C36E1EA004088DD /* mullvad_rust_runtime.h */, A992DA1F2C24709F00DE7CE5 /* MullvadRustRuntime.h */, - A9A557F42B7E3E5C0017ADA8 /* EphemeralPeerReceiver.swift */, + 014449942CA293B100C0C2F2 /* EncryptedDNSProxy.swift */, A948809A2BC9308D0090A44C /* EphemeralPeerExchangeActor.swift */, A9EB4F9C2B7FAB21002A2D7A /* EphemeralPeerNegotiator.swift */, + A9A557F42B7E3E5C0017ADA8 /* EphemeralPeerReceiver.swift */, + 7A99D3702D56220E00891FF7 /* MullvadApiCancellable.swift */, + 7A3215702D392F0B005DF395 /* MullvadApiCompletion.swift */, + 7AB931232D43C2C2005FCEBA /* MullvadApiContext.swift */, + 7AB931252D43D222005FCEBA /* MullvadApiResponse.swift */, F0DDE40F2B220458006B57A7 /* ShadowSocksProxy.swift */, 584023212A406BF5007B27AC /* TunnelObfuscator.swift */, - 014449942CA293B100C0C2F2 /* EncryptedDNSProxy.swift */, ); path = MullvadRustRuntime; sourceTree = "<group>"; @@ -4507,8 +4521,10 @@ 06FAE66A28F83CA30033DD93 /* RESTRequestFactory.swift */, 06FAE67428F83CA40033DD93 /* RESTRequestHandler.swift */, 06FAE66628F83CA30033DD93 /* RESTResponseHandler.swift */, + 7AB9312D2D4A5D0A005FCEBA /* RESTRustNetworkOperation.swift */, 06FAE67528F83CA40033DD93 /* RESTTaskIdentifier.swift */, 06FAE66528F83CA30033DD93 /* RESTURLSession.swift */, + 7A99D36E2D5606F900891FF7 /* MullvadApiRequestFactory.swift */, 06FAE67728F83CA40033DD93 /* ServerRelaysResponse.swift */, 06FAE66B28F83CA30033DD93 /* SSLPinningURLSessionDelegate.swift */, ); @@ -5390,7 +5406,7 @@ buildActionMask = 2147483647; files = ( 062B45A328FD4CA700746E77 /* le_root_cert.cer in Resources */, - 7A99D36D2D54FCC400891FF7 /* relays.json in Resources */, + 7A95B67B2D5F758300687524 /* relays.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5694,6 +5710,7 @@ F0B894EF2BF751C500817A42 /* RelayWithLocation.swift in Sources */, F0DDE42C2B220A15006B57A7 /* Midpoint.swift in Sources */, A90763C72B2858DC0045ADF0 /* CancellableChain.swift in Sources */, + 7AB9312F2D4A5D0A005FCEBA /* RESTRustNetworkOperation.swift in Sources */, 06799AF128F98E4800ACD94E /* RESTAPIProxy.swift in Sources */, F0DDE42A2B220A15006B57A7 /* Haversine.swift in Sources */, 589E76C02A9378F100E502F3 /* RESTRequestExecutor.swift in Sources */, @@ -5702,6 +5719,7 @@ F0164ED12B4F2DCB0020268D /* AccessMethodIterator.swift in Sources */, A9D99B9A2A1F7C3200DE27D3 /* RESTTransport.swift in Sources */, A90763BB2B2857D50045ADF0 /* Socks5AddressType.swift in Sources */, + 7A99D36F2D56070400891FF7 /* MullvadApiRequestFactory.swift in Sources */, F0F3161B2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift in Sources */, 06799AE028F98E4800ACD94E /* RESTCoding.swift in Sources */, A90763B72B2857D50045ADF0 /* Socks5DataStreamHandler.swift in Sources */, @@ -6055,6 +6073,7 @@ 44075DFB2CDA4F7400F61139 /* UDPOverTCPObfuscationSettingsViewModel.swift in Sources */, 7A6389DC2B7E3BD6008E77E1 /* CustomListViewModel.swift in Sources */, 4422C0712CCFF6790001A385 /* UDPOverTCPObfuscationSettingsView.swift in Sources */, + 7A95B6792D5F729300687524 /* DAITASettingsCoordinator.swift in Sources */, 7A9CCCC42A96302800DD6A34 /* TunnelCoordinator.swift in Sources */, 5827B0A42B0F38FD00CCBBA1 /* EditAccessMethodInteractorProtocol.swift in Sources */, 586C0D852B03D31E00E7CDD7 /* SocksSectionHandler.swift in Sources */, @@ -6157,7 +6176,6 @@ 588D7EDC2AF3A55E005DF40A /* ListAccessMethodInteractorProtocol.swift in Sources */, 588D7ED62AF3903F005DF40A /* ListAccessMethodViewController.swift in Sources */, 7A8A190E2CEB77C1000BCB5B /* SettingsRowViewFooter.swift in Sources */, - 7A3215742D3E5A85005DF395 /* DAITASettingsCoordinator.swift in Sources */, 7A6000FC2B628DF6001CF0D9 /* ListCellContentConfiguration.swift in Sources */, 582BB1B1229569620055B6EF /* UINavigationBar+Appearance.swift in Sources */, 7A9FA1442A2E3FE5000B728D /* CheckableSettingsCell.swift in Sources */, @@ -6690,11 +6708,15 @@ files = ( A9D9A4B12C36D10E004088DD /* ShadowSocksProxy.swift in Sources */, 014449952CA293B100C0C2F2 /* EncryptedDNSProxy.swift in Sources */, + 7AB931242D43C2CA005FCEBA /* MullvadApiContext.swift in Sources */, A9D9A4BB2C36D397004088DD /* EphemeralPeerNegotiator.swift in Sources */, A9D9A4B22C36D12D004088DD /* TunnelObfuscator.swift in Sources */, + 7AB931262D43D22F005FCEBA /* MullvadApiResponse.swift in Sources */, A9173C322C36CCDD00F6A08C /* EphemeralPeerReceiver.swift in Sources */, + 7A99D3712D56222000891FF7 /* MullvadApiCancellable.swift in Sources */, A93969812CE606190032A7A0 /* Maybenot.swift in Sources */, F05919802C45515200C301F3 /* EphemeralPeerExchangeActor.swift in Sources */, + 7A3215722D3934E6005DF395 /* MullvadApiCompletion.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift b/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift index 5e52a9d61f..917cd9ed29 100644 --- a/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift +++ b/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift @@ -93,7 +93,6 @@ final class AddressCacheTracker: @unchecked Sendable { return self.apiProxy.getAddressList(retryStrategy: .default) { result in self.setEndpoints(from: result) - finish(result.map { _ in true }) } } diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 2642309f76..5e30f2fae9 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -190,14 +190,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD transportProvider: REST.AnyTransportProvider { [weak self] in return self?.transportMonitor.makeTransport() }, - addressCache: addressCache + addressCache: addressCache, + apiContext: REST.apiContext ) } else { proxyFactory = REST.ProxyFactory.makeProxyFactory( transportProvider: REST.AnyTransportProvider { [weak self] in return self?.transportMonitor.makeTransport() }, - addressCache: addressCache + addressCache: addressCache, + apiContext: REST.apiContext ) } apiProxy = proxyFactory.createAPIProxy() diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift index 93520bd97b..228a6f9d9e 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift @@ -77,7 +77,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { let proxyFactory = REST.ProxyFactory.makeProxyFactory( transportProvider: transportProvider, - addressCache: addressCache + addressCache: addressCache, + apiContext: REST.apiContext ) let accountsProxy = proxyFactory.createAccountsProxy() let devicesProxy = proxyFactory.createDevicesProxy() diff --git a/mullvad-api/src/lib.rs b/mullvad-api/src/lib.rs index 997a0635c7..950ce10345 100644 --- a/mullvad-api/src/lib.rs +++ b/mullvad-api/src/lib.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; #[cfg(target_os = "android")] use futures::channel::mpsc; +use hyper::body::Incoming; #[cfg(target_os = "android")] use mullvad_types::account::{PlayPurchase, PlayPurchasePaymentToken}; use mullvad_types::{ @@ -746,13 +747,17 @@ impl ApiProxy { } pub async fn get_api_addrs(&self) -> Result<Vec<SocketAddr>, rest::Error> { + self.get_api_addrs_response().await?.deserialize().await + } + + pub async fn get_api_addrs_response(&self) -> Result<rest::Response<Incoming>, rest::Error> { let request = self .handle .factory .get(&format!("{APP_URL_PREFIX}/api-addrs"))? .expected_status(&[StatusCode::OK]); - let response = self.handle.service.request(request).await?; - response.deserialize().await + + self.handle.service.request(request).await } /// Check the availablility of `{APP_URL_PREFIX}/api-addrs`. diff --git a/mullvad-api/src/rest.rs b/mullvad-api/src/rest.rs index cab3bb7e0f..bab6d8112a 100644 --- a/mullvad-api/src/rest.rs +++ b/mullvad-api/src/rest.rs @@ -512,6 +512,10 @@ where pub async fn deserialize<T: serde::de::DeserializeOwned>(self) -> Result<T> { deserialize_body_inner(self.response).await } + + pub async fn body(self) -> Result<Vec<u8>> { + Ok(BodyExt::collect(self.response).await?.to_bytes().to_vec()) + } } #[derive(serde::Deserialize)] diff --git a/mullvad-ios/Cargo.toml b/mullvad-ios/Cargo.toml index ab4a7a3050..d1dfea5ba1 100644 --- a/mullvad-ios/Cargo.toml +++ b/mullvad-ios/Cargo.toml @@ -10,11 +10,16 @@ rust-version.workspace = true [lints] workspace = true +[features] +# Allow the API server to be used +api-override = ["mullvad-api/api-override"] + [target.'cfg(target_os = "ios")'.dependencies] libc = "0.2" log = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } tonic = { workspace = true } +hyper = { version = "1.4.1", features = ["client", "http1"] } hyper-util = { workspace = true } tower = { workspace = true } tunnel-obfuscation = { path = "../tunnel-obfuscation" } @@ -22,6 +27,8 @@ oslog = "0.2" talpid-types = { path = "../talpid-types" } talpid-tunnel-config-client = { path = "../talpid-tunnel-config-client" } mullvad-encrypted-dns-proxy = { path = "../mullvad-encrypted-dns-proxy" } +mullvad-api = { path = "../mullvad-api" } +serde_json = { workspace = true } shadowsocks-service = { workspace = true, features = [ "local", diff --git a/mullvad-ios/src/api_client/api.rs b/mullvad-ios/src/api_client/api.rs new file mode 100644 index 0000000000..ad3069a20b --- /dev/null +++ b/mullvad-ios/src/api_client/api.rs @@ -0,0 +1,58 @@ +use mullvad_api::{ + rest::{self, MullvadRestHandle}, + ApiProxy, +}; + +use super::{ + cancellation::{RequestCancelHandle, SwiftCancelHandle}, + completion::{CompletionCookie, SwiftCompletionHandler}, + response::SwiftMullvadApiResponse, + SwiftApiContext, +}; + +/// # Safety +/// +/// `api_context` must be pointing to a valid instance of `SwiftApiContext`. A `SwiftApiContext` is created +/// by calling `mullvad_api_init_new`. +/// +/// `completion_cookie` must be pointing to a valid instance of `CompletionCookie`. `CompletionCookie` is +/// safe because the pointer in `MullvadApiCompletion` is valid for the lifetime of the process where this +/// type is intended to be used. +/// +/// This function is not safe to call multiple times with the same `CompletionCookie`. +#[no_mangle] +pub unsafe extern "C" fn mullvad_api_get_addresses( + api_context: SwiftApiContext, + completion_cookie: *mut libc::c_void, +) -> SwiftCancelHandle { + let completion_handler = SwiftCompletionHandler::new(CompletionCookie(completion_cookie)); + + let Ok(tokio_handle) = crate::mullvad_ios_runtime() else { + completion_handler.finish(SwiftMullvadApiResponse::no_tokio_runtime()); + return SwiftCancelHandle::empty(); + }; + + let api_context = api_context.into_rust_context(); + + let completion = completion_handler.clone(); + let task = tokio_handle.clone().spawn(async move { + match mullvad_api_get_addresses_inner(api_context.rest_handle()).await { + Ok(response) => completion.finish(response), + Err(err) => { + log::error!("{err:?}"); + completion.finish(SwiftMullvadApiResponse::rest_error(err)); + } + } + }); + + RequestCancelHandle::new(task, completion_handler.clone()).into_swift() +} + +async fn mullvad_api_get_addresses_inner( + rest_client: MullvadRestHandle, +) -> Result<SwiftMullvadApiResponse, rest::Error> { + let api = ApiProxy::new(rest_client); + let response = api.get_api_addrs_response().await?; + + SwiftMullvadApiResponse::with_body(response).await +} diff --git a/mullvad-ios/src/api_client/cancellation.rs b/mullvad-ios/src/api_client/cancellation.rs new file mode 100644 index 0000000000..3c18340478 --- /dev/null +++ b/mullvad-ios/src/api_client/cancellation.rs @@ -0,0 +1,88 @@ +use std::ptr::null_mut; + +use tokio::task::JoinHandle; + +use super::{completion::SwiftCompletionHandler, response::SwiftMullvadApiResponse}; + +#[repr(C)] +pub struct SwiftCancelHandle { + ptr: *mut RequestCancelHandle, +} + +impl SwiftCancelHandle { + pub fn empty() -> Self { + Self { ptr: null_mut() } + } + + /// This consumes and nulls out the pointer. The caller is responsible for the pointer being valid + /// when calling `to_handle`. + unsafe fn into_handle(mut self) -> RequestCancelHandle { + // # Safety + // This call is safe as long as the pointer is only ever used from a single thread and the + // instance of `SwiftCancelHandle` was created with a valid pointer to + // `RequestCancelHandle`. + let handle = unsafe { *Box::from_raw(self.ptr) }; + self.ptr = null_mut(); + + handle + } +} + +pub struct RequestCancelHandle { + task: JoinHandle<()>, + completion: SwiftCompletionHandler, +} + +impl RequestCancelHandle { + pub fn new(task: JoinHandle<()>, completion: SwiftCompletionHandler) -> Self { + Self { task, completion } + } + + pub fn into_swift(self) -> SwiftCancelHandle { + SwiftCancelHandle { + ptr: Box::into_raw(Box::new(self)), + } + } + + pub fn cancel(self) { + let Self { task, completion } = self; + task.abort(); + // TODO: should this call block until the task returns? + // We can make it do that. + // let _ = handle.block_on(self.task); + completion.finish(SwiftMullvadApiResponse::cancelled()); + } +} + +/// Called by the Swift side to signal that a Mullvad API call should be cancelled. +/// After this call, the cancel token is no longer valid. +/// +/// # Safety +/// +/// `handle_ptr` must be pointing to a valid instance of `SwiftCancelHandle`. This function +/// is not safe to call multiple times with the same `SwiftCancelHandle`. +#[no_mangle] +extern "C" fn mullvad_api_cancel_task(handle_ptr: SwiftCancelHandle) { + if handle_ptr.ptr.is_null() { + return; + } + + let handle = unsafe { handle_ptr.into_handle() }; + handle.cancel() +} + +/// Called by the Swift side to signal that the Rust `SwiftCancelHandle` can be safely +/// dropped from memory. +/// +/// # Safety +/// +/// `handle_ptr` must be pointing to a valid instance of `SwiftCancelHandle`. This function +/// is not safe to call multiple times with the same `SwiftCancelHandle`. +#[no_mangle] +extern "C" fn mullvad_api_cancel_task_drop(handle_ptr: SwiftCancelHandle) { + if handle_ptr.ptr.is_null() { + return; + } + + let _handle = unsafe { handle_ptr.into_handle() }; +} diff --git a/mullvad-ios/src/api_client/completion.rs b/mullvad-ios/src/api_client/completion.rs new file mode 100644 index 0000000000..11a05acf8e --- /dev/null +++ b/mullvad-ios/src/api_client/completion.rs @@ -0,0 +1,51 @@ +use std::sync::{Arc, Mutex}; + +use super::response::SwiftMullvadApiResponse; + +extern "C" { + /// Maps to `mullvadApiCompletionFinish` on Swift side to facilitate callback based completion flow when doing + /// network calls through Mullvad API on Rust side. + /// + /// # Safety + /// + /// `response` must be pointing to a valid instance of `SwiftMullvadApiResponse`. + /// + /// `completion_cookie` must be pointing to a valid instance of `CompletionCookie`. `CompletionCookie` is safe + /// because the pointer in `MullvadApiCompletion` is valid for the lifetime of the process where this type is + /// intended to be used. + pub fn mullvad_api_completion_finish( + response: SwiftMullvadApiResponse, + completion_cookie: CompletionCookie, + ); +} + +#[repr(C)] +pub struct CompletionCookie(pub *mut std::ffi::c_void); +unsafe impl Send for CompletionCookie {} + +#[derive(Clone)] +pub struct SwiftCompletionHandler { + inner: Arc<Mutex<Option<CompletionCookie>>>, +} + +impl SwiftCompletionHandler { + pub fn new(cookie: CompletionCookie) -> Self { + Self { + inner: Arc::new(Mutex::new(Some(cookie))), + } + } + + // This function makes sure that completion is done only once. + pub fn finish(&self, response: SwiftMullvadApiResponse) { + let Ok(mut maybe_cookie) = self.inner.lock() else { + log::error!("Response handler panicked"); + return; + }; + + let Some(cookie) = maybe_cookie.take() else { + return; + }; + + unsafe { mullvad_api_completion_finish(response, cookie) }; + } +} diff --git a/mullvad-ios/src/api_client/mod.rs b/mullvad-ios/src/api_client/mod.rs new file mode 100644 index 0000000000..fdda5b0dbb --- /dev/null +++ b/mullvad-ios/src/api_client/mod.rs @@ -0,0 +1,82 @@ +use std::{ffi::CStr, sync::Arc}; + +use mullvad_api::{ + proxy::{ApiConnectionMode, StaticConnectionModeProvider}, + rest::MullvadRestHandle, + ApiEndpoint, Runtime, +}; + +mod api; +mod cancellation; +mod completion; +mod response; + +#[repr(C)] +pub struct SwiftApiContext(*const ApiContext); +impl SwiftApiContext { + pub fn new(context: ApiContext) -> SwiftApiContext { + SwiftApiContext(Arc::into_raw(Arc::new(context))) + } + + pub unsafe fn into_rust_context(self) -> Arc<ApiContext> { + Arc::increment_strong_count(self.0); + Arc::from_raw(self.0) + } +} + +pub struct ApiContext { + _api_client: Runtime, + rest_client: MullvadRestHandle, +} +impl ApiContext { + pub fn rest_handle(&self) -> MullvadRestHandle { + self.rest_client.clone() + } +} + +/// # Safety +/// +/// `host` must be a pointer to a null terminated string representing a hostname for Mullvad API host. +/// This hostname will be used for TLS validation but not used for domain name resolution. +/// +/// `address` must be a pointer to a null terminated string representing a socket address through which +/// the Mullvad API can be reached directly. +/// +/// If a context cannot be constructed this function will panic since the call site would not be able +/// to proceed in a meaningful way anyway. +/// +/// This function is safe. +#[no_mangle] +pub extern "C" fn mullvad_api_init_new(host: *const u8, address: *const u8) -> SwiftApiContext { + let host = unsafe { CStr::from_ptr(host.cast()) }; + let address = unsafe { CStr::from_ptr(address.cast()) }; + + let host = host.to_str().unwrap(); + let address = address.to_str().unwrap(); + + let endpoint = ApiEndpoint { + host: Some(String::from(host)), + address: Some(address.parse().unwrap()), + #[cfg(feature = "api-override")] + disable_tls: false, + #[cfg(feature = "api-override")] + force_direct: false, + }; + + let tokio_handle = crate::mullvad_ios_runtime().unwrap(); + + let api_context = tokio_handle.clone().block_on(async move { + // It is imperative that the REST runtime is created within an async context, otherwise + // ApiAvailability panics. + let api_client = mullvad_api::Runtime::new(tokio_handle, &endpoint); + let rest_client = api_client + .mullvad_rest_handle(StaticConnectionModeProvider::new(ApiConnectionMode::Direct)); + + ApiContext { + _api_client: api_client, + rest_client, + } + }); + + SwiftApiContext::new(api_context) +} diff --git a/mullvad-ios/src/api_client/response.rs b/mullvad-ios/src/api_client/response.rs new file mode 100644 index 0000000000..249f1040bd --- /dev/null +++ b/mullvad-ios/src/api_client/response.rs @@ -0,0 +1,115 @@ +use std::{ffi::CString, ptr::null_mut}; + +use mullvad_api::rest::{self, Response}; + +#[repr(C)] +pub struct SwiftMullvadApiResponse { + body: *mut u8, + body_size: usize, + status_code: u16, + error_description: *mut u8, + server_response_code: *mut u8, + success: bool, + should_retry: bool, + retry_after: u64, +} +impl SwiftMullvadApiResponse { + pub async fn with_body(response: Response<hyper::body::Incoming>) -> Result<Self, rest::Error> { + let status_code: u16 = response.status().into(); + let body: Vec<u8> = response.body().await?; + + let body_size = body.len(); + let body = body.into_boxed_slice(); + + Ok(Self { + body: Box::<[u8]>::into_raw(body).cast(), + body_size, + status_code, + error_description: null_mut(), + server_response_code: null_mut(), + success: true, + should_retry: false, + retry_after: 0, + }) + } + + pub fn rest_error(err: mullvad_api::rest::Error) -> Self { + if err.is_aborted() { + return Self::cancelled(); + } + + let to_cstr_pointer = |str| { + CString::new(str) + .map(|cstr| cstr.into_raw().cast()) + .unwrap_or(null_mut()) + }; + + let should_retry = err.is_network_error(); + let error_description = to_cstr_pointer(err.to_string()); + let (status_code, server_response_code): (u16, _) = + if let rest::Error::ApiError(status_code, error_code) = err { + (status_code.into(), to_cstr_pointer(error_code)) + } else { + (0, null_mut()) + }; + + Self { + body: null_mut(), + body_size: 0, + status_code, + error_description, + server_response_code, + success: false, + should_retry, + retry_after: 0, + } + } + + pub fn cancelled() -> Self { + Self { + success: false, + should_retry: false, + error_description: c"Request was cancelled".to_owned().into_raw().cast(), + body: null_mut(), + body_size: 0, + status_code: 0, + server_response_code: null_mut(), + retry_after: 0, + } + } + + pub fn no_tokio_runtime() -> Self { + Self { + success: false, + should_retry: false, + error_description: c"Failed to get Tokio runtime".to_owned().into_raw().cast(), + body: null_mut(), + body_size: 0, + status_code: 0, + server_response_code: null_mut(), + retry_after: 0, + } + } +} + +/// Called by the Swift side to signal that the Rust `SwiftMullvadApiResponse` can be safely +/// dropped from memory. +/// +/// # Safety +/// +/// `response` must be pointing to a valid instance of `SwiftMullvadApiResponse`. This function +/// is not safe to call multiple times with the same `SwiftMullvadApiResponse`. +#[no_mangle] +pub unsafe extern "C" fn mullvad_response_drop(response: SwiftMullvadApiResponse) { + if !response.body.is_null() { + let _ = Vec::from_raw_parts(response.body, response.body_size, response.body_size); + } + + if !response.error_description.is_null() { + let _ = CString::from_raw(response.error_description.cast()); + } + + if !response.server_response_code.is_null() { + let _ = CString::from_raw(response.server_response_code.cast()); + } +} diff --git a/mullvad-ios/src/lib.rs b/mullvad-ios/src/lib.rs index 0d88a33df9..fa23672e29 100644 --- a/mullvad-ios/src/lib.rs +++ b/mullvad-ios/src/lib.rs @@ -1,4 +1,5 @@ #![cfg(target_os = "ios")] +mod api_client; mod encrypted_dns_proxy; mod ephemeral_peer_proxy; mod shadowsocks_proxy; |
