1use 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
20pub const MAX_PACKET_SIZE: usize = 64 * 1024;
24
25#[cfg(not(wasm_browser))]
29pub(crate) const MAX_FRAME_SIZE: usize = 1024 * 1024;
30
31#[cfg(feature = "server")]
36pub(crate) const PING_INTERVAL: Duration = Duration::from_secs(15);
37
38#[cfg(feature = "server")]
40pub const PER_CLIENT_SEND_QUEUE_DEPTH: usize = 512;
41
42#[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#[derive(Debug, Clone, PartialEq, Eq, strum::Display)]
72#[non_exhaustive]
73pub enum RelayToClientMsg {
74 Datagrams {
76 remote_endpoint_id: EndpointId,
78 datagrams: Datagrams,
80 },
81 EndpointGone(EndpointId),
84 Status(Status),
86 Restarting {
88 reconnect_in: Duration,
91 try_for: Duration,
96 },
97 Ping([u8; 8]),
100 Pong([u8; 8]),
103
104 Health {
111 problem: String,
116 },
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)]
121#[non_exhaustive]
122pub enum Status {
123 #[display("The connection is healthy and has recovered from previous problems")]
125 Healthy,
126 #[display(
128 "Another endpoint connected with the same endpoint id. No more messages will be received."
129 )]
130 SameEndpointIdConnected,
131 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq)]
176#[non_exhaustive]
177pub enum ClientToRelayMsg {
178 Ping([u8; 8]),
181 Pong([u8; 8]),
184 Datagrams {
186 dst_endpoint_id: EndpointId,
188 datagrams: Datagrams,
190 },
191}
192
193#[derive(derive_more::Debug, Clone, PartialEq, Eq)]
198pub struct Datagrams {
199 pub ecn: Option<noq_proto::EcnCodepoint>,
201 pub segment_size: Option<NonZeroU16>,
204 #[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 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 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 + self.segment_size.map_or(0, |_| 2) + 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 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(); 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 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 #[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 + 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 + 4 }
385 Self::Health { problem } => problem.len(),
386 };
387 self.typ().encoded_len() + payload_len
388 }
389
390 #[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 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 + datagrams.encoded_len()
530 }
531 };
532 self.typ().encoded_len() + payload_len
533 }
534
535 #[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 "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 "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 "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 "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 #[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 const MAX_PAYLOAD_SIZE: usize = MAX_PACKET_SIZE - EndpointId::LENGTH - 1 - 2 ;
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 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 })
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 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()); }
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); }
1005}