Skip to main content

dig_nat/
mux.rs

1//! Stream multiplexing + byte-range streams over a single established peer connection.
2//!
3//! Every traversal tier (direct / UPnP / NAT-PMP / PCP / hole-punch / relayed) yields the SAME
4//! thing: one mTLS byte stream to the peer. On top of that single stream this module layers
5//! **[`yamux`]** multiplexing so the content/download layer can open **many cheap concurrent logical
6//! streams** to the peer with no head-of-line blocking — the transport is **streaming-first**, never
7//! "send request, buffer the whole response in memory".
8//!
9//! Two capabilities:
10//!
11//! 1. **Multiplexing** — [`PeerSession::open_stream`] opens an independent bidirectional
12//!    [`PeerStream`] (a tokio [`AsyncRead`] + [`AsyncWrite`]); open N of them concurrently and read
13//!    each incrementally with natural backpressure (yamux windows).
14//! 2. **Byte-range streams** — [`PeerSession::open_range_stream`] opens a stream scoped to a
15//!    `[offset, offset+len)` range of a named resource by writing a small [`RangeRequest`] preamble,
16//!    then hands back the stream so the caller reads exactly those bytes as they arrive. A downloader
17//!    opens range streams to DIFFERENT peers in parallel and reassembles — multi-source parallel
18//!    download falls out of "streams are cheap + multiplexed + range-scoped".
19//!
20//! The uniform abstraction holds regardless of how the connection was established, and regardless of
21//! whether the underlying byte stream is direct or (tier-6) relay-proxied.
22//!
23//! ## Wire alignment (normative)
24//!
25//! The control + range types here conform to the published **L7 peer-network spec** (docs.dig.net
26//! "L7 · DIG Node peer network", §8 streaming, §9 byte-range fetch + availability). The shapes are
27//! the `dig.getAvailability` / `dig.fetchRange` request/response and the streamed `RangeFrame`
28//! (`{offset, length, bytes, complete}`, first frame adding `total_length` + `chunk_lens` +
29//! `chunk_index` + `inclusion_proof` + `root`). Per-chunk integrity (split by `chunk_lens`, verify
30//! the whole-resource inclusion proof vs the chain-anchored `root`, AES-256-GCM-SIV-open) is done by
31//! the CONTENT layer above dig-nat; dig-nat carries these frames faithfully over the mux transport.
32
33use std::io;
34
35use serde::{Deserialize, Serialize};
36use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
37use tokio_util::compat::{Compat, FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
38
39/// One logical, bidirectional stream to the peer — a tokio [`AsyncRead`] + [`AsyncWrite`]. Reads
40/// deliver bytes incrementally as they arrive (streaming, with yamux-window backpressure); many
41/// [`PeerStream`]s coexist on one [`PeerSession`] without head-of-line blocking.
42///
43/// yamux streams are `futures` streams; this is the tokio-trait view via `tokio-util` compat.
44pub type PeerStream = Compat<yamux::Stream>;
45
46/// One item in a [`dig.getAvailability`](AvailabilityRequest) batch — a resource key at store, root,
47/// or capsule/resource granularity (inferred from which fields are present, per the L7 spec §9):
48/// `store_id` only → *has_store*; `+ root` → *has_root* (the capsule `store_id:root`); `+
49/// retrieval_key` → *has_resource*. Hashes are 64-hex.
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub struct AvailabilityItem {
52    /// The store id (64-hex). Always present.
53    pub store_id: String,
54    /// The generation root (64-hex). Present for root/resource granularity.
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub root: Option<String>,
57    /// The resource retrieval key (64-hex). Present for resource granularity.
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub retrieval_key: Option<String>,
60}
61
62/// The **availability pre-check** (`dig.getAvailability`, L7 spec §9) — asked BEFORE any range fetch.
63/// A multi-source download batches candidate peers × items in one call each and only fans byte-range
64/// requests at peers that answer *available* — never opening range streams to peers that may not hold
65/// the content. A message-style control call over the mux'd mTLS connection.
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct AvailabilityRequest {
68    /// The items to check, batched.
69    pub items: Vec<AvailabilityItem>,
70}
71
72/// One answer in an [`AvailabilityResponse`], positionally aligned with the request `items`.
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct AvailabilityAnswer {
75    /// Whether the peer holds the queried item. Always present.
76    pub available: bool,
77    /// (store granularity) generation roots the peer holds for the store, newest-first.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub roots: Option<Vec<String>>,
80    /// (root/resource granularity) the ciphertext length — lets the caller plan its ranges.
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub total_length: Option<u64>,
83    /// (root/resource granularity) the chunk count.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub chunk_count: Option<u64>,
86    /// Whether the peer holds the FULL resource/capsule (`true`) or only part (`false`).
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub complete: Option<bool>,
89}
90
91/// The peer's answer to an [`AvailabilityRequest`]: one [`AvailabilityAnswer`] per queried item,
92/// positionally aligned with the request's `items`.
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct AvailabilityResponse {
95    /// One answer per queried item, in request order.
96    pub items: Vec<AvailabilityAnswer>,
97}
98
99/// A byte-range request (`dig.fetchRange`, L7 spec §9) written at the start of a range-scoped stream.
100/// Identifies a resource (`store_id` + `retrieval_key` [+ `root`]) or a whole capsule
101/// (`capsule: true`, identified by `store_id` [+ `root`]) and the `[offset, offset+length)` range.
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct RangeRequest {
104    /// The store id (64-hex).
105    pub store_id: String,
106    /// The resource retrieval key (64-hex). Omitted when `capsule` is true.
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub retrieval_key: Option<String>,
109    /// The generation root (64-hex). Optional — defaults to the chain-anchored tip.
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub root: Option<String>,
112    /// Fetch a whole capsule / `.dig` (identified by `store_id` [+ `root`]) rather than one resource.
113    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
114    pub capsule: bool,
115    /// Start offset (bytes) into the resource ciphertext. Default `0`.
116    #[serde(default)]
117    pub offset: u64,
118    /// Length (bytes) to return (widened to whole-chunk boundaries; clamped to the node window).
119    pub length: u64,
120}
121
122/// One streamed `dig.fetchRange` frame (L7 spec §8 framing). Frames arrive in ascending `offset`
123/// order and tile the requested range exactly; the caller reassembles by `offset` and stops on
124/// `complete`. The **first frame** additionally carries the per-range verification metadata so a
125/// single-peer range is independently verifiable against the chain-anchored `root`.
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127pub struct RangeFrame {
128    /// This frame's start offset within the requested range.
129    pub offset: u64,
130    /// This frame's byte length.
131    pub length: u64,
132    /// The raw ciphertext bytes.
133    #[serde(with = "serde_bytes")]
134    pub bytes: Vec<u8>,
135    /// Whether this is the final frame of the range.
136    pub complete: bool,
137    /// (first frame only) the full resource ciphertext length.
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub total_length: Option<u64>,
140    /// (first frame only) per-chunk ciphertext lengths of the whole resource, in order.
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub chunk_lens: Option<Vec<u64>>,
143    /// (first frame only) index into `chunk_lens` of the first chunk in this frame.
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub chunk_index: Option<u64>,
146    /// (first frame only) merkle inclusion proof of the whole resource vs the generation `root`
147    /// (base64, relayed verbatim); `null`/absent for `capsule: true` (self-verifying on install).
148    #[serde(default, skip_serializing_if = "Option::is_none")]
149    pub inclusion_proof: Option<String>,
150    /// (first frame only) the generation root (64-hex) the inclusion proof is against.
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub root: Option<String>,
153}
154
155impl AvailabilityRequest {
156    /// Serialize as a `u32` big-endian length prefix + JSON body (the uniform control framing).
157    pub fn encode(&self) -> Vec<u8> {
158        encode_framed(self)
159    }
160    /// Read + decode an [`AvailabilityRequest`] from `r` (the peer/serving side).
161    pub async fn decode<R: AsyncRead + Unpin>(r: &mut R) -> io::Result<Self> {
162        decode_framed(r).await
163    }
164}
165
166impl AvailabilityResponse {
167    /// Serialize as a `u32` big-endian length prefix + JSON body.
168    pub fn encode(&self) -> Vec<u8> {
169        encode_framed(self)
170    }
171    /// Read + decode an [`AvailabilityResponse`] from `r` (the requesting side).
172    pub async fn decode<R: AsyncRead + Unpin>(r: &mut R) -> io::Result<Self> {
173        decode_framed(r).await
174    }
175}
176
177impl RangeRequest {
178    /// A range request for a content resource (`store_id` + `retrieval_key`).
179    pub fn resource(
180        store_id: impl Into<String>,
181        retrieval_key: impl Into<String>,
182        offset: u64,
183        length: u64,
184    ) -> Self {
185        RangeRequest {
186            store_id: store_id.into(),
187            retrieval_key: Some(retrieval_key.into()),
188            root: None,
189            capsule: false,
190            offset,
191            length,
192        }
193    }
194
195    /// Serialize as a `u32` big-endian length prefix + JSON body — the preamble a peer reads to learn
196    /// the resource + range before streaming the frames.
197    pub fn encode(&self) -> Vec<u8> {
198        encode_framed(self)
199    }
200    /// Read + decode a [`RangeRequest`] preamble from `r` (the serving side of a range stream).
201    pub async fn decode<R: AsyncRead + Unpin>(r: &mut R) -> io::Result<Self> {
202        decode_framed(r).await
203    }
204}
205
206impl RangeFrame {
207    /// Serialize as a `u32` big-endian length prefix + JSON body (one framed frame on the stream).
208    pub fn encode(&self) -> Vec<u8> {
209        encode_framed(self)
210    }
211    /// Read + decode one [`RangeFrame`] from `r`. Returns `Ok(None)` at clean end-of-stream (the
212    /// reader hit EOF on a frame boundary), so a consumer loops until `None` or `complete`.
213    pub async fn decode<R: AsyncRead + Unpin>(r: &mut R) -> io::Result<Option<Self>> {
214        decode_framed_opt(r).await
215    }
216}
217
218/// Maximum length-prefixed control/preamble body — guards against a malicious length prefix forcing
219/// a huge allocation.
220const MAX_FRAMED_BODY: usize = 64 * 1024;
221
222/// Serialize `value` as a `u32` big-endian length prefix + JSON body — the uniform framing for every
223/// small control message on a stream (availability + range preambles).
224fn encode_framed<T: Serialize>(value: &T) -> Vec<u8> {
225    let body = serde_json::to_vec(value).expect("control message serializes");
226    let mut out = Vec::with_capacity(4 + body.len());
227    out.extend_from_slice(&(body.len() as u32).to_be_bytes());
228    out.extend_from_slice(&body);
229    out
230}
231
232/// Read + decode a length-prefixed JSON control message from `r`, bounded by [`MAX_FRAMED_BODY`].
233async fn decode_framed<T: for<'de> Deserialize<'de>, R: AsyncRead + Unpin>(
234    r: &mut R,
235) -> io::Result<T> {
236    let mut len_buf = [0u8; 4];
237    r.read_exact(&mut len_buf).await?;
238    let len = u32::from_be_bytes(len_buf) as usize;
239    if len > MAX_FRAMED_BODY {
240        return Err(io::Error::new(
241            io::ErrorKind::InvalidData,
242            "control message too large",
243        ));
244    }
245    let mut body = vec![0u8; len];
246    r.read_exact(&mut body).await?;
247    serde_json::from_slice(&body).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
248}
249
250/// Like [`decode_framed`] but returns `Ok(None)` on a CLEAN end-of-stream at a frame boundary (the
251/// length prefix read hits immediate EOF), so a streaming consumer can loop until the stream ends.
252async fn decode_framed_opt<T: for<'de> Deserialize<'de>, R: AsyncRead + Unpin>(
253    r: &mut R,
254) -> io::Result<Option<T>> {
255    let mut len_buf = [0u8; 4];
256    match r.read_exact(&mut len_buf).await {
257        Ok(_) => {}
258        Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
259        Err(e) => return Err(e),
260    }
261    let len = u32::from_be_bytes(len_buf) as usize;
262    if len > MAX_FRAMED_BODY {
263        return Err(io::Error::new(
264            io::ErrorKind::InvalidData,
265            "control message too large",
266        ));
267    }
268    let mut body = vec![0u8; len];
269    r.read_exact(&mut body).await?;
270    serde_json::from_slice(&body)
271        .map(Some)
272        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
273}
274
275/// One command to the yamux driver task. yamux 0.13 has no `Control` handle, so we drive the
276/// [`yamux::Connection`] in a task and talk to it over this channel.
277enum MuxCommand {
278    /// Open a new outbound stream; the resulting [`yamux::Stream`] (or error) comes back on the sender.
279    OpenOutbound(tokio::sync::oneshot::Sender<Result<yamux::Stream, String>>),
280}
281
282/// A multiplexed session over one peer connection: open many concurrent logical [`PeerStream`]s.
283///
284/// yamux 0.13 exposes a poll-based [`yamux::Connection`] (no `Control` handle), so a background
285/// driver task owns the connection and serves open-stream requests over a command channel; inbound
286/// streams are surfaced on [`Self::inbound_rx`] for a serving node. Dropping the session closes the
287/// command channel, which ends the driver and tears down the underlying byte stream.
288pub struct PeerSession {
289    cmd_tx: tokio::sync::mpsc::Sender<MuxCommand>,
290    /// Inbound streams opened BY the peer (server role / bidirectional use). A pure client can
291    /// ignore this; a serving node reads accepted range-request streams from here.
292    inbound_rx: tokio::sync::mpsc::Receiver<PeerStream>,
293}
294
295impl std::fmt::Debug for PeerSession {
296    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297        f.debug_struct("PeerSession").finish_non_exhaustive()
298    }
299}
300
301impl PeerSession {
302    /// Wrap an established mTLS byte stream in yamux as the **client** (outbound-stream opener) and
303    /// spawn the driver. `io` is any tokio duplex stream (the mTLS [`tokio_rustls::client::TlsStream`]
304    /// or, in tests, a loopback stream). Returns the session; open streams with
305    /// [`Self::open_stream`] / [`Self::open_range_stream`].
306    pub fn client<S>(io: S) -> Self
307    where
308        S: AsyncRead + AsyncWrite + Send + Unpin + 'static,
309    {
310        Self::new(io, yamux::Mode::Client)
311    }
312
313    /// Wrap an established byte stream in yamux as the **server** (accepts inbound streams). Inbound
314    /// streams the peer opens are delivered via [`Self::accept_stream`]. Provided for symmetry + the
315    /// serving side of tests.
316    pub fn server<S>(io: S) -> Self
317    where
318        S: AsyncRead + AsyncWrite + Send + Unpin + 'static,
319    {
320        Self::new(io, yamux::Mode::Server)
321    }
322
323    fn new<S>(io: S, mode: yamux::Mode) -> Self
324    where
325        S: AsyncRead + AsyncWrite + Send + Unpin + 'static,
326    {
327        let (cmd_tx, cmd_rx) = tokio::sync::mpsc::channel::<MuxCommand>(64);
328        let (inbound_tx, inbound_rx) = tokio::sync::mpsc::channel::<PeerStream>(64);
329        let conn = yamux::Connection::new(io.compat(), yamux::Config::default(), mode);
330        tokio::spawn(drive_connection(conn, cmd_rx, inbound_tx));
331        PeerSession { cmd_tx, inbound_rx }
332    }
333
334    /// Open a new outbound logical stream to the peer. Cheap — open as many as you need to run
335    /// concurrent transfers without head-of-line blocking.
336    pub async fn open_stream(&mut self) -> io::Result<PeerStream> {
337        let (tx, rx) = tokio::sync::oneshot::channel();
338        self.cmd_tx
339            .send(MuxCommand::OpenOutbound(tx))
340            .await
341            .map_err(|_| io::Error::other("mux driver closed"))?;
342        let stream = rx
343            .await
344            .map_err(|_| io::Error::other("mux driver dropped request"))?
345            .map_err(io::Error::other)?;
346        Ok(stream.compat())
347    }
348
349    /// Accept the next inbound logical stream the peer opened (server side). Returns `None` when the
350    /// connection has closed. A pure client never calls this.
351    pub async fn accept_stream(&mut self) -> Option<PeerStream> {
352        self.inbound_rx.recv().await
353    }
354
355    /// Open a `dig.fetchRange` stream for `req`: opens a fresh logical stream, writes the
356    /// [`RangeRequest`] preamble, and returns the stream for the caller to read [`RangeFrame`]s from
357    /// (via [`RangeFrame::decode`]) as they arrive. The building block for multi-source parallel
358    /// range downloads — open one of these per (peer, range) and read them concurrently.
359    pub async fn open_range_stream(&mut self, req: &RangeRequest) -> io::Result<PeerStream> {
360        let mut stream = self.open_stream().await?;
361        stream.write_all(&req.encode()).await?;
362        stream.flush().await?;
363        Ok(stream)
364    }
365
366    /// **Availability pre-check** (`dig.getAvailability`) — ask the peer which of `items` it holds,
367    /// BEFORE opening any range streams. Opens a short-lived control stream, writes the batched
368    /// [`AvailabilityRequest`], reads the [`AvailabilityResponse`]. A multi-source downloader runs
369    /// this against candidate peers and only range-fetches from holders — the normative flow is:
370    /// discover peers → `query_availability` (batch) → fan byte-ranges across holders → verify each
371    /// vs the chain-anchored root → retry a bad range from another holder → reassemble.
372    pub async fn query_availability(
373        &mut self,
374        items: Vec<AvailabilityItem>,
375    ) -> io::Result<AvailabilityResponse> {
376        let req = AvailabilityRequest { items };
377        let mut stream = self.open_stream().await?;
378        stream.write_all(&req.encode()).await?;
379        stream.flush().await?;
380        AvailabilityResponse::decode(&mut stream).await
381    }
382}
383
384/// Drive one yamux [`Connection`](yamux::Connection): concurrently service open-outbound commands
385/// and surface inbound streams, until the command channel closes (session dropped) or the connection
386/// errors. This is the task that replaces yamux 0.12's `Control`.
387///
388/// `T` is the futures-io view of the byte stream (a `tokio-util` [`Compat`] of the tokio mTLS
389/// stream), since yamux operates on `futures::AsyncRead + AsyncWrite`.
390async fn drive_connection<T>(
391    mut conn: yamux::Connection<T>,
392    mut cmd_rx: tokio::sync::mpsc::Receiver<MuxCommand>,
393    inbound_tx: tokio::sync::mpsc::Sender<PeerStream>,
394) where
395    T: futures::AsyncRead + futures::AsyncWrite + Send + Unpin + 'static,
396{
397    use std::future::poll_fn;
398
399    loop {
400        tokio::select! {
401            // An open-outbound request from the session.
402            cmd = cmd_rx.recv() => {
403                match cmd {
404                    Some(MuxCommand::OpenOutbound(reply)) => {
405                        let res = poll_fn(|cx| conn.poll_new_outbound(cx)).await;
406                        let _ = reply.send(res.map_err(|e| e.to_string()));
407                    }
408                    None => {
409                        // Session dropped — close the connection and end the driver.
410                        let _ = poll_fn(|cx| conn.poll_close(cx)).await;
411                        return;
412                    }
413                }
414            }
415            // An inbound stream opened by the peer.
416            inbound = poll_fn(|cx| conn.poll_next_inbound(cx)) => {
417                match inbound {
418                    Some(Ok(stream)) => {
419                        // Deliver to a serving node; if no one is accepting, the stream is dropped.
420                        let _ = inbound_tx.try_send(stream.compat());
421                    }
422                    Some(Err(_)) | None => {
423                        // Connection closed / errored — end the driver.
424                        return;
425                    }
426                }
427            }
428        }
429    }
430}