Skip to content

Commit

Permalink
Add a connection timeout check for the in-tunnel TCP connection
Browse files Browse the repository at this point in the history
Signed-off-by: Bug Magnet <[email protected]>
  • Loading branch information
buggmagnet committed May 16, 2024
1 parent 6e0fafc commit 07152f3
Show file tree
Hide file tree
Showing 16 changed files with 390 additions and 216 deletions.
11 changes: 1 addition & 10 deletions ios/MullvadPostQuantum/MullvadPostQuantum.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,4 @@
//

#import <Foundation/Foundation.h>

//! Project version number for MullvadPostQuantum.
FOUNDATION_EXPORT double MullvadPostQuantumVersionNumber;

//! Project version string for MullvadPostQuantum.
FOUNDATION_EXPORT const unsigned char MullvadPostQuantumVersionString[];

// In this header, you should import all the public headers of your framework using statements like #import <MullvadPostQuantum/PublicHeader.h>


#import "talpid_tunnel_config_client.h"
68 changes: 52 additions & 16 deletions ios/MullvadPostQuantum/PacketTunnelProvider+TCPConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,46 +12,82 @@ import NetworkExtension
import TalpidTunnelConfigClientProxy
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(
connection: UnsafeMutableRawPointer?,
data: UnsafeMutableRawPointer,
rawConnection: UnsafeMutableRawPointer?,
rawData: UnsafeMutableRawPointer,
dataLength: UInt,
sender: UnsafeMutableRawPointer?
rawWriteAcknowledgement: UnsafeMutableRawPointer?
) {
guard let connection, let sender else { return }
let tcpConnection = Unmanaged<NWTCPConnection>.fromOpaque(connection).takeUnretainedValue()
let rawData = Data(bytes: data, count: Int(dataLength))
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 no more than 2 writes happen in parallel is done by virtue of not returning the execution context
// 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(rawData, completionHandler: { maybeError in
tcpConnection.write(data, completionHandler: { maybeError in
if maybeError != nil {
handle_sent(0, sender)
handle_sent(0, rawWriteAcknowledgement)
} else {
handle_sent(dataLength, sender)
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(
connection: UnsafeMutableRawPointer?,
sender: UnsafeMutableRawPointer?
rawConnection: UnsafeMutableRawPointer?,
rawReadAcknowledgement: UnsafeMutableRawPointer?
) {
guard let connection, let sender else { return }
let tcpConnection = Unmanaged<NWTCPConnection>.fromOpaque(connection).takeUnretainedValue()
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(Data().map { $0 }, 0, sender)
handle_recv(nil, 0, rawReadAcknowledgement)
} else {
handle_recv(data.map { $0 }, UInt(data.count), sender)
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(
rawPacketTunnel: UnsafeMutableRawPointer?,
Expand Down
12 changes: 7 additions & 5 deletions ios/MullvadPostQuantum/PostQuantumKeyNegotiator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,25 @@ import NetworkExtension
import TalpidTunnelConfigClientProxy
import WireGuardKitTypes

/**
Attempt to start the asynchronous process of key negotiation. Returns true if successfully started, false if failed.
*/
public class PostQuantumKeyNegotiator {
public init() {}

var cancelToken: PostQuantumCancelToken?

public func negotiateKey(
public func startNegotiation(
gatewayIP: IPv4Address,
devicePublicKey: PublicKey,
presharedKey: PrivateKey,
packetTunnel: NEPacketTunnelProvider,
tcpConnection: NWTCPConnection
) {
) -> Bool {
let packetTunnelPointer = Unmanaged.passUnretained(packetTunnel).toOpaque()
let opaqueConnection = Unmanaged.passUnretained(tcpConnection).toOpaque()
var cancelToken = PostQuantumCancelToken()

// TODO: Any non 0 return is considered a failure, and should be handled gracefully
let result = negotiate_post_quantum_key(
devicePublicKey.rawValue.map { $0 },
presharedKey.rawValue.map { $0 },
Expand All @@ -36,10 +38,10 @@ public class PostQuantumKeyNegotiator {
&cancelToken
)
guard result == 0 else {
// Handle failure here
return
return false
}
self.cancelToken = cancelToken
return true
}

public func cancelKeyNegotiation() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,44 @@ typedef struct PostQuantumCancelToken {
} PostQuantumCancelToken;

/**
* * # Safety * `sender` must be pointing to a valid instance of a `PostQuantumCancelToken` created by the `PacketTunnelProvider`
* Called by the Swift side to signal that the quantum-secure key exchange should be cancelled.
*
* # 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);

/**
* * # Safety * `sender` must be pointing to a valid instance of a `PostQuantumCancelToken` created by the `PacketTunnelProvider`.
* 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);

/**
* * # 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.
* 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);
void handle_sent(uintptr_t bytes_sent, const void *sender);

/**
* * # 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.
* 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,
Expand All @@ -39,7 +60,11 @@ void handle_recv(const uint8_t *data,
* Entry point for exchanging post quantum keys on iOS.
* The TCP connection must be created to go through the tunnel.
* # Safety
* This function is safe to call
* `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,
Expand Down
4 changes: 4 additions & 0 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
44DD7D272B6D18FB0005F67F /* MockTunnelInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */; };
44DD7D292B7113CA0005F67F /* MockTunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D282B7113CA0005F67F /* MockTunnel.swift */; };
44DD7D2D2B74E44A0005F67F /* QuantumResistanceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DD7D2C2B74E44A0005F67F /* QuantumResistanceSettings.swift */; };
44DF8AC42BF20BD200869CA4 /* PacketTunnelActor+PostQuantum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44DF8AC32BF20BD200869CA4 /* PacketTunnelActor+PostQuantum.swift */; };
5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4AF2940A47300C23744 /* TunnelConfiguration.swift */; };
5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4B12940A48700C23744 /* TunnelStore.swift */; };
5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Split.swift */; };
Expand Down Expand Up @@ -1429,6 +1430,7 @@
44DD7D262B6D18FB0005F67F /* MockTunnelInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnelInteractor.swift; sourceTree = "<group>"; };
44DD7D282B7113CA0005F67F /* MockTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnel.swift; sourceTree = "<group>"; };
44DD7D2C2B74E44A0005F67F /* QuantumResistanceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuantumResistanceSettings.swift; sourceTree = "<group>"; };
44DF8AC32BF20BD200869CA4 /* PacketTunnelActor+PostQuantum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PacketTunnelActor+PostQuantum.swift"; sourceTree = "<group>"; };
5802EBC42A8E44AC00E5CE4C /* AppRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRoutes.swift; sourceTree = "<group>"; };
5802EBC62A8E457A00E5CE4C /* AppRouteProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteProtocol.swift; sourceTree = "<group>"; };
5802EBC82A8E45BA00E5CE4C /* ApplicationRouterDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationRouterDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3122,6 +3124,7 @@
58342C032AAB61FB003BA12D /* State+Extensions.swift */,
586E8DB72AAF4AC4007BF3DA /* Task+Duration.swift */,
58DDA18E2ABC32380039C360 /* Timings.swift */,
44DF8AC32BF20BD200869CA4 /* PacketTunnelActor+PostQuantum.swift */,
);
path = Actor;
sourceTree = "<group>";
Expand Down Expand Up @@ -5483,6 +5486,7 @@
58FE25DA2AA72A8F003D1918 /* PacketTunnelActor.swift in Sources */,
587A5E522ADD7569003A70F1 /* ObservedState+Extensions.swift in Sources */,
58FE25E62AA738E8003D1918 /* TunnelAdapterProtocol.swift in Sources */,
44DF8AC42BF20BD200869CA4 /* PacketTunnelActor+PostQuantum.swift in Sources */,
583832252AC318A100EA2071 /* PacketTunnelActor+ConnectionMonitoring.swift in Sources */,
58C7A4552A863FB90060C66F /* TunnelMonitor.swift in Sources */,
58C7AF182ABD84AB007EDD7A /* ProxyURLResponse.swift in Sources */,
Expand Down
6 changes: 1 addition & 5 deletions ios/MullvadVPN/TunnelManager/TunnelManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -670,15 +670,11 @@ final class TunnelManager: StorePaymentObserver {
refreshDeviceState()
}
switch newTunnelStatus.state {
case .connecting, .reconnecting:
case .connecting, .reconnecting, .negotiatingPostQuantumKey:
// Start polling tunnel status to keep the relay information up to date
// while the tunnel process is trying to connect.
startPollingTunnelStatus(interval: establishingTunnelStatusPollInterval)

case .negotiatingPostQuantumKey:
// No need to poll the tunnel while negotiating post quantum keys, assume the connection will work
break

case .connected, .waitingForConnectivity(.noConnection):
// Start polling tunnel status to keep connectivity status up to date.
startPollingTunnelStatus(interval: establishedTunnelStatusPollInterval)
Expand Down
24 changes: 10 additions & 14 deletions ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
protocolObfuscator: ProtocolObfuscator<UDPOverTCPObfuscator>()
)

postQuantumActor = PostQuantumKeyExchangeActor(packetTunnel: self)
postQuantumActor = PostQuantumKeyExchangeActor(
packetTunnel: self,
onFailure: self.keyExchangeFailed
)

let urlRequestProxy = URLRequestProxy(dispatchQueue: internalQueue, transportProvider: transportProvider)

Expand Down Expand Up @@ -249,6 +252,7 @@ extension PacketTunnelProvider {
lastConnectionAttempt = connectionAttempt

case let .negotiatingPostQuantumKey(_, privateKey):
postQuantumActor.endCurrentNegotiation()
postQuantumActor.startNegotiation(with: privateKey)

case .initial, .connected, .disconnecting, .disconnected, .error:
Expand All @@ -258,16 +262,6 @@ extension PacketTunnelProvider {
}
}

func createTCPConnectionForPQPSK(_ gatewayAddress: String) -> NWTCPConnection {
let gatewayEndpoint = NWHostEndpoint(hostname: gatewayAddress, port: "1337")
return createTCPConnectionThroughTunnel(
to: gatewayEndpoint,
enableTLS: false,
tlsParameters: nil,
delegate: nil
)
}

private func stopObservingActorState() {
stateObserverTask?.cancel()
stateObserverTask = nil
Expand Down Expand Up @@ -306,11 +300,13 @@ extension PacketTunnelProvider {
extension PacketTunnelProvider: PostQuantumKeyReceiving {
func receivePostQuantumKey(_ key: PreSharedKey, ephemeralKey: PrivateKey) {
actor.replacePreSharedKey(key, ephemeralKey: ephemeralKey)
postQuantumActor.acknowledgeNegotiationConcluded()
postQuantumActor.endCurrentNegotiation()
}

func keyExchangeFailed() {
postQuantumActor.acknowledgeNegotiationConcluded()
actor.reconnect(to: .current)
postQuantumActor.endCurrentNegotiation()
// Do not try reconnecting to the `.current` relay, else the actor's `State` equality check will fail
// and it will not try to reconnect
actor.reconnect(to: .random)
}
}
Loading

0 comments on commit 07152f3

Please sign in to comment.