iroh_relay/protos/
relay.rs

1//! This module implements the send/recv relaying protocol.
2//!
3//! Protocol flow:
4//!  * server occasionally sends [`FrameType::Ping`]
5//!  * client responds to any [`FrameType::Ping`] with a [`FrameType::Pong`]
6//!  * clients sends [`FrameType::ClientToRelayDatagram`] or [`FrameType::ClientToRelayDatagramBatch`]
7//!  * server then sends [`FrameType::RelayToClientDatagram`] or [`FrameType::RelayToClientDatagramBatch`] to recipient
8//!  * server sends [`FrameType::EndpointGone`] when the other client disconnects
9
10use std::num::NonZeroU16;
11
12use bytes::{Buf, BufMut, Bytes, BytesMut};
13use iroh_base::{EndpointId, KeyParsingError};
14use n0_error::{e, ensure, stack_error};
15use n0_future::time::Duration;
16
17use super::common::{FrameType, FrameTypeError};
18use crate::{KeyCache, http::ProtocolVersion};
19
20/// The maximum size of a packet sent over relay.
21/// (This only includes the data bytes visible to the socket, not
22/// including its on-wire framing overhead)
23pub const MAX_PACKET_SIZE: usize = 64 * 1024;
24
25/// The maximum frame size.
26///
27/// This is also the minimum burst size that a rate-limiter has to accept.
28#[cfg(not(wasm_browser))]
29pub(crate) const MAX_FRAME_SIZE: usize = 1024 * 1024;
30
31/// Interval in which we ping the relay server to ensure the connection is alive.
32///
33/// The default QUIC max_idle_timeout is 30s, so setting that to half this time gives some
34/// chance of recovering.
35#[cfg(feature = "server")]
36pub(crate) const PING_INTERVAL: Duration = Duration::from_secs(15);
37
38/// The number of packets buffered for sending per client
39#[cfg(feature = "server")]
40pub const PER_CLIENT_SEND_QUEUE_DEPTH: usize = 512;
41
42/// Protocol send errors.
43#[stack_error(derive, add_meta, from_sources)]
44#[allow(missing_docs)]
45#[non_exhaustive]
46pub enum Error {
47    #[error("unexpected frame: got {got:?}, expected {expected:?}")]
48    UnexpectedFrame { got: FrameType, expected: FrameType },
49    #[error("Frame is too large, has {frame_len} bytes")]
50    FrameTooLarge { frame_len: usize },
51    #[error(transparent)]
52    FrameTypeError { source: FrameTypeError },
53    #[error("Invalid public key")]
54    InvalidPublicKey { source: KeyParsingError },
55    #[error("Invalid frame encoding")]
56    InvalidFrame {},
57    #[error("Invalid frame type: {frame_type:?}")]
58    InvalidFrameType { frame_type: FrameType },
59    #[error("Invalid protocol message encoding")]
60    InvalidProtocolMessageEncoding {
61        #[error(std_err)]
62        source: std::str::Utf8Error,
63    },
64    #[error("Received a frame not allowed in this protocol version.")]
65    FrameNotAllowedInVersion,
66    #[error("Too few bytes")]
67    TooSmall {},
68}
69
70/// The messages that a relay sends to clients or the clients receive from the relay.
71#[derive(Debug, Clone, PartialEq, Eq, strum::Display)]
72#[non_exhaustive]
73pub enum RelayToClientMsg {
74    /// Represents datagrams sent from relays (originally sent to them by another client).
75    Datagrams {
76        /// The [`EndpointId`] of the original sender.
77        remote_endpoint_id: EndpointId,
78        /// The datagrams and related metadata.
79        datagrams: Datagrams,
80    },
81    /// Indicates that the client identified by the underlying public key had previously sent you a
82    /// packet but has now disconnected from the relay.
83    EndpointGone(EndpointId),
84    /// A one-way message from relay to client, declaring the connection health state.
85    Status(Status),
86    /// A one-way message from relay to client, advertising that the relay is restarting.
87    Restarting {
88        /// An advisory duration that the client should wait before attempting to reconnect.
89        /// It might be zero. It exists for the relay to smear out the reconnects.
90        reconnect_in: Duration,
91        /// An advisory duration for how long the client should attempt to reconnect
92        /// before giving up and proceeding with its normal connection failure logic. The interval
93        /// between retries is undefined for now. A relay should not send a `try_for` duration more
94        /// than a few seconds.
95        try_for: Duration,
96    },
97    /// Request from the relay to reply to the
98    /// other side with a [`ClientToRelayMsg::Pong`] with the given payload.
99    Ping([u8; 8]),
100    /// Reply to a [`ClientToRelayMsg::Ping`] from a client
101    /// with the payload sent previously in the ping.
102    Pong([u8; 8]),
103
104    // -- Deprecated variants --
105    // We don't use `#[deprecated]` because this would throw warnings for the derived serde impls.
106    /// Removed since relay-protocol-v2:
107    /// A one-way message from relay to client, declaring the connection health state.
108    ///
109    /// Use [`Self::Status`] instead.
110    Health {
111        /// Description of why the connection is unhealthy.
112        ///
113        /// The default condition is healthy, so the relay doesn't broadcast a [`RelayToClientMsg::Health`]
114        /// until a problem exists.
115        problem: String,
116    },
117}
118
119/// One-way message from server to client indicating issues with the relay connection.
120#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)]
121#[non_exhaustive]
122pub enum Status {
123    /// The connection is healthy and recovered from previous problems.
124    #[display("The connection is healthy and has recovered from previous problems")]
125    Healthy,
126    /// Another endpoint connected with the same endpoint id. No more messages will be received.
127    #[display(
128        "Another endpoint connected with the same endpoint id. No more messages will be received."
129    )]
130    SameEndpointIdConnected,
131    /// The relay is rate-limiting traffic received from this endpoint.
132    ///
133    /// Sent once per connection when the relay first throttles reading from the client.
134    #[display(
135        "The relay is rate-limiting this endpoint; outbound relay traffic is being throttled. \
136        Send less data over the relay, or, if you operate this relay, raise Limits::client_rx. \
137        Read more about rate limiting at https://docs.iroh.computer/relays/rate-limiting"
138    )]
139    RateLimited,
140    /// Placeholder for backwards-compatibility for future new health status variants.
141    #[display("Unsupported health message ({_0})")]
142    Unknown(u8),
143}
144
145impl Status {
146    #[cfg(feature = "server")]
147    fn write_to<O: BufMut>(&self, mut dst: O) -> O {
148        match self {
149            Status::Healthy => dst.put_u8(0),
150            Status::SameEndpointIdConnected => dst.put_u8(1),
151            Status::RateLimited => dst.put_u8(2),
152            Status::Unknown(discriminant) => dst.put_u8(*discriminant),
153        }
154        dst
155    }
156
157    #[cfg(feature = "server")]
158    fn encoded_len(&self) -> usize {
159        1
160    }
161
162    fn from_bytes(mut bytes: Bytes) -> Result<Self, Error> {
163        ensure!(!bytes.is_empty(), Error::InvalidFrame);
164        let discriminant = bytes.get_u8();
165        match discriminant {
166            0 => Ok(Self::Healthy),
167            1 => Ok(Self::SameEndpointIdConnected),
168            2 => Ok(Self::RateLimited),
169            n => Ok(Self::Unknown(n)),
170        }
171    }
172}
173
174/// Messages that clients send to relays.
175#[derive(Debug, Clone, PartialEq, Eq)]
176#[non_exhaustive]
177pub enum ClientToRelayMsg {
178    /// Request from the client to the server to reply to the
179    /// other side with a [`RelayToClientMsg::Pong`] with the given payload.
180    Ping([u8; 8]),
181    /// Reply to a [`RelayToClientMsg::Ping`] from a server
182    /// with the payload sent previously in the ping.
183    Pong([u8; 8]),
184    /// Request from the client to relay datagrams to given remote endpoint.
185    Datagrams {
186        /// The remote endpoint to relay to.
187        dst_endpoint_id: EndpointId,
188        /// The datagrams and related metadata to relay.
189        datagrams: Datagrams,
190    },
191}
192
193/// One or multiple datagrams being transferred via the relay.
194///
195/// This type is modeled after [`noq_proto::Transmit`]
196/// (or even more similarly `noq_udp::Transmit`, but we don't depend on that library here).
197#[derive(derive_more::Debug, Clone, PartialEq, Eq)]
198pub struct Datagrams {
199    /// Explicit congestion notification bits
200    pub ecn: Option<noq_proto::EcnCodepoint>,
201    /// The segment size if this transmission contains multiple datagrams.
202    /// This is `None` if the transmit only contains a single datagram
203    pub segment_size: Option<NonZeroU16>,
204    /// The contents of the datagram(s)
205    #[debug(skip)]
206    pub contents: Bytes,
207}
208
209impl<T: AsRef<[u8]>> From<T> for Datagrams {
210    fn from(bytes: T) -> Self {
211        Self {
212            ecn: None,
213            segment_size: None,
214            contents: Bytes::copy_from_slice(bytes.as_ref()),
215        }
216    }
217}
218
219impl Datagrams {
220    /// Splits the current datagram into at maximum `num_segments` segments, returning
221    /// the batch with at most `num_segments` and leaving only the rest in `self`.
222    ///
223    /// Calling this on a datagram batch that only contains a single datagram (`segment_size == None`)
224    /// will result in returning essentially a clone of `self`, while making `self` empty afterwards.
225    ///
226    /// Calling this on a datagram batch with e.g. 15 datagrams with `num_segments == 10` will
227    /// result in returning a datagram batch that contains the first 10 datagrams and leave `self`
228    /// containing the remaining 5 datagrams.
229    ///
230    /// Calling this on a datagram batch with less than `num_segments` datagrams will result in
231    /// making `self` empty and returning essentially a clone of `self`.
232    pub fn take_segments(&mut self, num_segments: usize) -> Datagrams {
233        let Some(segment_size) = self.segment_size else {
234            let contents = std::mem::take(&mut self.contents);
235            return Datagrams {
236                ecn: self.ecn,
237                segment_size: None,
238                contents,
239            };
240        };
241
242        let usize_segment_size = usize::from(u16::from(segment_size));
243        let max_content_len = num_segments * usize_segment_size;
244        let contents = self
245            .contents
246            .split_to(std::cmp::min(max_content_len, self.contents.len()));
247
248        let is_datagram_batch = num_segments > 1 && usize_segment_size < contents.len();
249
250        // If this left our batch with only one more datagram, then remove the segment size
251        // to uphold the invariant that single-datagram batches don't have a segment size set.
252        if self.contents.len() <= usize_segment_size {
253            self.segment_size = None;
254        }
255
256        Datagrams {
257            ecn: self.ecn,
258            segment_size: is_datagram_batch.then_some(segment_size),
259            contents,
260        }
261    }
262
263    fn write_to<O: BufMut>(&self, mut dst: O) -> O {
264        let ecn = self.ecn.map_or(0, |ecn| ecn as u8);
265        dst.put_u8(ecn);
266        if let Some(segment_size) = self.segment_size {
267            dst.put_u16(segment_size.into());
268        }
269        dst.put(self.contents.as_ref());
270        dst
271    }
272
273    fn encoded_len(&self) -> usize {
274        1 // ECN byte
275        + self.segment_size.map_or(0, |_| 2) // segment size, when None, then a packed representation is assumed
276        + self.contents.len()
277    }
278
279    #[allow(clippy::len_zero, clippy::result_large_err)]
280    fn from_bytes(mut bytes: Bytes, is_batch: bool) -> Result<Self, Error> {
281        if is_batch {
282            // 1 bytes ECN, 2 bytes segment size
283            ensure!(bytes.len() >= 3, Error::InvalidFrame);
284        } else {
285            ensure!(bytes.len() >= 1, Error::InvalidFrame);
286        }
287
288        let ecn_byte = bytes.get_u8();
289        let ecn = noq_proto::EcnCodepoint::from_bits(ecn_byte);
290
291        let segment_size = if is_batch {
292            let segment_size = bytes.get_u16(); // length checked above
293            NonZeroU16::new(segment_size)
294        } else {
295            None
296        };
297
298        Ok(Self {
299            ecn,
300            segment_size,
301            contents: bytes,
302        })
303    }
304}
305
306impl RelayToClientMsg {
307    /// Returns this frame's corresponding frame type.
308    pub fn typ(&self) -> FrameType {
309        match self {
310            Self::Datagrams { datagrams, .. } => {
311                if datagrams.segment_size.is_some() {
312                    FrameType::RelayToClientDatagramBatch
313                } else {
314                    FrameType::RelayToClientDatagram
315                }
316            }
317            Self::EndpointGone { .. } => FrameType::EndpointGone,
318            Self::Ping { .. } => FrameType::Ping,
319            Self::Pong { .. } => FrameType::Pong,
320            Self::Status { .. } => FrameType::Status,
321            Self::Restarting { .. } => FrameType::Restarting,
322            Self::Health { .. } => FrameType::Health,
323        }
324    }
325
326    #[cfg(feature = "server")]
327    pub(crate) fn to_bytes(&self) -> BytesMut {
328        self.write_to(BytesMut::with_capacity(self.encoded_len()))
329    }
330
331    /// Encodes this frame for sending over websockets.
332    ///
333    /// Specifically meant for being put into a binary websocket message frame.
334    #[cfg(feature = "server")]
335    pub(crate) fn write_to<O: BufMut>(&self, mut dst: O) -> O {
336        dst = self.typ().write_to(dst);
337        match self {
338            Self::Datagrams {
339                remote_endpoint_id,
340                datagrams,
341            } => {
342                dst.put(remote_endpoint_id.as_ref());
343                dst = datagrams.write_to(dst);
344            }
345            Self::EndpointGone(endpoint_id) => {
346                dst.put(endpoint_id.as_ref());
347            }
348            Self::Ping(data) => {
349                dst.put(&data[..]);
350            }
351            Self::Pong(data) => {
352                dst.put(&data[..]);
353            }
354            Self::Health { problem } => {
355                dst.put(problem.as_ref());
356            }
357            Self::Restarting {
358                reconnect_in,
359                try_for,
360            } => {
361                dst.put_u32(reconnect_in.as_millis() as u32);
362                dst.put_u32(try_for.as_millis() as u32);
363            }
364            Self::Status(status) => {
365                dst = status.write_to(dst);
366            }
367        }
368        dst
369    }
370
371    #[cfg(feature = "server")]
372    pub(crate) fn encoded_len(&self) -> usize {
373        let payload_len = match self {
374            Self::Datagrams { datagrams, .. } => {
375                32 // endpointid
376                + datagrams.encoded_len()
377            }
378            Self::EndpointGone(_) => 32,
379            Self::Ping(_) | Self::Pong(_) => 8,
380            Self::Status(status) => status.encoded_len(),
381            Self::Restarting { .. } => {
382                4 // u32
383                + 4 // u32
384            }
385            Self::Health { problem } => problem.len(),
386        };
387        self.typ().encoded_len() + payload_len
388    }
389
390    /// Tries to decode a frame received over websockets.
391    ///
392    /// Specifically, bytes received from a binary websocket message frame.
393    ///
394    /// `protocol_version` is the negotiated protocol version for this connection.
395    #[allow(clippy::result_large_err)]
396    pub(crate) fn from_bytes(
397        mut content: Bytes,
398        cache: &KeyCache,
399        protocol_version: ProtocolVersion,
400    ) -> Result<Self, Error> {
401        let frame_type = FrameType::from_bytes(&mut content)?;
402        let frame_len = content.len();
403        ensure!(
404            frame_len <= MAX_PACKET_SIZE,
405            Error::FrameTooLarge { frame_len }
406        );
407
408        let res = match frame_type {
409            FrameType::RelayToClientDatagram | FrameType::RelayToClientDatagramBatch => {
410                ensure!(content.len() >= EndpointId::LENGTH, Error::InvalidFrame);
411
412                let remote_endpoint_id = cache.key_from_slice(&content[..EndpointId::LENGTH])?;
413                let datagrams = Datagrams::from_bytes(
414                    content.slice(EndpointId::LENGTH..),
415                    frame_type == FrameType::RelayToClientDatagramBatch,
416                )?;
417                Self::Datagrams {
418                    remote_endpoint_id,
419                    datagrams,
420                }
421            }
422            FrameType::EndpointGone => {
423                ensure!(content.len() == EndpointId::LENGTH, Error::InvalidFrame);
424                let endpoint_id = cache.key_from_slice(content.as_ref())?;
425                Self::EndpointGone(endpoint_id)
426            }
427            FrameType::Ping => {
428                ensure!(content.len() == 8, Error::InvalidFrame);
429                let mut data = [0u8; 8];
430                data.copy_from_slice(&content[..8]);
431                Self::Ping(data)
432            }
433            FrameType::Pong => {
434                ensure!(content.len() == 8, Error::InvalidFrame);
435                let mut data = [0u8; 8];
436                data.copy_from_slice(&content[..8]);
437                Self::Pong(data)
438            }
439            FrameType::Health => {
440                ensure!(
441                    protocol_version == ProtocolVersion::V1,
442                    Error::FrameNotAllowedInVersion
443                );
444                let problem = std::str::from_utf8(&content)?.to_owned();
445                Self::Health { problem }
446            }
447            FrameType::Restarting => {
448                ensure!(content.len() == 4 + 4, Error::InvalidFrame);
449                let reconnect_in = u32::from_be_bytes(
450                    content[..4]
451                        .try_into()
452                        .map_err(|_| e!(Error::InvalidFrame))?,
453                );
454                let try_for = u32::from_be_bytes(
455                    content[4..]
456                        .try_into()
457                        .map_err(|_| e!(Error::InvalidFrame))?,
458                );
459                let reconnect_in = Duration::from_millis(reconnect_in as u64);
460                let try_for = Duration::from_millis(try_for as u64);
461                Self::Restarting {
462                    reconnect_in,
463                    try_for,
464                }
465            }
466            FrameType::Status => {
467                ensure!(
468                    protocol_version >= ProtocolVersion::V2,
469                    Error::FrameNotAllowedInVersion
470                );
471                let status = Status::from_bytes(content)?;
472                Self::Status(status)
473            }
474            _ => {
475                return Err(e!(Error::InvalidFrameType { frame_type }));
476            }
477        };
478        Ok(res)
479    }
480}
481
482impl ClientToRelayMsg {
483    pub(crate) fn typ(&self) -> FrameType {
484        match self {
485            Self::Datagrams { datagrams, .. } => {
486                if datagrams.segment_size.is_some() {
487                    FrameType::ClientToRelayDatagramBatch
488                } else {
489                    FrameType::ClientToRelayDatagram
490                }
491            }
492            Self::Ping { .. } => FrameType::Ping,
493            Self::Pong { .. } => FrameType::Pong,
494        }
495    }
496
497    pub(crate) fn to_bytes(&self) -> BytesMut {
498        self.write_to(BytesMut::with_capacity(self.encoded_len()))
499    }
500
501    /// Encodes this frame for sending over websockets.
502    ///
503    /// Specifically meant for being put into a binary websocket message frame.
504    pub(crate) fn write_to<O: BufMut>(&self, mut dst: O) -> O {
505        dst = self.typ().write_to(dst);
506        match self {
507            Self::Datagrams {
508                dst_endpoint_id,
509                datagrams,
510            } => {
511                dst.put(dst_endpoint_id.as_ref());
512                dst = datagrams.write_to(dst);
513            }
514            Self::Ping(data) => {
515                dst.put(&data[..]);
516            }
517            Self::Pong(data) => {
518                dst.put(&data[..]);
519            }
520        }
521        dst
522    }
523
524    pub(crate) fn encoded_len(&self) -> usize {
525        let payload_len = match self {
526            Self::Ping(_) | Self::Pong(_) => 8,
527            Self::Datagrams { datagrams, .. } => {
528                32 // endpoint id
529                + datagrams.encoded_len()
530            }
531        };
532        self.typ().encoded_len() + payload_len
533    }
534
535    /// Tries to decode a frame received over websockets.
536    ///
537    /// Specifically, bytes received from a binary websocket message frame.
538    #[allow(clippy::result_large_err)]
539    #[cfg(feature = "server")]
540    pub(crate) fn from_bytes(mut content: Bytes, cache: &KeyCache) -> Result<Self, Error> {
541        let frame_type = FrameType::from_bytes(&mut content)?;
542        let frame_len = content.len();
543        ensure!(
544            frame_len <= MAX_PACKET_SIZE,
545            Error::FrameTooLarge { frame_len }
546        );
547
548        let res = match frame_type {
549            FrameType::ClientToRelayDatagram | FrameType::ClientToRelayDatagramBatch => {
550                ensure!(content.len() >= EndpointId::LENGTH, Error::InvalidFrame);
551
552                let dst_endpoint_id = cache.key_from_slice(&content[..EndpointId::LENGTH])?;
553                let datagrams = Datagrams::from_bytes(
554                    content.slice(EndpointId::LENGTH..),
555                    frame_type == FrameType::ClientToRelayDatagramBatch,
556                )?;
557                Self::Datagrams {
558                    dst_endpoint_id,
559                    datagrams,
560                }
561            }
562            FrameType::Ping => {
563                ensure!(content.len() == 8, Error::InvalidFrame);
564                let mut data = [0u8; 8];
565                data.copy_from_slice(&content[..8]);
566                Self::Ping(data)
567            }
568            FrameType::Pong => {
569                ensure!(content.len() == 8, Error::InvalidFrame);
570                let mut data = [0u8; 8];
571                data.copy_from_slice(&content[..8]);
572                Self::Pong(data)
573            }
574            _ => {
575                return Err(e!(Error::InvalidFrameType { frame_type }));
576            }
577        };
578        Ok(res)
579    }
580}
581
582#[cfg(test)]
583#[cfg(feature = "server")]
584mod tests {
585    use data_encoding::HEXLOWER;
586    use iroh_base::SecretKey;
587    use n0_error::Result;
588
589    use super::*;
590
591    fn check_expected_bytes(frames: Vec<(Vec<u8>, &str)>) {
592        for (bytes, expected_hex) in frames {
593            let stripped: Vec<u8> = expected_hex
594                .chars()
595                .filter_map(|s| {
596                    if s.is_ascii_whitespace() {
597                        None
598                    } else {
599                        Some(s as u8)
600                    }
601                })
602                .collect();
603            let expected_bytes = HEXLOWER.decode(&stripped).unwrap();
604            assert_eq!(HEXLOWER.encode(&bytes), HEXLOWER.encode(&expected_bytes));
605        }
606    }
607
608    #[test]
609    fn test_relay_client_frames_snapshot() -> Result {
610        let client_key = SecretKey::from_bytes(&[42u8; 32]);
611
612        check_expected_bytes(vec![
613            (
614                RelayToClientMsg::Health {
615                    problem: "Hello? Yes this is dog.".into(),
616                }
617                .write_to(Vec::new()),
618                "0b 48 65 6c 6c 6f 3f 20 59 65 73 20 74 68 69 73
619                20 69 73 20 64 6f 67 2e",
620            ),
621            (
622                RelayToClientMsg::EndpointGone(client_key.public()).write_to(Vec::new()),
623                "08 19 7f 6b 23 e1 6c 85 32 c6 ab c8 38 fa cd 5e
624                a7 89 be 0c 76 b2 92 03 34 03 9b fa 8b 3d 36 8d
625                61",
626            ),
627            (
628                RelayToClientMsg::Ping([42u8; 8]).write_to(Vec::new()),
629                "09 2a 2a 2a 2a 2a 2a 2a 2a",
630            ),
631            (
632                RelayToClientMsg::Pong([42u8; 8]).write_to(Vec::new()),
633                "0a 2a 2a 2a 2a 2a 2a 2a 2a",
634            ),
635            (
636                RelayToClientMsg::Datagrams {
637                    remote_endpoint_id: client_key.public(),
638                    datagrams: Datagrams {
639                        ecn: Some(noq::EcnCodepoint::Ce),
640                        segment_size: NonZeroU16::new(6),
641                        contents: "Hello World!".into(),
642                    },
643                }
644                .write_to(Vec::new()),
645                // frame type
646                // public key first 16 bytes
647                // public key second 16 bytes
648                // ECN byte
649                // segment size
650                // hello world contents bytes
651                "07
652                19 7f 6b 23 e1 6c 85 32 c6 ab c8 38 fa cd 5e a7
653                89 be 0c 76 b2 92 03 34 03 9b fa 8b 3d 36 8d 61
654                03
655                00 06
656                48 65 6c 6c 6f 20 57 6f 72 6c 64 21",
657            ),
658            (
659                RelayToClientMsg::Datagrams {
660                    remote_endpoint_id: client_key.public(),
661                    datagrams: Datagrams {
662                        ecn: Some(noq::EcnCodepoint::Ce),
663                        segment_size: None,
664                        contents: "Hello World!".into(),
665                    },
666                }
667                .write_to(Vec::new()),
668                // frame type
669                // public key first 16 bytes
670                // public key second 16 bytes
671                // ECN byte
672                // hello world contents bytes
673                "06
674                19 7f 6b 23 e1 6c 85 32 c6 ab c8 38 fa cd 5e a7
675                89 be 0c 76 b2 92 03 34 03 9b fa 8b 3d 36 8d 61
676                03
677                48 65 6c 6c 6f 20 57 6f 72 6c 64 21",
678            ),
679            (
680                RelayToClientMsg::Restarting {
681                    reconnect_in: Duration::from_millis(10),
682                    try_for: Duration::from_millis(20),
683                }
684                .write_to(Vec::new()),
685                "0c 00 00 00 0a 00 00 00 14",
686            ),
687            (
688                RelayToClientMsg::Status(Status::SameEndpointIdConnected).write_to(Vec::new()),
689                "0d 01",
690            ),
691            (
692                RelayToClientMsg::Status(Status::RateLimited).write_to(Vec::new()),
693                "0d 02",
694            ),
695        ]);
696
697        Ok(())
698    }
699
700    #[test]
701    fn test_client_relay_frames_snapshot() -> Result {
702        let client_key = SecretKey::from_bytes(&[42u8; 32]);
703
704        check_expected_bytes(vec![
705            (
706                ClientToRelayMsg::Ping([42u8; 8]).write_to(Vec::new()),
707                "09 2a 2a 2a 2a 2a 2a 2a 2a",
708            ),
709            (
710                ClientToRelayMsg::Pong([42u8; 8]).write_to(Vec::new()),
711                "0a 2a 2a 2a 2a 2a 2a 2a 2a",
712            ),
713            (
714                ClientToRelayMsg::Datagrams {
715                    dst_endpoint_id: client_key.public(),
716                    datagrams: Datagrams {
717                        ecn: Some(noq::EcnCodepoint::Ce),
718                        segment_size: NonZeroU16::new(6),
719                        contents: "Hello World!".into(),
720                    },
721                }
722                .write_to(Vec::new()),
723                // frame type
724                // public key first 16 bytes
725                // public key second 16 bytes
726                // ECN byte
727                // Segment size
728                // hello world contents
729                "05
730                19 7f 6b 23 e1 6c 85 32 c6 ab c8 38 fa cd 5e a7
731                89 be 0c 76 b2 92 03 34 03 9b fa 8b 3d 36 8d 61
732                03
733                00 06
734                48 65 6c 6c 6f 20 57 6f 72 6c 64 21",
735            ),
736            (
737                ClientToRelayMsg::Datagrams {
738                    dst_endpoint_id: client_key.public(),
739                    datagrams: Datagrams {
740                        ecn: Some(noq::EcnCodepoint::Ce),
741                        segment_size: None,
742                        contents: "Hello World!".into(),
743                    },
744                }
745                .write_to(Vec::new()),
746                // frame type
747                // public key first 16 bytes
748                // public key second 16 bytes
749                // ECN byte
750                // hello world contents
751                "04
752                19 7f 6b 23 e1 6c 85 32 c6 ab c8 38 fa cd 5e a7
753                89 be 0c 76 b2 92 03 34 03 9b fa 8b 3d 36 8d 61
754                03
755                48 65 6c 6c 6f 20 57 6f 72 6c 64 21",
756            ),
757        ]);
758
759        Ok(())
760    }
761
762    /// A datagram frame must contain at least an EndpointId (32 bytes) after
763    /// the frame type. A frame consisting only of the frame type byte used to
764    /// panic when slicing the destination endpoint id.
765    #[test]
766    fn regression_client_to_relay_undersized_datagram_rejected() {
767        for frame_type in [
768            FrameType::ClientToRelayDatagram,
769            FrameType::ClientToRelayDatagramBatch,
770        ] {
771            let encoded = frame_type.write_to(BytesMut::new()).freeze();
772            let result = ClientToRelayMsg::from_bytes(encoded, &KeyCache::test());
773            assert!(
774                matches!(result, Err(Error::InvalidFrame { .. })),
775                "expected InvalidFrame for {:?}, got {:?}",
776                frame_type,
777                result
778            );
779        }
780    }
781}
782
783#[cfg(all(test, feature = "server"))]
784mod proptests {
785    use iroh_base::SecretKey;
786    use proptest::{collection::vec, prelude::*};
787    use test_strategy::proptest;
788
789    use super::*;
790
791    fn secret_key() -> impl Strategy<Value = SecretKey> {
792        prop::array::uniform32(any::<u8>()).prop_map(SecretKey::from)
793    }
794
795    fn key() -> impl Strategy<Value = EndpointId> {
796        secret_key().prop_map(|key| key.public())
797    }
798
799    fn ecn() -> impl Strategy<Value = Option<noq_proto::EcnCodepoint>> {
800        (0..=3).prop_map(|n| match n {
801            1 => Some(noq_proto::EcnCodepoint::Ce),
802            2 => Some(noq_proto::EcnCodepoint::Ect0),
803            3 => Some(noq_proto::EcnCodepoint::Ect1),
804            _ => None,
805        })
806    }
807
808    fn datagrams() -> impl Strategy<Value = Datagrams> {
809        // The max payload size (conservatively, since with segment_size = 0 we'd have slightly more space)
810        const MAX_PAYLOAD_SIZE: usize = MAX_PACKET_SIZE - EndpointId::LENGTH - 1 /* ECN bytes */ - 2 /* segment size */;
811        (
812            ecn(),
813            prop::option::of(MAX_PAYLOAD_SIZE / 20..MAX_PAYLOAD_SIZE),
814            vec(any::<u8>(), 0..MAX_PAYLOAD_SIZE),
815        )
816            .prop_map(|(ecn, segment_size, data)| Datagrams {
817                ecn,
818                segment_size: segment_size
819                    .map(|ss| std::cmp::min(data.len(), ss) as u16)
820                    .and_then(NonZeroU16::new),
821                contents: Bytes::from(data),
822            })
823    }
824
825    /// Generates a random valid frame
826    fn relay_client_frame() -> impl Strategy<Value = RelayToClientMsg> {
827        let recv_packet = (key(), datagrams()).prop_map(|(remote_endpoint_id, datagrams)| {
828            RelayToClientMsg::Datagrams {
829                remote_endpoint_id,
830                datagrams,
831            }
832        });
833        let endpoint_gone = key().prop_map(RelayToClientMsg::EndpointGone);
834        let ping = prop::array::uniform8(any::<u8>()).prop_map(RelayToClientMsg::Ping);
835        let pong = prop::array::uniform8(any::<u8>()).prop_map(RelayToClientMsg::Pong);
836        let v1health = ".{0,65536}"
837            .prop_filter("exceeds MAX_PACKET_SIZE", |s| {
838                s.len() < MAX_PACKET_SIZE // a single unicode character can match a regex "." but take up multiple bytes
839            })
840            .prop_map(|problem| RelayToClientMsg::Health { problem });
841        let health = prop_oneof![
842            Just(Status::Healthy),
843            Just(Status::SameEndpointIdConnected),
844            Just(Status::RateLimited),
845        ]
846        .prop_map(RelayToClientMsg::Status);
847        let restarting = (any::<u32>(), any::<u32>()).prop_map(|(reconnect_in, try_for)| {
848            RelayToClientMsg::Restarting {
849                reconnect_in: Duration::from_millis(reconnect_in.into()),
850                try_for: Duration::from_millis(try_for.into()),
851            }
852        });
853        prop_oneof![
854            recv_packet,
855            endpoint_gone,
856            ping,
857            pong,
858            v1health,
859            restarting,
860            health
861        ]
862        .boxed()
863    }
864
865    fn client_relay_frame() -> impl Strategy<Value = ClientToRelayMsg> {
866        let send_packet = (key(), datagrams()).prop_map(|(dst_endpoint_id, datagrams)| {
867            ClientToRelayMsg::Datagrams {
868                dst_endpoint_id,
869                datagrams,
870            }
871        });
872        let ping = prop::array::uniform8(any::<u8>()).prop_map(ClientToRelayMsg::Ping);
873        let pong = prop::array::uniform8(any::<u8>()).prop_map(ClientToRelayMsg::Pong);
874        prop_oneof![send_packet, ping, pong]
875    }
876
877    /// The earliest protocol version in which `frame` is allowed.
878    fn allowed_version(frame: &RelayToClientMsg) -> ProtocolVersion {
879        match frame {
880            RelayToClientMsg::Health { .. } => ProtocolVersion::V1,
881            _ => ProtocolVersion::V2,
882        }
883    }
884
885    #[test]
886    fn v1health_rejected_in_v2() {
887        let frame = RelayToClientMsg::Health {
888            problem: "test".into(),
889        };
890        let encoded = frame.to_bytes().freeze();
891        let result = RelayToClientMsg::from_bytes(encoded, &KeyCache::test(), ProtocolVersion::V2);
892        assert!(matches!(
893            result,
894            Err(Error::FrameNotAllowedInVersion { .. })
895        ));
896    }
897
898    #[test]
899    fn status_rejected_in_v1() {
900        let frame = RelayToClientMsg::Status(Status::SameEndpointIdConnected);
901        let encoded = frame.to_bytes().freeze();
902        let result = RelayToClientMsg::from_bytes(encoded, &KeyCache::test(), ProtocolVersion::V1);
903        assert!(matches!(
904            result,
905            Err(Error::FrameNotAllowedInVersion { .. })
906        ));
907    }
908
909    #[proptest]
910    fn relay_client_frame_roundtrip(#[strategy(relay_client_frame())] frame: RelayToClientMsg) {
911        let version = allowed_version(&frame);
912        let encoded = frame.to_bytes().freeze();
913        let decoded = RelayToClientMsg::from_bytes(encoded, &KeyCache::test(), version).unwrap();
914        prop_assert_eq!(frame, decoded);
915    }
916
917    #[proptest]
918    fn client_relay_frame_roundtrip(#[strategy(client_relay_frame())] frame: ClientToRelayMsg) {
919        let encoded = frame.to_bytes().freeze();
920        let decoded = ClientToRelayMsg::from_bytes(encoded, &KeyCache::test()).unwrap();
921        prop_assert_eq!(frame, decoded);
922    }
923
924    #[proptest]
925    fn relay_client_frame_encoded_len(#[strategy(relay_client_frame())] frame: RelayToClientMsg) {
926        let claimed_encoded_len = frame.encoded_len();
927        let actual_encoded_len = frame.to_bytes().len();
928        prop_assert_eq!(claimed_encoded_len, actual_encoded_len);
929    }
930
931    #[proptest]
932    fn client_relay_frame_encoded_len(#[strategy(client_relay_frame())] frame: ClientToRelayMsg) {
933        let claimed_encoded_len = frame.encoded_len();
934        let actual_encoded_len = frame.to_bytes().len();
935        prop_assert_eq!(claimed_encoded_len, actual_encoded_len);
936    }
937
938    #[proptest]
939    fn datagrams_encoded_len(#[strategy(datagrams())] datagrams: Datagrams) {
940        let claimed_encoded_len = datagrams.encoded_len();
941        let actual_encoded_len = datagrams.write_to(Vec::new()).len();
942        prop_assert_eq!(claimed_encoded_len, actual_encoded_len);
943    }
944
945    const MAX_TEST_MSG_SIZE: usize = 100_000;
946
947    fn perturb_encoding(
948        inner: impl Strategy<Value = BytesMut> + Clone + 'static,
949    ) -> impl Strategy<Value = Bytes> + 'static {
950        #[derive(Debug, test_strategy::Arbitrary)]
951        enum GenStrategy {
952            ArbitraryBytes,
953            FlipBits,
954            Truncate,
955        }
956
957        any::<GenStrategy>().prop_ind_flat_map(move |strategy| match strategy {
958            GenStrategy::ArbitraryBytes => vec(any::<u8>(), 0..MAX_TEST_MSG_SIZE)
959                .prop_map(Bytes::from)
960                .boxed(),
961            GenStrategy::FlipBits => (vec(any::<u16>(), 0..20), any::<u8>(), inner.clone())
962                .prop_map(|(byte_positions, mask, mut bytes)| {
963                    let len = bytes.len();
964                    for pos in byte_positions {
965                        bytes[(pos as usize).min(len.saturating_sub(1))] ^= mask;
966                    }
967                    bytes.freeze()
968                })
969                .boxed(),
970            GenStrategy::Truncate => (vec(any::<u16>(), 0..20), inner.clone())
971                .prop_map(|(mut truncations, mut bytes)| {
972                    truncations.sort();
973                    let mut bytes_truncated = 0;
974                    for [trunc_start, trunc_end] in truncations.as_chunks::<2>().0 {
975                        let len = bytes.len();
976                        let start = usize::from(*trunc_start).saturating_sub(bytes_truncated);
977                        let end = usize::from(*trunc_end).saturating_sub(bytes_truncated);
978                        let rest = bytes.split_off(end.min(len));
979                        bytes.truncate(start.min(len));
980                        bytes.extend_from_slice(&rest);
981                        bytes_truncated += end - start;
982                    }
983                    bytes.freeze()
984                })
985                .boxed(),
986        })
987    }
988
989    #[proptest]
990    fn client_relay_frame_decode_no_panic(
991        #[strategy(perturb_encoding(client_relay_frame().boxed().prop_map(|msg| msg.to_bytes())))]
992        bytes: Bytes,
993    ) {
994        let _ = ClientToRelayMsg::from_bytes(bytes, &KeyCache::test()); // only assert no panic
995    }
996
997    #[proptest]
998    fn relay_client_frame_decode_no_panic(
999        #[strategy(perturb_encoding(relay_client_frame().boxed().prop_map(|msg| msg.to_bytes())))]
1000        bytes: Bytes,
1001        version: ProtocolVersion,
1002    ) {
1003        let _ = RelayToClientMsg::from_bytes(bytes, &KeyCache::test(), version); // only assert no panic
1004    }
1005}