Skip to main content

decdn_protocol/
framing.rs

1//! Length-prefixed wire framing for deCDN ALPNs (ADR 013).
2//!
3//! Every message on every QUIC stream is `varint(len) || postcard_bytes[0..len]`.
4//! The varint uses postcard's native continuation-bit encoding (7 data bits per
5//! byte, MSB = continuation, little-endian, 1–5 bytes for `u32`).
6//!
7//! Receivers read the varint length, reject anything above [`MAX_MESSAGE_SIZE`]
8//! before allocation, allocate exactly that many bytes, then deserialize the
9//! frame with [`decode_message`] (a thin wrapper over
10//! [`postcard::take_from_bytes`] that preserves the trailing remainder for
11//! future Tier-1 extension fields).
12
13use serde::Serialize;
14use serde::de::DeserializeOwned;
15use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
16
17/// Maximum framed message size. Pinned at exactly 16 MiB by ADR 013 §Wire
18/// Framing, uniformly across all ALPNs in v1.
19///
20/// The ceiling is a pre-allocation denial-of-service bound: [`read_frame`]
21/// allocates exactly `len` bytes after decoding the length prefix, so an
22/// unbounded length would let a peer force arbitrary-sized allocations. 16 MiB
23/// sits well above every documented ALPN message (`cdn/probe/v1` ≤ ~200 B,
24/// `cdn/client/v1` non-`ChunkData` ≤ ~1 KiB) and pairs with QUIC's
25/// `MAX_STREAMS` (ADR 005) for the total per-peer memory bound.
26///
27/// Changing this constant is a protocol-wide wire-compatibility decision —
28/// any edit, whether bump or shrink, requires an ADR 013 amendment first.
29/// The compile-time guard below catches accidental edits. Per-deployment
30/// tightening for memory-constrained nodes is a runtime-config concern
31/// (ADR 013 §Wire Framing), not a change to the constant itself.
32pub const MAX_MESSAGE_SIZE: u32 = 16 * 1024 * 1024;
33
34// Compile-time guardrail tying MAX_MESSAGE_SIZE to ADR 013. Any change to
35// the constant without an ADR 013 amendment fails the build.
36const _: () = assert!(
37    MAX_MESSAGE_SIZE == 16 * 1024 * 1024,
38    "MAX_MESSAGE_SIZE must be exactly 16 MiB per ADR 013 §Wire Framing; any change (bump or shrink) requires an ADR amendment",
39);
40
41/// Errors produced by the framing helpers. The handler-layer mapping to QUIC
42/// application error codes lives in the node crate.
43#[derive(Debug, thiserror::Error)]
44pub enum FrameError {
45    /// The underlying stream failed while reading or writing the frame.
46    #[error("frame I/O error: {0}")]
47    Io(#[from] std::io::Error),
48    /// The length prefix names more bytes than [`MAX_MESSAGE_SIZE`] allows.
49    /// Rejected before anything is buffered.
50    #[error("frame length {0} exceeds MAX_MESSAGE_SIZE ({MAX_MESSAGE_SIZE})")]
51    TooLarge(u32),
52    /// The length prefix is not a well-formed varint — a non-terminating
53    /// continuation run through all five bytes, or a fifth byte overflowing
54    /// `u32`. A short or empty buffer reads as truncated, not malformed.
55    #[error("malformed varint length prefix")]
56    Varint,
57    /// The frame's bytes are not a valid postcard encoding of the expected
58    /// message.
59    #[error("postcard decode error: {0}")]
60    Decode(#[from] postcard::Error),
61}
62
63/// Serialize `msg` to a postcard buffer suitable for passing to [`write_frame`].
64pub fn encode_message<M: Serialize>(msg: &M) -> Result<Vec<u8>, FrameError> {
65    Ok(postcard::to_allocvec(msg)?)
66}
67
68/// Decode a frame into a message and the unconsumed remainder. The remainder
69/// is reserved for Tier-1 extension fields (ADR 013 §Tier 1).
70pub fn decode_message<M: DeserializeOwned>(frame: &[u8]) -> Result<(M, &[u8]), FrameError> {
71    Ok(postcard::take_from_bytes::<M>(frame)?)
72}
73
74/// A top-level ALPN protocol enum (one per ALPN — [`crate::ProbeMessage`],
75/// [`crate::ClientMessage`], [`crate::DhtMessage`]). Serialized as the
76/// outermost postcard value in a frame, so its variant discriminant is the
77/// frame's leading varint.
78///
79/// Implementors expose their declared-variant count so the framing layer can
80/// tell an *unknown* discriminant (a variant this build does not know —
81/// ADR 013 `UNSUPPORTED_MESSAGE`, `0x01`) apart from a genuine parse fault
82/// (`MALFORMED_MESSAGE`, `0x03`) after a failed [`decode_message`].
83pub trait TopLevelEnum {
84    /// Number of declared variants. Postcard assigns discriminants
85    /// `0..VARIANT_COUNT` in declaration order, so any leading discriminant
86    /// `>= VARIANT_COUNT` names a variant this build does not know.
87    const VARIANT_COUNT: u32;
88}
89
90/// Classify a failed top-level-enum decode per ADR 013: return `true` iff the
91/// frame's leading discriminant names a variant at or beyond
92/// `M::VARIANT_COUNT` — i.e. an unknown/unsupported variant the receiver must
93/// answer with `UNSUPPORTED_MESSAGE` (`0x01`) rather than `MALFORMED_MESSAGE`
94/// (`0x03`).
95///
96/// A frame that does not begin with a well-formed varint (empty, a
97/// non-terminating continuation run, or a 5th byte that overflows `u32`) is a
98/// genuine parse fault, so this returns `false` and the caller keeps the
99/// `MALFORMED` classification. Callers should only consult this after
100/// [`decode_message`] has actually failed; a valid in-range discriminant whose
101/// inner payload is malformed also stays `MALFORMED` (this returns `false`).
102pub fn is_unknown_variant<M: TopLevelEnum>(frame: &[u8]) -> bool {
103    matches!(leading_varint_u32(frame), Some(d) if d >= M::VARIANT_COUNT)
104}
105
106/// Decode the leading postcard `u32` varint from `frame` (the top-level enum
107/// discriminant) without consuming the remainder. Returns `None` when the
108/// bytes do not begin with a well-formed varint — the synchronous mirror of
109/// [`read_varint_u32`]'s validation, over an in-memory slice.
110fn leading_varint_u32(frame: &[u8]) -> Option<u32> {
111    let mut result: u32 = 0;
112    for (i, &byte) in frame.iter().take(5).enumerate() {
113        let data = u32::from(byte & 0x7F);
114        // On the 5th byte (i == 4) only the low 4 bits are valid; reject overflow.
115        if i == 4 && byte & 0x70 != 0 {
116            return None;
117        }
118        let shift = u32::try_from(i).ok()?.saturating_mul(7);
119        let shifted = data.checked_shl(shift)?;
120        result |= shifted;
121        if byte & 0x80 == 0 {
122            return Some(result);
123        }
124    }
125    None
126}
127
128/// Try to locate one complete length-prefixed frame at the front of `buf`
129/// without consuming it, returning `(header_len, payload_len)` — the byte
130/// counts of the varint prefix and the payload it announces. Returns `Ok(None)`
131/// when `buf` does not yet hold a complete frame (the varint is truncated, or
132/// fewer than `payload_len` payload bytes have arrived).
133///
134/// This is the synchronous, non-consuming counterpart to [`read_frame`], for a
135/// caller that fills a byte buffer incrementally with **cancellation-safe**
136/// reads and must split frames off it after each top-up — e.g. the per-voucher
137/// reader's read-ahead (#1486), which reads ahead under a timeout that
138/// [`read_frame`] (built on `read_exact`) could not survive without losing
139/// partial bytes. The
140/// caller slices the payload as `buf[header_len..header_len + payload_len]`,
141/// decodes it with [`decode_message`], then drains `header_len + payload_len`
142/// bytes.
143///
144/// The announced length is validated against [`MAX_MESSAGE_SIZE`] as soon as the
145/// varint is complete — before the caller waits for (or allocates) the payload —
146/// mirroring [`read_frame`]'s pre-allocation denial-of-service bound.
147///
148/// # Errors
149///
150/// [`FrameError::Varint`] for a malformed length prefix (a non-terminating
151/// continuation run within the first 5 bytes, or a 5th byte overflowing `u32`);
152/// [`FrameError::TooLarge`] when the announced length exceeds
153/// [`MAX_MESSAGE_SIZE`].
154pub fn parse_frame(buf: &[u8]) -> Result<Option<(usize, usize)>, FrameError> {
155    let Some((len, header_len)) = decode_varint_prefix(buf)? else {
156        return Ok(None);
157    };
158    if len > MAX_MESSAGE_SIZE {
159        return Err(FrameError::TooLarge(len));
160    }
161    let payload_len = len as usize;
162    if buf.len().saturating_sub(header_len) < payload_len {
163        return Ok(None);
164    }
165    Ok(Some((header_len, payload_len)))
166}
167
168/// Decode a postcard `u32` varint from the front of `buf`, returning
169/// `(value, bytes_consumed)`, `Ok(None)` when the varint is truncated (fewer
170/// than its continuation bytes have arrived), or [`FrameError::Varint`] when the
171/// bytes present are already malformed. The synchronous mirror of
172/// [`read_varint_u32`].
173fn decode_varint_prefix(buf: &[u8]) -> Result<Option<(u32, usize)>, FrameError> {
174    let mut result: u32 = 0;
175    for (i, &byte) in buf.iter().take(5).enumerate() {
176        let data = u32::from(byte & 0x7F);
177        // On the 5th byte (i == 4) only the low 4 bits are valid; reject overflow.
178        if i == 4 && byte & 0x70 != 0 {
179            return Err(FrameError::Varint);
180        }
181        let shift = u32::try_from(i)
182            .map_err(|_| FrameError::Varint)?
183            .saturating_mul(7);
184        let shifted = data.checked_shl(shift).ok_or(FrameError::Varint)?;
185        result |= shifted;
186        if byte & 0x80 == 0 {
187            return Ok(Some((result, i + 1)));
188        }
189    }
190    // Ran out of buffer mid-varint (< 5 bytes, all continuations) → truncated;
191    // exactly 5 continuation bytes with the terminator missing is malformed.
192    if buf.len() >= 5 {
193        return Err(FrameError::Varint);
194    }
195    Ok(None)
196}
197
198/// Read one length-prefixed frame from an async reader.
199///
200/// The length prefix is validated against [`MAX_MESSAGE_SIZE`] *before* any
201/// payload allocation, so a malicious peer cannot trigger a large allocation
202/// by sending a single oversized varint.
203pub async fn read_frame<R: AsyncRead + Unpin>(r: &mut R) -> Result<Vec<u8>, FrameError> {
204    let len = read_varint_u32(r).await?;
205    if len > MAX_MESSAGE_SIZE {
206        return Err(FrameError::TooLarge(len));
207    }
208    let mut buf = vec![0u8; len as usize];
209    r.read_exact(&mut buf).await?;
210    Ok(buf)
211}
212
213/// Write one length-prefixed frame to an async writer.
214pub async fn write_frame<W: AsyncWrite + Unpin>(
215    w: &mut W,
216    payload: &[u8],
217) -> Result<(), FrameError> {
218    let len = u32::try_from(payload.len()).map_err(|_| FrameError::TooLarge(u32::MAX))?;
219    if len > MAX_MESSAGE_SIZE {
220        return Err(FrameError::TooLarge(len));
221    }
222    write_varint_u32(w, len).await?;
223    w.write_all(payload).await?;
224    Ok(())
225}
226
227async fn read_varint_u32<R: AsyncRead + Unpin>(r: &mut R) -> Result<u32, FrameError> {
228    let mut result: u32 = 0;
229    for i in 0u32..5 {
230        let mut b = [0u8; 1];
231        r.read_exact(&mut b).await?;
232        let byte = b[0];
233        let data = u32::from(byte & 0x7F);
234        let shift = i.saturating_mul(7);
235        // On the 5th byte (i=4) only the low 4 bits are valid; reject overflow.
236        if i == 4 && byte & 0x70 != 0 {
237            return Err(FrameError::Varint);
238        }
239        let shifted = data.checked_shl(shift).ok_or(FrameError::Varint)?;
240        result |= shifted;
241        if byte & 0x80 == 0 {
242            return Ok(result);
243        }
244    }
245    Err(FrameError::Varint)
246}
247
248/// Encode `value` as a postcard varint into the caller's stack buffer, returning
249/// the number of bytes written (1..=5).
250///
251/// A `u32` encodes to at most 5 varint bytes (`ceil(32/7) = 5`), which is why the
252/// buffer is a fixed `[u8; 5]` and why the count returned always fits it.
253///
254/// The `out.get_mut(idx)` guards exist to satisfy the `indexing_slicing` clippy lint.
255/// Each one stops the encode rather than dropping a byte and counting it anyway: the
256/// returned length is bytes *written*, never bytes intended, so no caller can be
257/// handed a count that outruns the bytes behind it.
258#[must_use]
259pub(crate) fn encode_varint_u32(mut value: u32, out: &mut [u8; 5]) -> usize {
260    let mut idx = 0usize;
261    loop {
262        let byte = (value & 0x7F) as u8;
263        value >>= 7;
264        let last = value == 0;
265        let Some(slot) = out.get_mut(idx) else {
266            break;
267        };
268        *slot = if last { byte } else { byte | 0x80 };
269        idx = idx.saturating_add(1);
270        if last {
271            break;
272        }
273    }
274    debug_assert!(idx <= 5, "u32 varint must fit in 5 bytes, idx={idx}");
275    idx
276}
277
278async fn write_varint_u32<W: AsyncWrite + Unpin>(w: &mut W, value: u32) -> std::io::Result<()> {
279    let mut buf = [0u8; 5];
280    let idx = encode_varint_u32(value, &mut buf);
281    // A length prefix that silently shortens to nothing is worse than a failed write:
282    // `write_frame` would put the payload on the wire behind it, the peer would read
283    // the payload's first byte as the length, and the stream — and the cumulative
284    // wire-byte count vouchers are paid against — would desync with no error on
285    // either side. Unreachable while `buf` is `[u8; 5]`; refuse rather than rely on it.
286    let Some(bytes) = buf.get(..idx) else {
287        return Err(std::io::Error::other(format!(
288            "varint length prefix wants {idx} bytes but only {} were encoded",
289            buf.len()
290        )));
291    };
292    w.write_all(bytes).await
293}
294
295#[cfg(test)]
296#[allow(
297    clippy::unwrap_used,
298    clippy::expect_used,
299    clippy::panic,
300    clippy::indexing_slicing
301)]
302mod proptests;
303
304#[cfg(test)]
305mod tests;