diff options
| author | Bug Magnet <marco.nikic@mullvad.net> | 2024-06-24 13:21:40 +0200 |
|---|---|---|
| committer | Emīls <emils@mullvad.net> | 2024-07-17 11:48:13 +0200 |
| commit | 2df280fbf02fa1b39efffb0b27b297d39d6369c0 (patch) | |
| tree | 1b6a657bb8c594e82a50bd4dfcbfb85d34395fa1 /ios/MullvadRustRuntimeTests | |
| parent | b6fe08388dcbfbe1fb54a1c89322c329be2f54f9 (diff) | |
| download | mullvadvpn-2df280fbf02fa1b39efffb0b27b297d39d6369c0.tar.xz mullvadvpn-2df280fbf02fa1b39efffb0b27b297d39d6369c0.zip | |
Add a Rust FFI, Disable sandboxing for scripts
Diffstat (limited to 'ios/MullvadRustRuntimeTests')
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: ()) + } + }) + } + } +} |
