Skip to main content

irtt_proto/
request.rs

1//! Directional request codecs: one sender-side encoder and one receiver-side
2//! structural decoder.
3//!
4//! The two directions are deliberately asymmetric. Encoding an ECHO request
5//! needs the negotiated [`Params`] because the sender constructs a
6//! negotiated-length datagram; decoding one does not, because a receiver reads
7//! only the token and sequence number and treats the rest as opaque.
8
9use crate::{
10    envelope,
11    flags::{has, FLAG_CLOSE, FLAG_OPEN, FLAG_REPLY},
12    layout::{echo_packet_len, PacketLayout},
13    params::Params,
14    ProtoError, Result, SEQ_SIZE, TOKEN_SIZE,
15};
16
17/// A request to construct, described in sender-side semantic terms.
18///
19/// Every variant borrows what it needs; the encoder owns nothing.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum RequestToEncode<'a> {
22    /// Session-opening request. `no_test` selects the *no-test* form, which
23    /// adds `FLAG_CLOSE` and asks the peer to answer without creating a
24    /// session.
25    Open { params: &'a Params, no_test: bool },
26    /// Session teardown request.
27    Close { token: u64 },
28    /// Round-trip probe. `params` supplies the negotiated packet layout and
29    /// length, and `payload` is placed at the negotiated payload offset.
30    Echo {
31        token: u64,
32        sequence: u32,
33        params: &'a Params,
34        payload: &'a [u8],
35    },
36}
37
38/// A structurally decoded inbound request, borrowed from the packet.
39///
40/// `hmac_present` reports only that `FLAG_HMAC` was set and that the packet is
41/// laid out with an authentication field. It is **not** an authentication
42/// result: the field may hold arbitrary bytes. Use [`verify_packet_hmac`] to
43/// check the MAC once the applicable key is known.
44///
45/// [`verify_packet_hmac`]: crate::verify_packet_hmac
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct DecodedRequest<'a> {
48    pub hmac_present: bool,
49    pub kind: DecodedRequestKind<'a>,
50}
51
52/// The request kinds a receiver can distinguish from the packet alone.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum DecodedRequestKind<'a> {
55    /// `FLAG_OPEN` was set. `params` is the still-encoded parameter payload;
56    /// see [`decode_request`] for why it is not decoded here.
57    Open { no_test: bool, params: &'a [u8] },
58    /// `FLAG_CLOSE` was set without `FLAG_OPEN`. Trailing bytes after the token
59    /// are accepted and not exposed, because they carry no meaning.
60    Close { token: u64 },
61    /// Neither `FLAG_OPEN` nor `FLAG_CLOSE` was set. `tail` is everything after
62    /// the sequence number, which is opaque to a receiver.
63    Echo {
64        token: u64,
65        sequence: u32,
66        tail: &'a [u8],
67    },
68}
69
70/// Encodes an outbound request.
71///
72/// This is sender-side semantic construction: the variant determines the flags,
73/// so callers never assemble flag bits themselves.
74///
75/// `hmac_key` is authoritative. `Some(key)` sets `FLAG_HMAC`, reserves the
76/// 16-byte authentication field after the header, and signs the finished
77/// datagram; `None` produces an unauthenticated packet.
78///
79/// [`RequestToEncode::Echo`] requires the negotiated [`Params`] because the
80/// sender must build a datagram of the negotiated length with the payload at
81/// the negotiated offset. The receiving side needs no `Params` — see
82/// [`decode_request`].
83///
84/// # Errors
85///
86/// Returns [`ProtoError::PayloadTooLarge`] when an ECHO payload does not fit
87/// the negotiated length, and [`ProtoError::PacketLengthUnrepresentable`] when
88/// the negotiated length is positive and wider than `usize`. A *negative*
89/// negotiated length is not an error: see [`echo_packet_len`], which floors it
90/// at the mandatory field block.
91///
92/// # Example
93///
94/// ```
95/// use irtt_proto::{encode_request, Params, RequestToEncode};
96///
97/// // An unauthenticated ECHO probe. `params` supplies the negotiated length,
98/// // which reserves four payload bytes beyond the 16-byte echo header.
99/// let params = Params { length: 20, ..Params::default() };
100/// let packet = encode_request(
101///     RequestToEncode::Echo {
102///         token: 0x1234_5678_9abc_def0,
103///         sequence: 7,
104///         params: &params,
105///         payload: &[1, 2, 3, 4],
106///     },
107///     None,
108/// ).unwrap();
109/// assert_eq!(packet.len(), 20);
110/// ```
111pub fn encode_request(request: RequestToEncode<'_>, hmac_key: Option<&[u8]>) -> Result<Vec<u8>> {
112    match request {
113        RequestToEncode::Open { params, no_test } => encode_open(params, no_test, hmac_key),
114        RequestToEncode::Close { token } => encode_close(token, hmac_key),
115        RequestToEncode::Echo {
116            token,
117            sequence,
118            params,
119            payload,
120        } => encode_echo(token, sequence, params, payload, hmac_key),
121    }
122}
123
124/// Structurally decodes an inbound request.
125///
126/// This is packet structure only — it is **not** admission control. It performs
127/// no token or session lookup, applies no server policy, and needs no HMAC key,
128/// precisely so that a receiver can extract a token *before* it knows which
129/// session and key apply.
130///
131/// Classification, for a packet with valid magic and no reserved flag bits set:
132///
133/// - `FLAG_REPLY` set — rejected; a reply is never an inbound request.
134/// - `FLAG_OPEN` set — [`DecodedRequestKind::Open`], with `no_test` from
135///   `FLAG_CLOSE`. **Open takes precedence over Close and over an echo-shaped
136///   body**: an `OPEN | CLOSE` packet is a no-test open, and bytes that look
137///   like a token and sequence number are parameter data.
138/// - otherwise `FLAG_CLOSE` set — [`DecodedRequestKind::Close`].
139/// - otherwise — [`DecodedRequestKind::Echo`].
140///
141/// `FLAG_HMAC` is orthogonal to all of the above and is reported as
142/// [`DecodedRequest::hmac_present`], which indicates field presence rather than
143/// valid authentication.
144///
145/// Open parameters are returned as encoded bytes rather than a decoded
146/// [`Params`], so that an authenticating receiver can verify a packet before
147/// spending effort on untrusted parameter data. A packet with malformed
148/// parameters decodes structurally and fails later at [`Params::decode`].
149///
150/// Only structural minimum lengths are enforced — 4/12/16 bytes for
151/// Open/Close/Echo, plus 16 with `FLAG_HMAC`. Trailing bytes beyond a kind's
152/// fields are tolerated, no negotiated length ceiling is applied, and a zero
153/// token is accepted; discarding stale, unknown, or oversized requests is the
154/// receiving layer's policy.
155///
156/// # Errors
157///
158/// Returns [`ProtoError::PacketTooShort`], [`ProtoError::BadMagic`],
159/// [`ProtoError::ReservedFlags`], or [`ProtoError::UnexpectedFlag`] for
160/// `FLAG_REPLY`.
161///
162/// # Example
163///
164/// ```
165/// use irtt_proto::{decode_request, encode_request, DecodedRequestKind, RequestToEncode};
166///
167/// let packet = encode_request(
168///     RequestToEncode::Close { token: 0x1234_5678_9abc_def0 },
169///     None,
170/// ).unwrap();
171///
172/// let request = decode_request(&packet).unwrap();
173/// assert!(!request.hmac_present);
174/// assert_eq!(request.kind, DecodedRequestKind::Close { token: 0x1234_5678_9abc_def0 });
175/// ```
176pub fn decode_request(packet: &[u8]) -> Result<DecodedRequest<'_>> {
177    let envelope = envelope::decode_structural(packet)?;
178    if has(envelope.flags, FLAG_REPLY) {
179        return Err(ProtoError::UnexpectedFlag(FLAG_REPLY));
180    }
181
182    let body = envelope.body_offset;
183    let kind = if has(envelope.flags, FLAG_OPEN) {
184        // Open wins over Close and over an echo-shaped body.
185        require_len(packet, body)?;
186        DecodedRequestKind::Open {
187            no_test: has(envelope.flags, FLAG_CLOSE),
188            params: &packet[body..],
189        }
190    } else if has(envelope.flags, FLAG_CLOSE) {
191        require_len(packet, body + TOKEN_SIZE)?;
192        DecodedRequestKind::Close {
193            token: read_token(packet, body),
194        }
195    } else {
196        let tail_offset = body + TOKEN_SIZE + SEQ_SIZE;
197        require_len(packet, tail_offset)?;
198        DecodedRequestKind::Echo {
199            token: read_token(packet, body),
200            sequence: read_sequence(packet, body + TOKEN_SIZE),
201            tail: &packet[tail_offset..],
202        }
203    };
204
205    Ok(DecodedRequest {
206        hmac_present: envelope.hmac_present,
207        kind,
208    })
209}
210
211fn encode_open(params: &Params, no_test: bool, hmac_key: Option<&[u8]>) -> Result<Vec<u8>> {
212    let flags = if no_test {
213        FLAG_OPEN | FLAG_CLOSE
214    } else {
215        FLAG_OPEN
216    };
217    let encoded = params.encode();
218    let mut out = envelope::begin(
219        flags,
220        hmac_key,
221        PacketLayout::open_request(hmac_key.is_some()).header_len() + encoded.len(),
222    )?;
223    out.extend_from_slice(&encoded);
224    envelope::finish(out, hmac_key)
225}
226
227fn encode_close(token: u64, hmac_key: Option<&[u8]>) -> Result<Vec<u8>> {
228    let mut out = envelope::begin(
229        FLAG_CLOSE,
230        hmac_key,
231        PacketLayout::close_request(hmac_key.is_some()).header_len(),
232    )?;
233    out.extend_from_slice(&token.to_le_bytes());
234    envelope::finish(out, hmac_key)
235}
236
237fn encode_echo(
238    token: u64,
239    sequence: u32,
240    params: &Params,
241    payload: &[u8],
242    hmac_key: Option<&[u8]>,
243) -> Result<Vec<u8>> {
244    let layout = PacketLayout::echo(hmac_key.is_some(), params);
245    let len = echo_packet_len(hmac_key.is_some(), params)?;
246    let payload_offset = layout.header_len();
247    let available_payload_len = len.saturating_sub(payload_offset);
248    if payload.len() > available_payload_len {
249        return Err(ProtoError::PayloadTooLarge {
250            available: available_payload_len,
251            provided: payload.len(),
252        });
253    }
254
255    let mut out = envelope::begin(0, hmac_key, len)?;
256    out.extend_from_slice(&token.to_le_bytes());
257    out.extend_from_slice(&sequence.to_le_bytes());
258    // Everything from here to the negotiated length is zeroed: the reply-field
259    // positions the sender does not fill in, then the payload region.
260    out.resize(len, 0);
261    out[payload_offset..payload_offset + payload.len()].copy_from_slice(payload);
262
263    envelope::finish(out, hmac_key)
264}
265
266fn require_len(packet: &[u8], needed: usize) -> Result<()> {
267    if packet.len() < needed {
268        return Err(ProtoError::PacketTooShort {
269            needed,
270            actual: packet.len(),
271        });
272    }
273    Ok(())
274}
275
276fn read_token(packet: &[u8], offset: usize) -> u64 {
277    u64::from_le_bytes(packet[offset..offset + TOKEN_SIZE].try_into().unwrap())
278}
279
280fn read_sequence(packet: &[u8], offset: usize) -> u32 {
281    u32::from_le_bytes(packet[offset..offset + SEQ_SIZE].try_into().unwrap())
282}