summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@mullvad.net>2025-01-16 15:52:16 +0100
committerJon Petersson <jon.petersson@mullvad.net>2025-02-21 15:08:01 +0100
commit041bf3f20f3cb0ed6703065eee4d09b5f938f730 (patch)
tree13ed3e3895269a8bb6980b5d6eab3238f1e09252
parent9a8535ef787d784d2d75dbd1474e1a1846413e83 (diff)
downloadmullvadvpn-041bf3f20f3cb0ed6703065eee4d09b5f938f730.tar.xz
mullvadvpn-041bf3f20f3cb0ed6703065eee4d09b5f938f730.zip
Implement an FFI to fetch API IP addresses using mullvad-api
-rw-r--r--Cargo.lock3
-rw-r--r--ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift7
-rw-r--r--ios/MullvadMockData/MullvadREST/MockProxyFactory.swift10
-rw-r--r--ios/MullvadREST/ApiHandlers/MullvadApiRequestFactory.swift37
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift33
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTDefaults.swift8
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTProxy.swift15
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTProxyFactory.swift14
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTResponseHandler.swift57
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTRustNetworkOperation.swift168
-rw-r--r--ios/MullvadRESTTests/RequestExecutorTests.swift3
-rw-r--r--ios/MullvadRustRuntime/MullvadApiCancellable.swift23
-rw-r--r--ios/MullvadRustRuntime/MullvadApiCompletion.swift28
-rw-r--r--ios/MullvadRustRuntime/MullvadApiContext.swift27
-rw-r--r--ios/MullvadRustRuntime/MullvadApiResponse.swift55
-rw-r--r--ios/MullvadRustRuntime/include/mullvad_rust_runtime.h107
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj50
-rw-r--r--ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift1
-rw-r--r--ios/MullvadVPN/AppDelegate.swift6
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift3
-rw-r--r--mullvad-api/src/lib.rs9
-rw-r--r--mullvad-api/src/rest.rs4
-rw-r--r--mullvad-ios/Cargo.toml7
-rw-r--r--mullvad-ios/src/api_client/api.rs58
-rw-r--r--mullvad-ios/src/api_client/cancellation.rs88
-rw-r--r--mullvad-ios/src/api_client/completion.rs51
-rw-r--r--mullvad-ios/src/api_client/mod.rs82
-rw-r--r--mullvad-ios/src/api_client/response.rs115
-rw-r--r--mullvad-ios/src/lib.rs1
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;