summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadRustRuntimeTests
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/MullvadRustRuntimeTests
parentb6fe08388dcbfbe1fb54a1c89322c329be2f54f9 (diff)
downloadmullvadvpn-2df280fbf02fa1b39efffb0b27b297d39d6369c0.tar.xz
mullvadvpn-2df280fbf02fa1b39efffb0b27b297d39d6369c0.zip
Add a Rust FFI, Disable sandboxing for scripts
Diffstat (limited to 'ios/MullvadRustRuntimeTests')
-rw-r--r--ios/MullvadRustRuntimeTests/MullvadPostQuantum+Stubs.swift95
-rw-r--r--ios/MullvadRustRuntimeTests/PostQuantumKeyExchangeActorTests.swift94
-rw-r--r--ios/MullvadRustRuntimeTests/TCPConnection.swift73
-rw-r--r--ios/MullvadRustRuntimeTests/TCPUnsafeListener.swift81
-rw-r--r--ios/MullvadRustRuntimeTests/TunnelObfuscationTests.swift49
-rw-r--r--ios/MullvadRustRuntimeTests/UDPConnection.swift72
6 files changed, 464 insertions, 0 deletions
diff --git a/ios/MullvadRustRuntimeTests/MullvadPostQuantum+Stubs.swift b/ios/MullvadRustRuntimeTests/MullvadPostQuantum+Stubs.swift
new file mode 100644
index 0000000000..31c7edf6e9
--- /dev/null
+++ b/ios/MullvadRustRuntimeTests/MullvadPostQuantum+Stubs.swift
@@ -0,0 +1,95 @@
+//
+// MullvadPostQuantum+Stubs.swift
+// MullvadPostQuantumTests
+//
+// Created by Marco Nikic on 2024-06-12.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+@testable import MullvadRustRuntime
+@testable import MullvadTypes
+import NetworkExtension
+@testable import PacketTunnelCore
+@testable import WireGuardKitTypes
+
+// swiftlint:disable function_parameter_count
+class NWTCPConnectionStub: NWTCPConnection {
+ var _isViable = false
+ override var isViable: Bool {
+ _isViable
+ }
+
+ func becomeViable() {
+ willChangeValue(for: \.isViable)
+ _isViable = true
+ didChangeValue(for: \.isViable)
+ }
+}
+
+class TunnelProviderStub: TunnelProvider {
+ let tcpConnection: NWTCPConnectionStub
+
+ init(tcpConnection: NWTCPConnectionStub) {
+ self.tcpConnection = tcpConnection
+ }
+
+ func createTCPConnectionThroughTunnel(
+ to remoteEndpoint: NWEndpoint,
+ enableTLS: Bool,
+ tlsParameters TLSParameters: NWTLSParameters?,
+ delegate: Any?
+ ) -> NWTCPConnection {
+ tcpConnection
+ }
+}
+
+class FailedNegotiatorStub: PostQuantumKeyNegotiating {
+ var onCancelKeyNegotiation: (() -> Void)?
+
+ required init() {
+ onCancelKeyNegotiation = nil
+ }
+
+ init(onCancelKeyNegotiation: (() -> Void)? = nil) {
+ self.onCancelKeyNegotiation = onCancelKeyNegotiation
+ }
+
+ func startNegotiation(
+ gatewayIP: IPv4Address,
+ devicePublicKey: WireGuardKitTypes.PublicKey,
+ presharedKey: WireGuardKitTypes.PrivateKey,
+ packetTunnel: PacketTunnelCore.TunnelProvider,
+ tcpConnection: NWTCPConnection,
+ postQuantumKeyExchangeTimeout: MullvadTypes.Duration
+ ) -> Bool { false }
+
+ func cancelKeyNegotiation() {
+ onCancelKeyNegotiation?()
+ }
+}
+
+class SuccessfulNegotiatorStub: PostQuantumKeyNegotiating {
+ var onCancelKeyNegotiation: (() -> Void)?
+ required init() {
+ onCancelKeyNegotiation = nil
+ }
+
+ init(onCancelKeyNegotiation: (() -> Void)? = nil) {
+ self.onCancelKeyNegotiation = onCancelKeyNegotiation
+ }
+
+ func startNegotiation(
+ gatewayIP: IPv4Address,
+ devicePublicKey: WireGuardKitTypes.PublicKey,
+ presharedKey: WireGuardKitTypes.PrivateKey,
+ packetTunnel: PacketTunnelCore.TunnelProvider,
+ tcpConnection: NWTCPConnection,
+ postQuantumKeyExchangeTimeout: MullvadTypes.Duration
+ ) -> Bool { true }
+
+ func cancelKeyNegotiation() {
+ onCancelKeyNegotiation?()
+ }
+}
+
+// swiftlint:enable function_parameter_count
diff --git a/ios/MullvadRustRuntimeTests/PostQuantumKeyExchangeActorTests.swift b/ios/MullvadRustRuntimeTests/PostQuantumKeyExchangeActorTests.swift
new file mode 100644
index 0000000000..201ff51633
--- /dev/null
+++ b/ios/MullvadRustRuntimeTests/PostQuantumKeyExchangeActorTests.swift
@@ -0,0 +1,94 @@
+//
+// PostQuantumKeyExchangeActorTests.swift
+// PostQuantumKeyExchangeActorTests
+//
+// Created by Marco Nikic on 2024-06-12.
+// Copyright © 2024 Mullvad VPN AB. All rights reserved.
+//
+
+@testable import MullvadMockData
+@testable import MullvadRustRuntime
+@testable import MullvadTypes
+import NetworkExtension
+@testable import PacketTunnelCore
+@testable import WireGuardKitTypes
+import XCTest
+
+class MullvadPostQuantumTests: XCTestCase {
+ var tcpConnection: NWTCPConnectionStub!
+ var tunnelProvider: TunnelProviderStub!
+
+ override func setUpWithError() throws {
+ tcpConnection = NWTCPConnectionStub()
+ tunnelProvider = TunnelProviderStub(tcpConnection: tcpConnection)
+ }
+
+ func testKeyExchangeFailsWhenNegotiationCannotStart() {
+ let negotiationFailure = expectation(description: "Negotiation failed")
+
+ let keyExchangeActor = PostQuantumKeyExchangeActor(
+ packetTunnel: tunnelProvider,
+ onFailure: {
+ negotiationFailure.fulfill()
+ },
+ negotiationProvider: FailedNegotiatorStub.self,
+ iteratorProvider: { AnyIterator { .milliseconds(10) } }
+ )
+
+ let privateKey = PrivateKey()
+ keyExchangeActor.startNegotiation(with: privateKey)
+ tcpConnection.becomeViable()
+
+ wait(for: [negotiationFailure])
+ }
+
+ func testKeyExchangeFailsWhenTCPConnectionIsNotReadyInTime() {
+ let negotiationFailure = expectation(description: "Negotiation failed")
+
+ // Setup the actor to wait only 10 milliseconds before declaring the TCP connection is not ready in time.
+ let keyExchangeActor = PostQuantumKeyExchangeActor(
+ packetTunnel: tunnelProvider,
+ onFailure: {
+ negotiationFailure.fulfill()
+ },
+ negotiationProvider: FailedNegotiatorStub.self,
+ iteratorProvider: { AnyIterator { .milliseconds(10) } }
+ )
+
+ let privateKey = PrivateKey()
+ keyExchangeActor.startNegotiation(with: privateKey)
+
+ wait(for: [negotiationFailure])
+ }
+
+ func testResetEndsTheCurrentNegotiation() throws {
+ let unexpectedNegotiationFailure = expectation(description: "Unexpected negotiation failure")
+ unexpectedNegotiationFailure.isInverted = true
+
+ let keyExchangeActor = PostQuantumKeyExchangeActor(
+ packetTunnel: tunnelProvider,
+ onFailure: {
+ unexpectedNegotiationFailure.fulfill()
+ },
+ negotiationProvider: SuccessfulNegotiatorStub.self,
+ iteratorProvider: { AnyIterator { .milliseconds(10) } }
+ )
+
+ let privateKey = PrivateKey()
+ keyExchangeActor.startNegotiation(with: privateKey)
+
+ let negotiationProvider = try XCTUnwrap(
+ keyExchangeActor.negotiation?
+ .negotiator as? SuccessfulNegotiatorStub
+ )
+
+ let negotationCancelledExpectation = expectation(description: "Negotiation cancelled")
+ negotiationProvider.onCancelKeyNegotiation = {
+ negotationCancelledExpectation.fulfill()
+ }
+
+ keyExchangeActor.reset()
+
+ wait(for: [negotationCancelledExpectation, unexpectedNegotiationFailure], timeout: .UnitTest.invertedTimeout)
+ }
+}
diff --git a/ios/MullvadRustRuntimeTests/TCPConnection.swift b/ios/MullvadRustRuntimeTests/TCPConnection.swift
new file mode 100644
index 0000000000..64c4e2a77d
--- /dev/null
+++ b/ios/MullvadRustRuntimeTests/TCPConnection.swift
@@ -0,0 +1,73 @@
+//
+// TCPConnection.swift
+// TunnelObfuscationTests
+//
+// Created by pronebird on 27/06/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Network
+
+/// Minimal implementation of TCP connection capable of receiving data.
+/// > Warning: Do not use this implementation in production code. See the warning in `start()`.
+class TCPConnection {
+ private let dispatchQueue = DispatchQueue(label: "TCPConnection")
+ private let nwConnection: NWConnection
+
+ init(nwConnection: NWConnection) {
+ self.nwConnection = nwConnection
+ }
+
+ deinit {
+ cancel()
+ }
+
+ /// Establishes the TCP connection.
+ ///
+ /// > Warning: This implementation is **not safe to use in production**
+ /// It will cancel the `listener.stateUpdateHandler` after it becomes ready and ignore future updates.
+ ///
+ /// Waits for the underlying connection to become ready before returning control to the caller, otherwise throws an
+ /// error if connection state indicates as such.
+ func start() async throws {
+ try await withCheckedThrowingContinuation { continuation in
+ nwConnection.stateUpdateHandler = { state in
+ switch state {
+ case .ready:
+ continuation.resume(returning: ())
+ case let .failed(error):
+ continuation.resume(throwing: error)
+ case .cancelled:
+ continuation.resume(throwing: CancellationError())
+ default:
+ return
+ }
+ // Reset state update handler after resuming continuation.
+ self.nwConnection.stateUpdateHandler = nil
+ }
+ nwConnection.start(queue: dispatchQueue)
+ }
+ }
+
+ func cancel() {
+ nwConnection.cancel()
+ }
+
+ func receiveData(minimumLength: Int, maximumLength: Int) async throws -> Data {
+ return try await withCheckedThrowingContinuation { continuation in
+ nwConnection.receive(
+ minimumIncompleteLength: minimumLength,
+ maximumLength: maximumLength
+ ) { content, _, isComplete, error in
+ if let error {
+ continuation.resume(throwing: error)
+ } else if let content {
+ continuation.resume(returning: content)
+ } else if isComplete {
+ continuation.resume(returning: Data())
+ }
+ }
+ }
+ }
+}
diff --git a/ios/MullvadRustRuntimeTests/TCPUnsafeListener.swift b/ios/MullvadRustRuntimeTests/TCPUnsafeListener.swift
new file mode 100644
index 0000000000..7d7b9ed949
--- /dev/null
+++ b/ios/MullvadRustRuntimeTests/TCPUnsafeListener.swift
@@ -0,0 +1,81 @@
+//
+// TCPUnsafeListener.swift
+// TunnelObfuscationTests
+//
+// Created by pronebird on 27/06/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Network
+
+/// Minimal implementation of a TCP listener.
+/// > Warning: Do not use this implementation in production code. See the warning in `start()`.
+class TCPUnsafeListener {
+ private let dispatchQueue = DispatchQueue(label: "TCPListener")
+ private let listener: NWListener
+
+ /// A stream of new TCP connections.
+ /// The caller may iterate over this stream to accept new TCP connections.
+ ///
+ /// `TCPConnection` objects are returned unopen, so the caller has to call `TCPConnection.start()` to accept the
+ /// connection before initiating the data exchange.
+ let newConnections: AsyncStream<TCPConnection>
+
+ init() throws {
+ let listener = try NWListener(using: .tcp)
+
+ newConnections = AsyncStream { continuation in
+ listener.newConnectionHandler = { nwConnection in
+ continuation.yield(TCPConnection(nwConnection: nwConnection))
+ }
+ continuation.onTermination = { @Sendable _ in
+ listener.newConnectionHandler = nil
+ }
+ }
+
+ self.listener = listener
+ }
+
+ deinit {
+ cancel()
+ }
+
+ /// Local TCP port bound by listener on which it accepts new connections.
+ var listenPort: UInt16 {
+ return listener.port?.rawValue ?? 0
+ }
+
+ /// Start listening on a randomly assigned port which should be available via `listenPort` once this call returns
+ /// control back to the caller.
+ ///
+ /// > Warning: This implementation is **not safe to use in production**
+ /// It will cancel the `listener.stateUpdateHandler` after it becomes ready and ignore future updates.
+ ///
+ /// Waits for the underlying connection to become ready before returning control to the caller, otherwise throws an
+ /// error if connection state indicates as such.
+ func start() async throws {
+ try await withCheckedThrowingContinuation { continuation in
+ listener.stateUpdateHandler = { state in
+ switch state {
+ case .ready:
+ continuation.resume(returning: ())
+ case let .failed(error):
+ continuation.resume(throwing: error)
+ case let .waiting(error):
+ continuation.resume(throwing: error)
+ case .cancelled:
+ continuation.resume(throwing: CancellationError())
+ default:
+ return
+ }
+ // Reset state update handler after resuming continuation.
+ self.listener.stateUpdateHandler = nil
+ }
+ listener.start(queue: dispatchQueue)
+ }
+ }
+
+ func cancel() {
+ listener.cancel()
+ }
+}
diff --git a/ios/MullvadRustRuntimeTests/TunnelObfuscationTests.swift b/ios/MullvadRustRuntimeTests/TunnelObfuscationTests.swift
new file mode 100644
index 0000000000..b2e28a468f
--- /dev/null
+++ b/ios/MullvadRustRuntimeTests/TunnelObfuscationTests.swift
@@ -0,0 +1,49 @@
+//
+// TunnelObfuscationTests.swift
+// TunnelObfuscationTests
+//
+// Created by pronebird on 27/06/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import MullvadRustRuntime
+import Network
+import XCTest
+
+final class TunnelObfuscationTests: XCTestCase {
+ func testRunningObfuscatorProxy() async throws {
+ // Each packet is prefixed with u16 that contains a payload length.
+ let preambleLength = MemoryLayout<UInt16>.size
+ let markerData = Data([109, 117, 108, 108, 118, 97, 100])
+ let packetLength = markerData.count + preambleLength
+
+ let tcpListener = try TCPUnsafeListener()
+ try await tcpListener.start()
+
+ let obfuscator = UDPOverTCPObfuscator(remoteAddress: IPv4Address.loopback, tcpPort: tcpListener.listenPort)
+ obfuscator.start()
+
+ // Accept incoming connections
+ let connectionDataTask = Task {
+ for await newConnection in tcpListener.newConnections {
+ try await newConnection.start()
+
+ return try await newConnection.receiveData(
+ minimumLength: packetLength,
+ maximumLength: packetLength
+ )
+ }
+ throw POSIXError(.ECANCELED)
+ }
+
+ // Send marker data over UDP
+ let connection = UDPConnection(remote: IPv4Address.loopback, port: obfuscator.localUdpPort)
+ try await connection.start()
+ try await connection.sendData(markerData)
+
+ // Validate the sent data
+ let receivedData = try await connectionDataTask.value
+ XCTAssert(receivedData.count == packetLength)
+ XCTAssertEqual(receivedData[preambleLength...], markerData)
+ }
+}
diff --git a/ios/MullvadRustRuntimeTests/UDPConnection.swift b/ios/MullvadRustRuntimeTests/UDPConnection.swift
new file mode 100644
index 0000000000..8848643c05
--- /dev/null
+++ b/ios/MullvadRustRuntimeTests/UDPConnection.swift
@@ -0,0 +1,72 @@
+//
+// UDPConnection.swift
+// TunnelObfuscationTests
+//
+// Created by pronebird on 27/06/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import Network
+
+/// Minimal implementation of UDP connection capable of sending data.
+/// > Warning: Do not use this implementation in production code. See the warning in `start()`.
+class UDPConnection {
+ private let dispatchQueue = DispatchQueue(label: "UDPConnection")
+ private let nwConnection: NWConnection
+
+ init(remote: IPAddress, port: UInt16) {
+ nwConnection = NWConnection(
+ host: NWEndpoint.Host("\(remote)"),
+ port: NWEndpoint.Port(integerLiteral: port),
+ using: .udp
+ )
+ }
+
+ deinit {
+ cancel()
+ }
+
+ /// Establishes the UDP connection.
+ ///
+ /// > Warning: This implementation is **not safe to use in production**
+ /// It will cancel the `listener.stateUpdateHandler` after it becomes ready and ignore future updates.
+ ///
+ /// Waits for the underlying connection to become ready before returning control to the caller, otherwise throws an
+ /// error if connection state indicates as such.
+ func start() async throws {
+ return try await withCheckedThrowingContinuation { continuation in
+ nwConnection.stateUpdateHandler = { state in
+ switch state {
+ case .ready:
+ continuation.resume(returning: ())
+ case let .failed(error):
+ continuation.resume(throwing: error)
+ case .cancelled:
+ continuation.resume(throwing: CancellationError())
+ default:
+ return
+ }
+ // Reset state update handler after resuming continuation.
+ self.nwConnection.stateUpdateHandler = nil
+ }
+ nwConnection.start(queue: dispatchQueue)
+ }
+ }
+
+ func cancel() {
+ nwConnection.cancel()
+ }
+
+ func sendData(_ data: Data) async throws {
+ return try await withCheckedThrowingContinuation { continuation in
+ nwConnection.send(content: data, completion: .contentProcessed { error in
+ if let error {
+ continuation.resume(throwing: error)
+ } else {
+ continuation.resume(returning: ())
+ }
+ })
+ }
+ }
+}