Trim obvious comments, add architecture.md stub

This commit is contained in:
mfahampshire
2026-03-17 15:56:04 +00:00
parent 3c92ce60ca
commit 0f8a8ddf7e
4 changed files with 81 additions and 24 deletions
@@ -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<ip_packet_bytes>
```
## 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.
@@ -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<Recipient
Ok(ipr_address)
}
// ---------------------------------------------------------------------------
// IpMixStream
// ---------------------------------------------------------------------------
/// A bidirectional tunnel for sending and receiving IP packets through the mixnet.
///
/// Wraps a [`MixnetStream`] (opened to an IPR exit gateway) and provides a
@@ -156,7 +148,6 @@ pub struct IpMixStream {
client: MixnetClient,
/// Parses incoming IPR protocol responses.
listener: IprListener,
/// Reusable read buffer to avoid allocating per `handle_incoming()` call.
read_buf: Vec<u8>,
allocated_ips: Option<IpPair>,
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
}
@@ -230,7 +230,6 @@ impl MixnetMessageSinkTranslator for ToIprDataResponse {
&self,
bundled_ip_packets: &[u8],
) -> std::result::Result<InputMessage, nym_sdk::Error> {
// 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);
@@ -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,