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: ¶ms,
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}