Skip to main content

irtt_proto/
layout.rs

1use crate::{
2    params::{Params, StampAt},
3    ProtoError, Result, HEADER_SIZE, HMAC_SIZE, RECV_COUNT_SIZE, RECV_WINDOW_SIZE, SEQ_SIZE,
4    TIMESTAMP_SIZE, TOKEN_SIZE,
5};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct PacketLayout {
9    pub hmac: bool,
10    pub token: bool,
11    pub sequence: bool,
12    pub recv_count: bool,
13    pub recv_window: bool,
14    pub recv_wall: bool,
15    pub recv_mono: bool,
16    pub midpoint_wall: bool,
17    pub midpoint_mono: bool,
18    pub send_wall: bool,
19    pub send_mono: bool,
20}
21
22impl PacketLayout {
23    pub fn open_request(hmac: bool) -> Self {
24        Self {
25            hmac,
26            token: false,
27            sequence: false,
28            recv_count: false,
29            recv_window: false,
30            recv_wall: false,
31            recv_mono: false,
32            midpoint_wall: false,
33            midpoint_mono: false,
34            send_wall: false,
35            send_mono: false,
36        }
37    }
38
39    pub fn open_reply(hmac: bool) -> Self {
40        Self {
41            token: true,
42            ..Self::open_request(hmac)
43        }
44    }
45
46    pub fn echo(hmac: bool, params: &Params) -> Self {
47        let clock = params.clock;
48        Self {
49            hmac,
50            token: true,
51            sequence: true,
52            recv_count: params.received_stats.has_count(),
53            recv_window: params.received_stats.has_window(),
54            recv_wall: matches!(params.stamp_at, StampAt::Receive | StampAt::Both)
55                && clock.has_wall(),
56            recv_mono: matches!(params.stamp_at, StampAt::Receive | StampAt::Both)
57                && clock.has_mono(),
58            midpoint_wall: matches!(params.stamp_at, StampAt::Midpoint) && clock.has_wall(),
59            midpoint_mono: matches!(params.stamp_at, StampAt::Midpoint) && clock.has_mono(),
60            send_wall: matches!(params.stamp_at, StampAt::Send | StampAt::Both) && clock.has_wall(),
61            send_mono: matches!(params.stamp_at, StampAt::Send | StampAt::Both) && clock.has_mono(),
62        }
63    }
64
65    pub fn close_request(hmac: bool) -> Self {
66        Self {
67            hmac,
68            token: true,
69            sequence: false,
70            recv_count: false,
71            recv_window: false,
72            recv_wall: false,
73            recv_mono: false,
74            midpoint_wall: false,
75            midpoint_mono: false,
76            send_wall: false,
77            send_mono: false,
78        }
79    }
80
81    pub fn header_len(self) -> usize {
82        HEADER_SIZE
83            + if self.hmac { HMAC_SIZE } else { 0 }
84            + if self.token { TOKEN_SIZE } else { 0 }
85            + if self.sequence { SEQ_SIZE } else { 0 }
86            + if self.recv_count { RECV_COUNT_SIZE } else { 0 }
87            + if self.recv_window {
88                RECV_WINDOW_SIZE
89            } else {
90                0
91            }
92            + self.timestamp_count() * TIMESTAMP_SIZE
93    }
94
95    pub fn timestamp_count(self) -> usize {
96        [
97            self.recv_wall,
98            self.recv_mono,
99            self.midpoint_wall,
100            self.midpoint_mono,
101            self.send_wall,
102            self.send_mono,
103        ]
104        .into_iter()
105        .filter(|present| *present)
106        .count()
107    }
108}
109
110pub fn echo_header_len(hmac: bool, params: &Params) -> usize {
111    PacketLayout::echo(hmac, params).header_len()
112}
113
114/// The datagram length of an ECHO request or reply under `params`.
115///
116/// The negotiated length is a floor a peer asks for, never a ceiling: a packet
117/// can never be shorter than the field block its negotiated layout requires, so
118/// the result is `max(header, requested)`.
119///
120/// A negative negotiated length is a legitimate input rather than an encoder
121/// error. It is accepted during open and echoed back unchanged, so a session
122/// can genuinely carry one, and there is no datagram shorter than none — it
123/// therefore requests no space beyond the mandatory field block, exactly as
124/// zero does.
125///
126/// # Errors
127///
128/// Returns [`ProtoError::PacketLengthUnrepresentable`] for a positive length
129/// this platform's `usize` cannot hold. That is a representability check, not a
130/// size policy: a wire value that cannot even name a local buffer must not be
131/// converted into one that can, because the result is handed to an allocator.
132/// It is unreachable on a 64-bit target, where every positive `i64` converts,
133/// and is the reason this returns a [`Result`] at all.
134///
135/// A ceiling on what a negotiated length may legitimately *be* — an MTU, a
136/// resource bound, a maximum packet size — is deliberately not here. That is
137/// server and runtime policy, and it belongs where the negotiation happens.
138///
139/// # Example
140///
141/// ```
142/// use irtt_proto::{echo_packet_len, Params};
143///
144/// // The default layout's mandatory field block is 16 bytes (header, token,
145/// // sequence), so a smaller requested length is floored up to it.
146/// assert_eq!(echo_packet_len(false, &Params::default()).unwrap(), 16);
147/// assert_eq!(
148///     echo_packet_len(false, &Params { length: 40, ..Params::default() }).unwrap(),
149///     40,
150/// );
151/// ```
152pub fn echo_packet_len(hmac: bool, params: &Params) -> Result<usize> {
153    let header_len = echo_header_len(hmac, params);
154    let requested = if params.length <= 0 {
155        // No datagram is shorter than none, so a negative length asks for
156        // nothing beyond the mandatory field block, exactly as zero does.
157        0
158    } else {
159        usize::try_from(params.length).map_err(|_| ProtoError::PacketLengthUnrepresentable {
160            length: params.length,
161        })?
162    };
163    Ok(header_len.max(requested))
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::params::{Clock, ReceivedStats, StampAt};
170
171    fn params(stats: ReceivedStats, stamp_at: StampAt, clock: Clock) -> Params {
172        Params {
173            received_stats: stats,
174            stamp_at,
175            clock,
176            ..Params::default()
177        }
178    }
179
180    fn expected_optional_len(stats: ReceivedStats, stamp_at: StampAt, clock: Clock) -> usize {
181        let stats_len = match stats {
182            ReceivedStats::None => 0,
183            ReceivedStats::Count => RECV_COUNT_SIZE,
184            ReceivedStats::Window => RECV_WINDOW_SIZE,
185            ReceivedStats::Both => RECV_COUNT_SIZE + RECV_WINDOW_SIZE,
186        };
187        let clock_count = match clock {
188            Clock::Unspecified => 0,
189            Clock::Wall | Clock::Monotonic => 1,
190            Clock::Both => 2,
191        };
192        let timestamp_groups = match stamp_at {
193            StampAt::None => 0,
194            StampAt::Send | StampAt::Receive | StampAt::Midpoint => 1,
195            StampAt::Both => 2,
196        };
197        stats_len + timestamp_groups * clock_count * TIMESTAMP_SIZE
198    }
199
200    #[test]
201    fn verified_layout_lengths() {
202        assert_eq!(PacketLayout::open_request(false).header_len(), 4);
203        assert_eq!(PacketLayout::open_request(true).header_len(), 20);
204        assert_eq!(PacketLayout::open_reply(false).header_len(), 12);
205        assert_eq!(PacketLayout::open_reply(true).header_len(), 28);
206        assert_eq!(PacketLayout::close_request(false).header_len(), 12);
207        assert_eq!(PacketLayout::close_request(true).header_len(), 28);
208        assert_eq!(
209            echo_header_len(
210                false,
211                &params(ReceivedStats::None, StampAt::None, Clock::Both)
212            ),
213            16
214        );
215        assert_eq!(
216            echo_header_len(
217                false,
218                &params(ReceivedStats::Count, StampAt::Send, Clock::Wall)
219            ),
220            28
221        );
222        assert_eq!(
223            echo_header_len(
224                false,
225                &params(ReceivedStats::Window, StampAt::Receive, Clock::Monotonic)
226            ),
227            32
228        );
229        assert_eq!(
230            echo_header_len(
231                false,
232                &params(ReceivedStats::Both, StampAt::Midpoint, Clock::Both)
233            ),
234            44
235        );
236        assert_eq!(
237            echo_header_len(
238                false,
239                &params(ReceivedStats::Both, StampAt::Both, Clock::Both)
240            ),
241            60
242        );
243        assert_eq!(
244            echo_header_len(
245                true,
246                &params(ReceivedStats::Both, StampAt::Both, Clock::Both)
247            ),
248            76
249        );
250    }
251
252    #[test]
253    fn layout_matrix_matches_stats_timestamps_clock_and_hmac_rules() {
254        for stats in [
255            ReceivedStats::None,
256            ReceivedStats::Count,
257            ReceivedStats::Window,
258            ReceivedStats::Both,
259        ] {
260            for stamp_at in [
261                StampAt::None,
262                StampAt::Send,
263                StampAt::Receive,
264                StampAt::Both,
265                StampAt::Midpoint,
266            ] {
267                for clock in [Clock::Wall, Clock::Monotonic, Clock::Both] {
268                    for hmac in [false, true] {
269                        let params = params(stats, stamp_at, clock);
270                        let layout = PacketLayout::echo(hmac, &params);
271                        let expected_len = HEADER_SIZE
272                            + TOKEN_SIZE
273                            + SEQ_SIZE
274                            + if hmac { HMAC_SIZE } else { 0 }
275                            + expected_optional_len(stats, stamp_at, clock);
276
277                        assert_eq!(
278                            layout.header_len(),
279                            expected_len,
280                            "unexpected length for stats={stats:?} stamp_at={stamp_at:?} clock={clock:?} hmac={hmac}"
281                        );
282                        assert_eq!(layout.recv_count, stats.has_count());
283                        assert_eq!(layout.recv_window, stats.has_window());
284                        assert_eq!(
285                            layout.recv_wall,
286                            matches!(stamp_at, StampAt::Receive | StampAt::Both)
287                                && clock.has_wall()
288                        );
289                        assert_eq!(
290                            layout.recv_mono,
291                            matches!(stamp_at, StampAt::Receive | StampAt::Both)
292                                && clock.has_mono()
293                        );
294                        assert_eq!(
295                            layout.midpoint_wall,
296                            matches!(stamp_at, StampAt::Midpoint) && clock.has_wall()
297                        );
298                        assert_eq!(
299                            layout.midpoint_mono,
300                            matches!(stamp_at, StampAt::Midpoint) && clock.has_mono()
301                        );
302                        assert_eq!(
303                            layout.send_wall,
304                            matches!(stamp_at, StampAt::Send | StampAt::Both) && clock.has_wall()
305                        );
306                        assert_eq!(
307                            layout.send_mono,
308                            matches!(stamp_at, StampAt::Send | StampAt::Both) && clock.has_mono()
309                        );
310
311                        if hmac {
312                            assert_eq!(
313                                echo_header_len(true, &params) - echo_header_len(false, &params),
314                                HMAC_SIZE
315                            );
316                        }
317                    }
318                }
319            }
320        }
321    }
322
323    #[test]
324    fn negotiated_length_is_floored_at_the_required_field_block() {
325        // The 60-byte header of this layout is the floor. A negative negotiated
326        // length is not an error: it asks for nothing beyond the mandatory
327        // fields, exactly as zero does.
328        let mut p = params(ReceivedStats::Both, StampAt::Both, Clock::Both);
329        for (length, expected) in [(-4096, 60), (-1, 60), (0, 60), (20, 60), (92, 92)] {
330            p.length = length;
331            assert_eq!(
332                echo_packet_len(false, &p),
333                Ok(expected),
334                "unexpected packet length for negotiated length {length}"
335            );
336        }
337    }
338
339    /// A positive length wider than `usize` must stay an error rather than
340    /// becoming a saturated buffer size. Only reachable where `usize` is
341    /// narrower than `i64`; on a 64-bit target every positive `i64` converts.
342    #[cfg(target_pointer_width = "32")]
343    #[test]
344    fn a_length_wider_than_usize_is_rejected_rather_than_saturated() {
345        let mut p = params(ReceivedStats::Both, StampAt::Both, Clock::Both);
346        p.length = 5_000_000_000;
347
348        assert_eq!(
349            echo_packet_len(false, &p),
350            Err(ProtoError::PacketLengthUnrepresentable {
351                length: 5_000_000_000
352            })
353        );
354    }
355}