summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadRustRuntime
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2024-06-24 13:21:40 +0200
committerEmīls <emils@mullvad.net>2024-07-17 11:48:13 +0200
commit2df280fbf02fa1b39efffb0b27b297d39d6369c0 (patch)
tree1b6a657bb8c594e82a50bd4dfcbfb85d34395fa1 /ios/MullvadRustRuntime
parentb6fe08388dcbfbe1fb54a1c89322c329be2f54f9 (diff)
downloadmullvadvpn-2df280fbf02fa1b39efffb0b27b297d39d6369c0.tar.xz
mullvadvpn-2df280fbf02fa1b39efffb0b27b297d39d6369c0.zip
Add a Rust FFI, Disable sandboxing for scripts
Diffstat (limited to 'ios/MullvadRustRuntime')
-rw-r--r--ios/MullvadRustRuntime/MullvadRustRuntime.h10
-rw-r--r--ios/MullvadRustRuntime/PacketTunnelProvider+TCPConnection.swift112
-rw-r--r--ios/MullvadRustRuntime/PostQuantumKeyExchangeActor.swift137
-rw-r--r--ios/MullvadRustRuntime/PostQuantumKeyNegotiator.swift78
-rw-r--r--ios/MullvadRustRuntime/ShadowSocksProxy.swift91
-rw-r--r--ios/MullvadRustRuntime/UDPOverTCPObfuscator.swift87
-rw-r--r--ios/MullvadRustRuntime/include/mullvad_rust_runtime.h137
-rw-r--r--ios/MullvadRustRuntime/module.private.modulemap5
8 files changed, 657 insertions, 0 deletions
diff --git a/ios/MullvadRustRuntime/MullvadRustRuntime.h b/ios/MullvadRustRuntime/MullvadRustRuntime.h
new file mode 100644
index 0000000000..a490829628
--- /dev/null
+++ b/ios/MullvadRustRuntime/MullvadRustRuntime.h
@@ -0,0 +1,10 @@
+//
+// MullvadRustRuntime.h
+// MullvadRustRuntime
+//
+// Created by Marco Nikic on 2024-06-20.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import "mullvad_rust_runtime.h"
diff --git a/ios/MullvadRustRuntime/PacketTunnelProvider+TCPConnection.swift b/ios/MullvadRustRuntime/PacketTunnelProvider+TCPConnection.swift
new file mode 100644
index 0000000000..4c1b3b1607
--- /dev/null
+++ b/ios/MullvadRustRuntime/PacketTunnelProvider+TCPConnection.swift
@@ -0,0 +1,112 @@
+//
+// PacketTunnelProvider+TCPConnection.swift
+// PacketTunnel
+//
+// Created by Marco Nikic on 2024-02-15.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadRustRuntimeProxy
+import MullvadTypes
+import NetworkExtension
+import WireGuardKitTypes
+
+/// Writes data to the in-tunnel TCP connection
+///
+/// This FFI function is called by Rust whenever there is data to be written to the in-tunnel TCP connection when exchanging
+/// quantum-resistant pre shared keys.
+///
+/// Whenever the flow control is given back from the connection, acknowledge that data was written using `rawWriteAcknowledgement`.
+/// - Parameters:
+/// - rawConnection: A raw pointer to the in-tunnel TCP connection
+/// - rawData: A raw pointer to the data to write in the connection
+/// - dataLength: The length of data to write in the connection
+/// - rawWriteAcknowledgement: An opaque pointer needed for write acknowledgement
+@_cdecl("swift_nw_tcp_connection_send")
+func tcpConnectionSend(
+ rawConnection: UnsafeMutableRawPointer?,
+ rawData: UnsafeMutableRawPointer,
+ dataLength: UInt,
+ rawWriteAcknowledgement: UnsafeMutableRawPointer?
+) {
+ guard let rawConnection, let rawWriteAcknowledgement else {
+ handle_sent(0, rawWriteAcknowledgement)
+ return
+ }
+ let tcpConnection = Unmanaged<NWTCPConnection>.fromOpaque(rawConnection).takeUnretainedValue()
+ let data = Data(bytes: rawData, count: Int(dataLength))
+
+ // The guarantee that all writes are sequential is done by virtue of not returning the execution context
+ // to Rust before this closure is done executing.
+ tcpConnection.write(data, completionHandler: { maybeError in
+ if maybeError != nil {
+ handle_sent(0, rawWriteAcknowledgement)
+ } else {
+ handle_sent(dataLength, rawWriteAcknowledgement)
+ }
+ })
+}
+
+/// Reads data to the in-tunnel TCP connection
+///
+/// This FFI function is called by Rust whenever there is data to be read from the in-tunnel TCP connection when exchanging
+/// quantum-resistant pre shared keys.
+///
+/// Whenever the flow control is given back from the connection, acknowledge that data was read using `rawReadAcknowledgement`.
+/// - Parameters:
+/// - rawConnection: A raw pointer to the in-tunnel TCP connection
+/// - rawReadAcknowledgement: An opaque pointer needed for read acknowledgement
+@_cdecl("swift_nw_tcp_connection_read")
+func tcpConnectionReceive(
+ rawConnection: UnsafeMutableRawPointer?,
+ rawReadAcknowledgement: UnsafeMutableRawPointer?
+) {
+ guard let rawConnection, let rawReadAcknowledgement else {
+ handle_recv(nil, 0, rawReadAcknowledgement)
+ return
+ }
+ let tcpConnection = Unmanaged<NWTCPConnection>.fromOpaque(rawConnection).takeUnretainedValue()
+ tcpConnection.readMinimumLength(0, maximumLength: Int(UInt16.max)) { data, maybeError in
+ if let data {
+ if maybeError != nil {
+ handle_recv(nil, 0, rawReadAcknowledgement)
+ } else {
+ handle_recv(data.map { $0 }, UInt(data.count), rawReadAcknowledgement)
+ }
+ }
+ }
+}
+
+/// End sequence of a quantum-secure pre shared key exchange.
+///
+/// This FFI function is called by Rust when the quantum-secure pre shared key exchange has either failed, or succeeded.
+/// When both the `rawPresharedKey` and the `rawEphemeralKey` are raw pointers to 32 bytes data arrays,
+/// the quantum-secure key exchange is considered successful. In any other case, the exchange is considered failed.
+///
+/// - Parameters:
+/// - rawPacketTunnel: A raw pointer to the running instance of `NEPacketTunnelProvider`
+/// - rawPresharedKey: A raw pointer to the quantum-secure pre shared key
+/// - rawEphemeralKey: A raw pointer to the ephemeral private key of the device
+@_cdecl("swift_post_quantum_key_ready")
+func receivePostQuantumKey(
+ rawPostQuantumKeyReceiver: UnsafeMutableRawPointer?,
+ rawPresharedKey: UnsafeMutableRawPointer?,
+ rawEphemeralKey: UnsafeMutableRawPointer?
+) {
+ guard let rawPostQuantumKeyReceiver else { return }
+ let postQuantumKeyReceiver = Unmanaged<PostQuantumKeyReceiver>.fromOpaque(rawPostQuantumKeyReceiver)
+ .takeUnretainedValue()
+
+ guard
+ let rawPresharedKey,
+ let rawEphemeralKey,
+ let ephemeralKey = PrivateKey(rawValue: Data(bytes: rawEphemeralKey, count: 32)),
+ let key = PreSharedKey(rawValue: Data(bytes: rawPresharedKey, count: 32))
+ else {
+ postQuantumKeyReceiver.keyExchangeFailed()
+ return
+ }
+
+ postQuantumKeyReceiver.receivePostQuantumKey(key, ephemeralKey: ephemeralKey)
+}
diff --git a/ios/MullvadRustRuntime/PostQuantumKeyExchangeActor.swift b/ios/MullvadRustRuntime/PostQuantumKeyExchangeActor.swift
new file mode 100644
index 0000000000..8028c1ba3e
--- /dev/null
+++ b/ios/MullvadRustRuntime/PostQuantumKeyExchangeActor.swift
@@ -0,0 +1,137 @@
+//
+// PostQuantumKeyExchangeActor.swift
+// PacketTunnel
+//
+// Created by Marco Nikic on 2024-04-12.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadRustRuntimeProxy
+import MullvadTypes
+import NetworkExtension
+import WireGuardKitTypes
+
+public class PostQuantumKeyExchangeActor {
+ struct Negotiation {
+ var negotiator: PostQuantumKeyNegotiating
+ var inTunnelTCPConnection: NWTCPConnection
+ var tcpConnectionObserver: NSKeyValueObservation
+
+ func cancel() {
+ negotiator.cancelKeyNegotiation()
+ tcpConnectionObserver.invalidate()
+ inTunnelTCPConnection.cancel()
+ }
+ }
+
+ unowned let packetTunnel: any TunnelProvider
+ internal var negotiation: Negotiation?
+ private var timer: DispatchSourceTimer?
+ private var keyExchangeRetriesIterator: AnyIterator<Duration>!
+ private let iteratorProvider: () -> AnyIterator<Duration>
+ private let negotiationProvider: PostQuantumKeyNegotiating.Type
+
+ // Callback in the event of the negotiation failing on startup
+ var onFailure: () -> Void
+
+ public init(
+ packetTunnel: any TunnelProvider,
+ onFailure: @escaping (() -> Void),
+ negotiationProvider: PostQuantumKeyNegotiating.Type = PostQuantumKeyNegotiator.self,
+ iteratorProvider: @escaping () -> AnyIterator<Duration>
+ ) {
+ self.packetTunnel = packetTunnel
+ self.onFailure = onFailure
+ self.negotiationProvider = negotiationProvider
+ self.iteratorProvider = iteratorProvider
+ self.keyExchangeRetriesIterator = iteratorProvider()
+ }
+
+ private func createTCPConnection(_ gatewayEndpoint: NWHostEndpoint) -> NWTCPConnection {
+ self.packetTunnel.createTCPConnectionThroughTunnel(
+ to: gatewayEndpoint,
+ enableTLS: false,
+ tlsParameters: nil,
+ delegate: nil
+ )
+ }
+
+ /// Starts a new key exchange.
+ ///
+ /// Any ongoing key negotiation is stopped before starting a new one.
+ /// An exponential backoff timer is used to stop the exchange if it takes too long,
+ /// or if the TCP connection takes too long to become ready.
+ /// It is reset after every successful key exchange.
+ ///
+ /// - Parameter privateKey: The device's current private key
+ public func startNegotiation(with privateKey: PrivateKey) {
+ endCurrentNegotiation()
+ let negotiator = negotiationProvider.init()
+
+ let gatewayAddress = "10.64.0.1"
+ let IPv4Gateway = IPv4Address(gatewayAddress)!
+ let endpoint = NWHostEndpoint(hostname: gatewayAddress, port: "\(CONFIG_SERVICE_PORT)")
+ let inTunnelTCPConnection = createTCPConnection(endpoint)
+
+ // This will become the new private key of the device
+ let ephemeralSharedKey = PrivateKey()
+
+ let tcpConnectionTimeout = keyExchangeRetriesIterator.next() ?? .seconds(10)
+ // If the connection never becomes viable, force a reconnection after 10 seconds
+ scheduleInTunnelConnectionTimeout(startTime: .now() + tcpConnectionTimeout)
+
+ let tcpConnectionObserver = inTunnelTCPConnection.observe(\.isViable, options: [
+ .initial,
+ .new,
+ ]) { [weak self] observedConnection, _ in
+ guard let self, observedConnection.isViable else { return }
+ self.negotiation?.tcpConnectionObserver.invalidate()
+ self.timer?.cancel()
+
+ if !negotiator.startNegotiation(
+ gatewayIP: IPv4Gateway,
+ devicePublicKey: privateKey.publicKey,
+ presharedKey: ephemeralSharedKey,
+ postQuantumKeyReceiver: packetTunnel,
+ tcpConnection: inTunnelTCPConnection,
+ postQuantumKeyExchangeTimeout: tcpConnectionTimeout
+ ) {
+ self.negotiation = nil
+ self.onFailure()
+ }
+ }
+ negotiation = Negotiation(
+ negotiator: negotiator,
+ inTunnelTCPConnection: inTunnelTCPConnection,
+ tcpConnectionObserver: tcpConnectionObserver
+ )
+ }
+
+ /// Cancels the ongoing key exchange.
+ public func endCurrentNegotiation() {
+ negotiation?.cancel()
+ negotiation = nil
+ }
+
+ /// Resets the exponential timeout for successful key exchanges, and ends the current key exchange.
+ public func reset() {
+ keyExchangeRetriesIterator = iteratorProvider()
+ endCurrentNegotiation()
+ }
+
+ private func scheduleInTunnelConnectionTimeout(startTime: DispatchWallTime) {
+ let newTimer = DispatchSource.makeTimerSource()
+
+ newTimer.setEventHandler { [weak self] in
+ self?.onFailure()
+ self?.timer?.cancel()
+ }
+
+ newTimer.schedule(wallDeadline: startTime)
+ newTimer.activate()
+
+ timer?.cancel()
+ timer = newTimer
+ }
+}
diff --git a/ios/MullvadRustRuntime/PostQuantumKeyNegotiator.swift b/ios/MullvadRustRuntime/PostQuantumKeyNegotiator.swift
new file mode 100644
index 0000000000..c653fc8983
--- /dev/null
+++ b/ios/MullvadRustRuntime/PostQuantumKeyNegotiator.swift
@@ -0,0 +1,78 @@
+//
+// PostQuantumKeyNegotiator.swift
+// PacketTunnel
+//
+// Created by Marco Nikic on 2024-02-16.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+import NetworkExtension
+import WireGuardKitTypes
+
+// swiftlint:disable function_parameter_count
+public protocol PostQuantumKeyNegotiating {
+ func startNegotiation(
+ gatewayIP: IPv4Address,
+ devicePublicKey: PublicKey,
+ presharedKey: PrivateKey,
+ postQuantumKeyReceiver: any TunnelProvider,
+ tcpConnection: NWTCPConnection,
+ postQuantumKeyExchangeTimeout: Duration
+ ) -> Bool
+
+ func cancelKeyNegotiation()
+
+ init()
+}
+
+/**
+ Attempt to start the asynchronous process of key negotiation. Returns true if successfully started, false if failed.
+ */
+public class PostQuantumKeyNegotiator: PostQuantumKeyNegotiating {
+ required public init() {}
+
+ var cancelToken: PostQuantumCancelToken?
+
+ public func startNegotiation(
+ gatewayIP: IPv4Address,
+ devicePublicKey: PublicKey,
+ presharedKey: PrivateKey,
+ postQuantumKeyReceiver: any TunnelProvider,
+ tcpConnection: NWTCPConnection,
+ postQuantumKeyExchangeTimeout: Duration
+ ) -> Bool {
+ // swiftlint:disable:next force_cast
+ let postQuantumKeyReceiver = Unmanaged.passUnretained(postQuantumKeyReceiver as! PostQuantumKeyReceiver)
+ .toOpaque()
+ let opaqueConnection = Unmanaged.passUnretained(tcpConnection).toOpaque()
+ var cancelToken = PostQuantumCancelToken()
+
+ let result = negotiate_post_quantum_key(
+ devicePublicKey.rawValue.map { $0 },
+ presharedKey.rawValue.map { $0 },
+ postQuantumKeyReceiver,
+ opaqueConnection,
+ &cancelToken,
+ UInt64(postQuantumKeyExchangeTimeout.timeInterval)
+ )
+ guard result == 0 else {
+ return false
+ }
+ self.cancelToken = cancelToken
+ return true
+ }
+
+ public func cancelKeyNegotiation() {
+ guard var cancelToken else { return }
+ cancel_post_quantum_key_exchange(&cancelToken)
+ }
+
+ deinit {
+ guard var cancelToken else { return }
+ drop_post_quantum_key_exchange_token(&cancelToken)
+ }
+}
+
+// swiftlint:enable function_parameter_count
diff --git a/ios/MullvadRustRuntime/ShadowSocksProxy.swift b/ios/MullvadRustRuntime/ShadowSocksProxy.swift
new file mode 100644
index 0000000000..cd83f2129a
--- /dev/null
+++ b/ios/MullvadRustRuntime/ShadowSocksProxy.swift
@@ -0,0 +1,91 @@
+//
+// ShadowsocksProxy.swift
+// MullvadREST
+//
+// Created by Emils on 19/04/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadRustRuntimeProxy
+import Network
+
+/// A Swift wrapper around a Rust implementation of Shadowsocks proxy instance
+public class ShadowsocksProxy {
+ private var proxyConfig: ProxyHandle
+ private let forwardAddress: IPAddress
+ private let forwardPort: UInt16
+ private let bridgeAddress: IPAddress
+ private let bridgePort: UInt16
+ private let password: String
+ private let cipher: String
+ private var didStart = false
+ private let stateLock = NSLock()
+
+ public init(
+ forwardAddress: IPAddress,
+ forwardPort: UInt16,
+ bridgeAddress: IPAddress,
+ bridgePort: UInt16,
+ password: String,
+ cipher: String
+ ) {
+ proxyConfig = ProxyHandle(context: nil, port: 0)
+ self.forwardAddress = forwardAddress
+ self.forwardPort = forwardPort
+ self.bridgeAddress = bridgeAddress
+ self.bridgePort = bridgePort
+ self.password = password
+ self.cipher = cipher
+ }
+
+ /// The local port for the shadow socks proxy
+ ///
+ /// - Returns: The local port for the shadow socks proxy when it has started, 0 otherwise.
+ public func localPort() -> UInt16 {
+ stateLock.lock()
+ defer { stateLock.unlock() }
+ return proxyConfig.port
+ }
+
+ deinit {
+ stop()
+ }
+
+ /// Starts the socks proxy
+ public func start() {
+ stateLock.lock()
+ defer { stateLock.unlock() }
+ guard didStart == false else { return }
+ didStart = true
+
+ // Get the raw bytes access to `proxyConfig`
+ _ = withUnsafeMutablePointer(to: &proxyConfig) { config in
+ start_shadowsocks_proxy(
+ forwardAddress.rawValue.map { $0 },
+ UInt(forwardAddress.rawValue.count),
+ forwardPort,
+ bridgeAddress.rawValue.map { $0 },
+ UInt(bridgeAddress.rawValue.count),
+ bridgePort,
+ password,
+ UInt(password.count),
+ cipher,
+ UInt(cipher.count),
+ config
+ )
+ }
+ }
+
+ /// Stops the socks proxy
+ public func stop() {
+ stateLock.lock()
+ defer { stateLock.unlock() }
+ guard didStart == true else { return }
+ didStart = false
+
+ _ = withUnsafeMutablePointer(to: &proxyConfig) { config in
+ stop_shadowsocks_proxy(config)
+ }
+ }
+}
diff --git a/ios/MullvadRustRuntime/UDPOverTCPObfuscator.swift b/ios/MullvadRustRuntime/UDPOverTCPObfuscator.swift
new file mode 100644
index 0000000000..8d5d874c84
--- /dev/null
+++ b/ios/MullvadRustRuntime/UDPOverTCPObfuscator.swift
@@ -0,0 +1,87 @@
+//
+// TunnelObfuscator.swift
+// TunnelObfuscation
+//
+// Created by pronebird on 19/06/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadRustRuntimeProxy
+import MullvadTypes
+import Network
+
+public protocol TunnelObfuscation {
+ init(remoteAddress: IPAddress, tcpPort: UInt16)
+ func start()
+ func stop()
+ var localUdpPort: UInt16 { get }
+ var remotePort: UInt16 { get }
+
+ var transportLayer: TransportLayer { get }
+}
+
+/// Class that implements UDP over TCP obfuscation by accepting traffic on a local UDP port and proxying it over TCP to the remote endpoint.
+public final class UDPOverTCPObfuscator: TunnelObfuscation {
+ private let stateLock = NSLock()
+ private let remoteAddress: IPAddress
+ internal let tcpPort: UInt16
+
+ private var proxyHandle = ProxyHandle(context: nil, port: 0)
+ private var isStarted = false
+
+ /// Returns local UDP port used by proxy and bound to 127.0.0.1 (IPv4).
+ /// The returned value can be zero if obfuscator hasn't started yet.
+ public var localUdpPort: UInt16 {
+ return stateLock.withLock { proxyHandle.port }
+ }
+
+ public var remotePort: UInt16 { tcpPort }
+
+ public var transportLayer: TransportLayer { .tcp }
+
+ /// Initialize tunnel obfuscator with remote server address and TCP port where udp2tcp is running.
+ public init(remoteAddress: IPAddress, tcpPort: UInt16) {
+ self.remoteAddress = remoteAddress
+ self.tcpPort = tcpPort
+ }
+
+ deinit {
+ stop()
+ }
+
+ public func start() {
+ stateLock.withLock {
+ guard !isStarted else { return }
+
+ let result = withUnsafeMutablePointer(to: &proxyHandle) { proxyHandlePointer in
+ let addressData = remoteAddress.rawValue
+
+ return start_tunnel_obfuscator_proxy(
+ addressData.map { $0 },
+ UInt(addressData.count),
+ tcpPort,
+ proxyHandlePointer
+ )
+ }
+
+ assert(result == 0)
+
+ isStarted = true
+ }
+ }
+
+ public func stop() {
+ stateLock.withLock {
+ guard isStarted else { return }
+
+ let result = withUnsafeMutablePointer(to: &proxyHandle) {
+ stop_tunnel_obfuscator_proxy($0)
+ }
+
+ assert(result == 0)
+
+ isStarted = false
+ }
+ }
+}
diff --git a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h
new file mode 100644
index 0000000000..9b5c8bd4c1
--- /dev/null
+++ b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h
@@ -0,0 +1,137 @@
+// This file is generated automatically. To update it forcefully, run `cargo run -p mullvad-ios --target aarch64-apple-ios`.
+
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+typedef struct PostQuantumCancelToken {
+ void *context;
+} PostQuantumCancelToken;
+
+typedef struct ProxyHandle {
+ void *context;
+ uint16_t port;
+} ProxyHandle;
+
+extern const uint16_t CONFIG_SERVICE_PORT;
+
+/**
+ * Called by the Swift side to signal that the quantum-secure key exchange should be cancelled.
+ * After this call, the cancel token is no longer valid.
+ *
+ * # Safety
+ * `sender` must be pointing to a valid instance of a `PostQuantumCancelToken` created by the
+ * `PacketTunnelProvider`.
+ */
+void cancel_post_quantum_key_exchange(const struct PostQuantumCancelToken *sender);
+
+/**
+ * Called by the Swift side to signal that the Rust `PostQuantumCancelToken` can be safely dropped
+ * from memory.
+ *
+ * # Safety
+ * `sender` must be pointing to a valid instance of a `PostQuantumCancelToken` created by the
+ * `PacketTunnelProvider`.
+ */
+void drop_post_quantum_key_exchange_token(const struct PostQuantumCancelToken *sender);
+
+/**
+ * Called by Swift whenever data has been written to the in-tunnel TCP connection when exchanging
+ * quantum-resistant pre shared keys.
+ *
+ * If `bytes_sent` is 0, this indicates that the connection was closed or that an error occurred.
+ *
+ * # Safety
+ * `sender` must be pointing to a valid instance of a `write_tx` created by the `IosTcpProvider`
+ * Callback to call when the TCP connection has written data.
+ */
+void handle_sent(uintptr_t bytes_sent, const void *sender);
+
+/**
+ * Called by Swift whenever data has been read from the in-tunnel TCP connection when exchanging
+ * quantum-resistant pre shared keys.
+ *
+ * If `data` is null or empty, this indicates that the connection was closed or that an error
+ * occurred. An empty buffer is sent to the underlying reader to signal EOF.
+ *
+ * # Safety
+ * `sender` must be pointing to a valid instance of a `read_tx` created by the `IosTcpProvider`
+ *
+ * Callback to call when the TCP connection has received data.
+ */
+void handle_recv(const uint8_t *data, uintptr_t data_len, const void *sender);
+
+/**
+ * Entry point for exchanging post quantum keys on iOS.
+ * The TCP connection must be created to go through the tunnel.
+ * # Safety
+ * `public_key` and `ephemeral_key` must be valid respective `PublicKey` and `PrivateKey` types.
+ * They will not be valid after this function is called, and thus must be copied here.
+ * `packet_tunnel` and `tcp_connection` must be valid pointers to a packet tunnel and a TCP
+ * connection instances.
+ * `cancel_token` should be owned by the caller of this function.
+ */
+int32_t negotiate_post_quantum_key(const uint8_t *public_key,
+ const uint8_t *ephemeral_key,
+ const void *packet_tunnel,
+ const void *tcp_connection,
+ struct PostQuantumCancelToken *cancel_token,
+ uint64_t post_quantum_key_exchange_timeout);
+
+/**
+ * Called when there is data to send on the TCP connection.
+ * The TCP connection must write data on the wire, then call the `handle_sent` function.
+ */
+extern void swift_nw_tcp_connection_send(const void *connection,
+ const void *data,
+ uintptr_t data_len,
+ const void *sender);
+
+/**
+ * Called when there is data to read on the TCP connection.
+ * The TCP connection must read data from the wire, then call the `handle_read` function.
+ */
+extern void swift_nw_tcp_connection_read(const void *connection, const void *sender);
+
+/**
+ * Called when the preshared post quantum key is ready.
+ * `raw_preshared_key` might be NULL if the key negotiation failed.
+ */
+extern void swift_post_quantum_key_ready(const void *raw_packet_tunnel,
+ const uint8_t *raw_preshared_key,
+ const uint8_t *raw_ephemeral_private_key);
+
+/**
+ * # Safety
+ * `addr`, `password`, `cipher` must be valid for the lifetime of this function call and they must
+ * be backed by the amount of bytes as stored in the respective `*_len` parameters.
+ *
+ * `proxy_config` must be pointing to a valid memory region for the size of a `ProxyHandle`
+ * instance.
+ */
+int32_t start_shadowsocks_proxy(const uint8_t *forward_address,
+ uintptr_t forward_address_len,
+ uint16_t forward_port,
+ const uint8_t *addr,
+ uintptr_t addr_len,
+ uint16_t port,
+ const uint8_t *password,
+ uintptr_t password_len,
+ const uint8_t *cipher,
+ uintptr_t cipher_len,
+ struct ProxyHandle *proxy_config);
+
+/**
+ * # Safety
+ * `proxy_config` must be pointing to a valid instance of a `ProxyInstance`, as instantiated by
+ * `start_shadowsocks_proxy`.
+ */
+int32_t stop_shadowsocks_proxy(struct ProxyHandle *proxy_config);
+
+int32_t start_tunnel_obfuscator_proxy(const uint8_t *peer_address,
+ uintptr_t peer_address_len,
+ uint16_t peer_port,
+ struct ProxyHandle *proxy_handle);
+
+int32_t stop_tunnel_obfuscator_proxy(struct ProxyHandle *proxy_handle);
diff --git a/ios/MullvadRustRuntime/module.private.modulemap b/ios/MullvadRustRuntime/module.private.modulemap
new file mode 100644
index 0000000000..5115e403a2
--- /dev/null
+++ b/ios/MullvadRustRuntime/module.private.modulemap
@@ -0,0 +1,5 @@
+framework module MullvadRustRuntimeProxy {
+ header "mullvad_rust_runtime.h"
+ link "libmullvad_ios"
+ export *
+}