diff --git a/sdk/rust/nym-sdk/src/ipr_wrapper/architecture.md b/sdk/rust/nym-sdk/src/ipr_wrapper/architecture.md new file mode 100644 index 0000000000..7740bfe36c --- /dev/null +++ b/sdk/rust/nym-sdk/src/ipr_wrapper/architecture.md @@ -0,0 +1,79 @@ +# IpMixStream Architecture + +## Overview + +`IpMixStream` tunnels IP packets through the Nym mixnet to an IP Packet Router +(IPR) exit gateway. It provides a high-level API over a single `MixnetStream`, +which handles LP Stream framing and Sphinx packet transport automatically. + +## Data Flow + +```text +Client IPR (exit gateway) +------ ------------------ +IpMixStream.send_ip_packet(bytes) + IpPacketRequest.to_bytes() + MixnetStream.write() + LP Stream frame + Sphinx packets + mixnet ──────────────────> on_reconstructed_message() + detect LpFrameKind::Stream + strip LP header + parse IpPacketRequest + write IP packet to TUN + ──> internet + + internet response arrives on TUN + ConnectedClientHandler + wrap in IpPacketResponse + wrap in LP Stream frame + mixnet <────────────────── send via Sphinx/SURBs + + stream router dispatches + by stream_id + MixnetStream.read() + IprListener parses response +IpMixStream.handle_incoming() + returns Vec +``` + +## Layer Stack + +```text +IpMixStream IPR protocol (connect, data, disconnect) +MixnetStream AsyncRead + AsyncWrite, LP Stream framing, seq numbers +Stream Router Dispatches inbound messages by stream_id +MixnetClient Sphinx packet encryption, SURB management +Mixnet Entry GW -> Mix1 -> Mix2 -> Mix3 -> Exit GW +``` + +## LP Stream Framing + +All messages between client and IPR are wrapped in LP Stream frames: + +- **Client -> IPR**: `MixnetStream.write()` wraps each write in an LP Stream + Data frame (stream_id, sequence number, payload). The IPR detects + `LpFrameKind::Stream` and strips the header before processing. + +- **IPR -> Client**: Both inline responses (connect handshake, pong) and async + TUN responses are wrapped in LP Stream frames with the same stream_id. The + client's stream router dispatches by stream_id to the correct `MixnetStream`. + +## Connection Lifecycle + +1. `IpMixStream::new(env)` -- discover IPR, connect MixnetClient, open MixnetStream +2. `connect_tunnel()` -- send connect request, receive allocated IPs +3. `send_ip_packet()` / `handle_incoming()` -- steady-state data transfer +4. `disconnect_stream()` -- tear down MixnetClient + +## Key Design Decisions + +- **MixnetStream over MixnetClient**: One stream per IPR tunnel. LP framing is + handled by MixnetStream internally, no manual frame construction needed. + +- **Multiplexing at IP layer**: Different remote hosts are addressed by IP + packet destination headers, not by opening multiple streams. + +- **stream_id threading**: The IPR stores stream_id in each client's + `ConnectedClientHandler` so async TUN responses are wrapped in matching LP + Stream frames for correct dispatch at the client. diff --git a/sdk/rust/nym-sdk/src/ipr_wrapper/ip_mix_stream.rs b/sdk/rust/nym-sdk/src/ipr_wrapper/ip_mix_stream.rs index 7352283709..2c4bfbd8d7 100644 --- a/sdk/rust/nym-sdk/src/ipr_wrapper/ip_mix_stream.rs +++ b/sdk/rust/nym-sdk/src/ipr_wrapper/ip_mix_stream.rs @@ -32,10 +32,6 @@ const IPR_CONNECT_TIMEOUT: Duration = Duration::from_secs(60); /// provides ample headroom. const READ_BUF_SIZE: usize = 64 * 1024; -// --------------------------------------------------------------------------- -// Gateway discovery helpers -// --------------------------------------------------------------------------- - #[derive(Clone)] pub struct IprWithPerformance { pub(crate) address: Recipient, @@ -125,10 +121,6 @@ async fn get_random_ipr(client: nym_http_api_client::Client) -> Result, allocated_ips: Option, connection_state: ConnectionState, @@ -226,8 +217,6 @@ impl IpMixStream { let (request, request_id) = IpPacketRequest::new_connect_request(None); debug!("Sending connect request with ID: {}", request_id); - // Write the connect request — MixnetStream wraps it in an LP Stream - // Data frame automatically. let request_bytes = request.to_bytes()?; self.stream .write_all(&request_bytes) @@ -353,12 +342,10 @@ impl IpMixStream { } } - /// Get the allocated IP addresses for this tunnel. pub fn allocated_ips(&self) -> Option<&IpPair> { self.allocated_ips.as_ref() } - /// Check if the tunnel is currently connected. pub fn is_connected(&self) -> bool { self.connection_state == ConnectionState::Connected } diff --git a/service-providers/ip-packet-router/src/clients/connected_client_handler.rs b/service-providers/ip-packet-router/src/clients/connected_client_handler.rs index 6381841828..bde64d05e3 100644 --- a/service-providers/ip-packet-router/src/clients/connected_client_handler.rs +++ b/service-providers/ip-packet-router/src/clients/connected_client_handler.rs @@ -230,7 +230,6 @@ impl MixnetMessageSinkTranslator for ToIprDataResponse { &self, bundled_ip_packets: &[u8], ) -> std::result::Result { - // Create an IPR packet response that the recipient can understand let response_packet = create_ip_packet_response(bundled_ip_packets, self.client_version)?; // Optionally wrap in LP Stream frame for stream-mode clients @@ -249,7 +248,6 @@ impl MixnetMessageSinkTranslator for ToIprDataResponse { response_packet }; - // Wrap in a mixnet input message let input_message = crate::util::create_message::create_input_message(&self.send_to, final_packet) .with_max_retransmissions(0); diff --git a/service-providers/ip-packet-router/src/mixnet_listener.rs b/service-providers/ip-packet-router/src/mixnet_listener.rs index 886264b2c2..1675c3daf6 100644 --- a/service-providers/ip-packet-router/src/mixnet_listener.rs +++ b/service-providers/ip-packet-router/src/mixnet_listener.rs @@ -466,13 +466,8 @@ impl MixnetListener { /// Handle LP Stream-framed messages. /// - /// LP Stream frames carry IPR requests in the frame content. We parse the - /// stream attributes, process the inner payload, and handle responses inline - /// (wrapped in LP Stream frames) — the same pattern used by the KCP handler. - /// - /// The `current_stream_id` field is set during processing so that connect - /// handlers can pass it to `ConnectedClientHandler`, which wraps async TUN - /// responses in LP Stream frames too. + /// Parses stream attributes, processes the inner IPR payload, and handles + /// responses inline (wrapped in LP Stream frames) — same pattern as KCP. async fn on_stream_frame( &mut self, reconstructed: ReconstructedMessage, @@ -482,7 +477,6 @@ impl MixnetListener { reconstructed.message.len() ); - // Parse stream attributes from the LP header let header = LpFrameHeader::parse(&reconstructed.message) .map_err(|e| IpPacketRouterError::Other(format!("Invalid LP frame header: {e}")))?; let attrs = StreamFrameAttributes::parse(&header.frame_attributes).map_err(|e| { @@ -508,7 +502,6 @@ impl MixnetListener { return Ok(vec![]); } - // Strip LP header, process inner payload as IPR message let inner_reconstructed = ReconstructedMessage { message: payload.to_vec(), sender_tag: reconstructed.sender_tag,