From e0522c2a554ee6b9721e44d9df23d516ae26de61 Mon Sep 17 00:00:00 2001 From: Jeffrey Moon Date: Sun, 8 Mar 2026 19:21:16 -0400 Subject: [PATCH 01/15] Add IGMPv1 header with reserved-byte preservation, layer integration, and tests --- etherparse/src/err/layer.rs | 6 + etherparse/src/transport/igmpv1_header.rs | 457 ++++++++++++++++++++++ etherparse/src/transport/mod.rs | 4 + 3 files changed, 467 insertions(+) create mode 100644 etherparse/src/transport/igmpv1_header.rs diff --git a/etherparse/src/err/layer.rs b/etherparse/src/err/layer.rs index d977fb29..7bf4f331 100644 --- a/etherparse/src/err/layer.rs +++ b/etherparse/src/err/layer.rs @@ -50,6 +50,8 @@ pub enum Layer { Icmpv4TimestampReply, /// Error occurred while parsing an ICMPv6 packet. Icmpv6, + /// Error occurred while parsing an IGMPv1 packet. + Igmpv1, /// Error occurred while parsing an Address Resolution Protocol packet. Arp, } @@ -83,6 +85,7 @@ impl Layer { Icmpv4Timestamp => "ICMP Timestamp Error", Icmpv4TimestampReply => "ICMP Timestamp Reply Error", Icmpv6 => "ICMPv6 Packet Error", + Igmpv1 => "IGMPv1 Packet Error", Arp => "Address Resolution Protocol Packet Error", } } @@ -116,6 +119,7 @@ impl core::fmt::Display for Layer { Icmpv4Timestamp => write!(f, "ICMP timestamp message"), Icmpv4TimestampReply => write!(f, "ICMP timestamp reply message"), Icmpv6 => write!(f, "ICMPv6 packet"), + Igmpv1 => write!(f, "IGMPv1 packet"), Arp => write!(f, "Address Resolution Protocol packet"), } } @@ -185,6 +189,7 @@ mod test { (Icmpv4Timestamp, "ICMP Timestamp Error"), (Icmpv4TimestampReply, "ICMP Timestamp Reply Error"), (Icmpv6, "ICMPv6 Packet Error"), + (Igmpv1, "IGMPv1 Packet Error"), (Arp, "Address Resolution Protocol Packet Error"), ]; for test in tests { @@ -219,6 +224,7 @@ mod test { (Icmpv4Timestamp, "ICMP timestamp message"), (Icmpv4TimestampReply, "ICMP timestamp reply message"), (Icmpv6, "ICMPv6 packet"), + (Igmpv1, "IGMPv1 packet"), (Arp, "Address Resolution Protocol packet"), ]; for test in tests { diff --git a/etherparse/src/transport/igmpv1_header.rs b/etherparse/src/transport/igmpv1_header.rs new file mode 100644 index 00000000..1cc41c0b --- /dev/null +++ b/etherparse/src/transport/igmpv1_header.rs @@ -0,0 +1,457 @@ +use crate::*; + +/// Membership Query message type. +pub const IGMPV1_TYPE_MEMBERSHIP_QUERY: u8 = 0x11; +/// Version 1 Membership Report message type. +pub const IGMPV1_TYPE_MEMBERSHIP_REPORT: u8 = 0x12; + +/// A header of an IGMPv1 packet. +/// +/// IGMPv1 has a fixed header size of 8 bytes: +/// type, reserved, checksum and group address. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Igmpv1Header { + /// IGMP message type. + pub igmp_type: u8, + /// Reserved/unused octet. + pub reserved: u8, + /// Checksum in the IGMP header. + pub checksum: u16, + /// Group address. + pub group_address: [u8; 4], +} + +impl Igmpv1Header { + /// Number of bytes/octets an [`Igmpv1Header`] takes up in serialized form. + pub const LEN: usize = 8; + + /// Constructs an [`Igmpv1Header`] with reserved & checksum set to 0. + #[inline] + pub fn new(igmp_type: u8, group_address: [u8; 4]) -> Igmpv1Header { + Igmpv1Header { + igmp_type, + reserved: 0, + checksum: 0, + group_address, + } + } + + /// Creates an [`Igmpv1Header`] with a checksum calculated from the header values. + #[inline] + pub fn with_checksum(igmp_type: u8, group_address: [u8; 4]) -> Igmpv1Header { + let mut result = Igmpv1Header::new(igmp_type, group_address); + result.update_checksum(); + result + } + + /// Reads an IGMPv1 header from a slice directly and returns a tuple containing + /// the resulting header & unused part of the slice. + #[inline] + pub fn from_slice(slice: &[u8]) -> Result<(Igmpv1Header, &[u8]), err::LenError> { + if slice.len() < Self::LEN { + return Err(err::LenError { + required_len: Self::LEN, + len: slice.len(), + len_source: LenSource::Slice, + layer: err::Layer::Igmpv1, + layer_start_offset: 0, + }); + } + + Ok(( + Igmpv1Header::from_bytes([ + slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7], + ]), + &slice[Self::LEN..], + )) + } + + /// Read an [`Igmpv1Header`] from a static sized byte array. + #[inline] + pub fn from_bytes(bytes: [u8; 8]) -> Igmpv1Header { + Igmpv1Header { + igmp_type: bytes[0], + reserved: bytes[1], + checksum: u16::from_be_bytes([bytes[2], bytes[3]]), + group_address: [bytes[4], bytes[5], bytes[6], bytes[7]], + } + } + + /// Reads an IGMPv1 header from the given reader. + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] + pub fn read( + reader: &mut T, + ) -> Result { + let mut bytes = [0u8; Self::LEN]; + reader.read_exact(&mut bytes)?; + Ok(Igmpv1Header::from_bytes(bytes)) + } + + /// Write the IGMPv1 header to the given writer. + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] + pub fn write(&self, writer: &mut T) -> Result<(), std::io::Error> { + writer.write_all(&self.to_bytes()) + } + + /// Length in bytes/octets of this header type. + #[inline] + pub const fn header_len(&self) -> usize { + Self::LEN + } + + /// Calculates and returns the checksum based on the current header values. + #[inline] + pub fn calc_checksum(&self) -> u16 { + checksum::Sum16BitWords::new() + .add_2bytes([self.igmp_type, self.reserved]) + .add_4bytes(self.group_address) + .ones_complement() + .to_be() + } + + /// Calculates and updates the checksum in the header. + #[inline] + pub fn update_checksum(&mut self) { + self.checksum = self.calc_checksum(); + } + + /// Converts the header to on-the-wire bytes. + #[inline] + pub fn to_bytes(&self) -> [u8; 8] { + let checksum_be = self.checksum.to_be_bytes(); + [ + self.igmp_type, + self.reserved, + checksum_be[0], + checksum_be[1], + self.group_address[0], + self.group_address[1], + self.group_address[2], + self.group_address[3], + ] + } +} + +#[cfg(test)] +mod test { + use crate::{ + err::{Layer, LenError}, + *, + }; + use alloc::{format, vec, vec::Vec}; + use proptest::prelude::*; + #[cfg(feature = "std")] + use std::io::Cursor; + + #[test] + fn constants() { + assert_eq!(8, Igmpv1Header::LEN); + assert_eq!(0x11, IGMPV1_TYPE_MEMBERSHIP_QUERY); + assert_eq!(0x12, IGMPV1_TYPE_MEMBERSHIP_REPORT); + } + + proptest! { + #[test] + fn new(igmp_type in any::(), group_address in any::<[u8;4]>()) { + assert_eq!( + Igmpv1Header { + igmp_type, + reserved: 0, + checksum: 0, + group_address, + }, + Igmpv1Header::new(igmp_type, group_address) + ); + } + } + + proptest! { + #[test] + fn with_checksum(igmp_type in any::(), group_address in any::<[u8;4]>()) { + let header = Igmpv1Header::with_checksum(igmp_type, group_address); + assert_eq!(igmp_type, header.igmp_type); + assert_eq!(0, header.reserved); + assert_eq!(group_address, header.group_address); + assert_eq!(header.calc_checksum(), header.checksum); + } + } + + proptest! { + #[test] + fn from_slice( + igmp_type in any::(), + reserved in any::(), + checksum in any::(), + group_address in any::<[u8;4]>(), + suffix in proptest::collection::vec(any::(), 0..16) + ) { + let checksum_be = checksum.to_be_bytes(); + let mut bytes = vec![ + igmp_type, + reserved, + checksum_be[0], + checksum_be[1], + group_address[0], + group_address[1], + group_address[2], + group_address[3], + ]; + bytes.extend_from_slice(&suffix); + + let (actual, rest) = Igmpv1Header::from_slice(&bytes).unwrap(); + assert_eq!( + Igmpv1Header { + igmp_type, + reserved, + checksum, + group_address, + }, + actual + ); + assert_eq!(suffix.as_slice(), rest); + + for bad_len in 0..Igmpv1Header::LEN { + assert_eq!( + Igmpv1Header::from_slice(&bytes[..bad_len]), + Err(LenError{ + required_len: Igmpv1Header::LEN, + len: bad_len, + len_source: LenSource::Slice, + layer: Layer::Igmpv1, + layer_start_offset: 0, + }) + ); + } + } + } + + proptest! { + #[test] + fn from_bytes( + igmp_type in any::(), + reserved in any::(), + checksum in any::(), + group_address in any::<[u8;4]>(), + ) { + let checksum_be = checksum.to_be_bytes(); + let bytes = [ + igmp_type, + reserved, + checksum_be[0], + checksum_be[1], + group_address[0], + group_address[1], + group_address[2], + group_address[3], + ]; + + assert_eq!( + Igmpv1Header { + igmp_type, + reserved, + checksum, + group_address, + }, + Igmpv1Header::from_bytes(bytes) + ); + } + } + + proptest! { + #[test] + #[cfg(feature = "std")] + fn read( + igmp_type in any::(), + reserved in any::(), + checksum in any::(), + group_address in any::<[u8;4]>(), + suffix in proptest::collection::vec(any::(), 0..16) + ) { + let input = Igmpv1Header { + igmp_type, + reserved, + checksum, + group_address, + }; + let mut bytes = input.to_bytes().to_vec(); + bytes.extend_from_slice(&suffix); + + let mut cursor = Cursor::new(&bytes); + let actual = Igmpv1Header::read(&mut cursor).unwrap(); + assert_eq!(input, actual); + assert_eq!(Igmpv1Header::LEN as u64, cursor.position()); + + for bad_len in 0..Igmpv1Header::LEN { + let mut c = Cursor::new(&bytes[..bad_len]); + assert!(Igmpv1Header::read(&mut c).is_err()); + } + } + } + + proptest! { + #[test] + #[cfg(feature = "std")] + fn write( + igmp_type in any::(), + reserved in any::(), + checksum in any::(), + group_address in any::<[u8;4]>() + ) { + let input = Igmpv1Header { + igmp_type, + reserved, + checksum, + group_address, + }; + + let mut out = Vec::new(); + input.write(&mut out).unwrap(); + assert_eq!(input.to_bytes().as_slice(), out.as_slice()); + + for bad_len in 0..Igmpv1Header::LEN { + let mut buf = [0u8; Igmpv1Header::LEN]; + let mut c = Cursor::new(&mut buf[..bad_len]); + assert!(input.write(&mut c).is_err()); + } + } + } + + proptest! { + #[test] + fn header_len( + igmp_type in any::(), + reserved in any::(), + checksum in any::(), + group_address in any::<[u8;4]>() + ) { + let input = Igmpv1Header { + igmp_type, + reserved, + checksum, + group_address, + }; + assert_eq!(Igmpv1Header::LEN, input.header_len()); + } + } + + proptest! { + #[test] + fn calc_checksum( + igmp_type in any::(), + reserved in any::(), + checksum in any::(), + group_address in any::<[u8;4]>() + ) { + let input = Igmpv1Header { + igmp_type, + reserved, + checksum, + group_address, + }; + + let expected = checksum::Sum16BitWords::new() + .add_2bytes([igmp_type, reserved]) + .add_4bytes(group_address) + .ones_complement() + .to_be(); + assert_eq!(expected, input.calc_checksum()); + } + } + + proptest! { + #[test] + fn update_checksum( + igmp_type in any::(), + reserved in any::(), + checksum in any::(), + group_address in any::<[u8;4]>() + ) { + let mut input = Igmpv1Header { + igmp_type, + reserved, + checksum, + group_address, + }; + input.update_checksum(); + assert_eq!(input.calc_checksum(), input.checksum); + } + } + + proptest! { + #[test] + fn to_bytes( + igmp_type in any::(), + reserved in any::(), + checksum in any::(), + group_address in any::<[u8;4]>() + ) { + let input = Igmpv1Header { + igmp_type, + reserved, + checksum, + group_address, + }; + let checksum_be = checksum.to_be_bytes(); + assert_eq!( + [ + igmp_type, + reserved, + checksum_be[0], + checksum_be[1], + group_address[0], + group_address[1], + group_address[2], + group_address[3], + ], + input.to_bytes() + ); + } + } + + proptest! { + #[test] + fn clone_eq( + igmp_type in any::(), + reserved in any::(), + checksum in any::(), + group_address in any::<[u8;4]>() + ) { + let input = Igmpv1Header { + igmp_type, + reserved, + checksum, + group_address, + }; + assert_eq!(input, input.clone()); + } + } + + proptest! { + #[test] + fn debug( + igmp_type in any::(), + reserved in any::(), + checksum in any::(), + group_address in any::<[u8;4]>() + ) { + let input = Igmpv1Header { + igmp_type, + reserved, + checksum, + group_address, + }; + assert_eq!( + format!( + "Igmpv1Header {{ igmp_type: {}, reserved: {}, checksum: {}, group_address: {:?} }}", + igmp_type, + reserved, + checksum, + group_address, + ), + format!("{:?}", input) + ); + } + } +} diff --git a/etherparse/src/transport/mod.rs b/etherparse/src/transport/mod.rs index 45e36b1a..e04c99bc 100644 --- a/etherparse/src/transport/mod.rs +++ b/etherparse/src/transport/mod.rs @@ -25,6 +25,9 @@ pub use icmpv6_slice::*; mod icmpv6_type; pub use icmpv6_type::*; +mod igmpv1_header; +pub use igmpv1_header::*; + mod tcp_header; pub use tcp_header::*; @@ -66,3 +69,4 @@ pub use udp_header_slice::*; mod udp_slice; pub use udp_slice::*; + From 4e70a579f6f9c3820a64e5dd6192356dd4c5045f Mon Sep 17 00:00:00 2001 From: Julian Schmid Date: Sun, 26 Apr 2026 07:51:37 +0200 Subject: [PATCH 02/15] Add RFC1112 to README as reference --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 155dc9b4..fe402aea 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,7 @@ Read the documentations of the different methods for a more details: * [Arp hardware identifiers definitions](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/plain/include/uapi/linux/if_arp.h?id=e33c4963bf536900f917fb65a687724d5539bc21) on the Linux kernel * ["IEEE Standard for Local and metropolitan area networks-Media Access Control (MAC) Security," in IEEE Std 802.1AE-2018 (Revision of IEEE Std 802.1AE-2006) , vol., no., pp.1-239, 26 Dec. 2018, doi: 10.1109/IEEESTD.2018.8585421.](https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8585421&isnumber=8585420) * ["IEEE Standard for Local and metropolitan area networks--Media Access Control (MAC) Security Corrigendum 1: Tag Control Information Figure," in IEEE Std 802.1AE-2018/Cor 1-2020 (Corrigendum to IEEE Std 802.1AE-2018) , vol., no., pp.1-14, 21 July 2020, doi: 10.1109/IEEESTD.2020.9144679.](https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=9144679&isnumber=9144678) +* Host Extensions for IP Multicasting (IGMPv1) [RFC 1112](https://tools.ietf.org/html/rfc1112) ## License Licensed under either of Apache License, Version 2.0 or MIT license at your option. The corresponding license texts can be found in the LICENSE-APACHE file and the LICENSE-MIT file. From 098f36a7f5fe335883b791b837364c470de738a2 Mon Sep 17 00:00:00 2001 From: Julian Schmid Date: Sun, 26 Apr 2026 07:53:23 +0200 Subject: [PATCH 03/15] Add RFC 1112 to crate doc index --- etherparse/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/etherparse/src/lib.rs b/etherparse/src/lib.rs index e1860b73..144c8e7b 100644 --- a/etherparse/src/lib.rs +++ b/etherparse/src/lib.rs @@ -281,7 +281,8 @@ //! * [Arp hardware identifiers definitions](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/plain/include/uapi/linux/if_arp.h?id=e33c4963bf536900f917fb65a687724d5539bc21) on the Linux kernel //! * ["IEEE Standard for Local and metropolitan area networks-Media Access Control (MAC) Security," in IEEE Std 802.1AE-2018 (Revision of IEEE Std 802.1AE-2006) , vol., no., pp.1-239, 26 Dec. 2018, doi: 10.1109/IEEESTD.2018.8585421.](https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8585421&isnumber=8585420) //! * ["IEEE Standard for Local and metropolitan area networks--Media Access Control (MAC) Security Corrigendum 1: Tag Control Information Figure," in IEEE Std 802.1AE-2018/Cor 1-2020 (Corrigendum to IEEE Std 802.1AE-2018) , vol., no., pp.1-14, 21 July 2020, doi: 10.1109/IEEESTD.2020.9144679.](https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=9144679&isnumber=9144678) - +//! * * Host Extensions for IP Multicasting (IGMPv1) [RFC 1112](https://tools.ietf.org/html/rfc1112) +// // # Reason for 'bool_comparison' disable: // // Clippy triggers triggers errors like the following if the warning stays enabled: From ae32a8bf04d2ccb724301abe444f809b5285a0f9 Mon Sep 17 00:00:00 2001 From: Julian Schmid Date: Fri, 1 May 2026 19:53:04 +0200 Subject: [PATCH 04/15] Reworked IGMP header to support multiple IGMP versions --- etherparse/src/err/igmp/header_error.rs | 76 ++ etherparse/src/err/igmp/header_slice_error.rs | 142 +++ etherparse/src/err/igmp/mod.rs | 5 + etherparse/src/err/layer.rs | 12 +- etherparse/src/err/mod.rs | 1 + etherparse/src/err/value_type.rs | 7 + .../src/transport/igmp/group_address.rs | 102 ++ .../src/transport/igmp/leave_group_type.rs | 23 + .../src/transport/igmp/max_response_code.rs | 60 + .../transport/igmp/membership_query_type.rs | 40 + .../membership_query_with_sources_header.rs | 57 + .../igmp/membership_report_v1_type.rs | 23 + .../igmp/membership_report_v2_type.rs | 22 + .../igmp/membership_report_v3_header.rs | 43 + etherparse/src/transport/igmp/mod.rs | 55 + etherparse/src/transport/igmp/qrv.rs | 270 +++++ etherparse/src/transport/igmp_header.rs | 1030 +++++++++++++++++ etherparse/src/transport/igmp_type.rs | 23 + etherparse/src/transport/igmpv1_header.rs | 457 -------- etherparse/src/transport/mod.rs | 11 +- 20 files changed, 1993 insertions(+), 466 deletions(-) create mode 100644 etherparse/src/err/igmp/header_error.rs create mode 100644 etherparse/src/err/igmp/header_slice_error.rs create mode 100644 etherparse/src/err/igmp/mod.rs create mode 100644 etherparse/src/transport/igmp/group_address.rs create mode 100644 etherparse/src/transport/igmp/leave_group_type.rs create mode 100644 etherparse/src/transport/igmp/max_response_code.rs create mode 100644 etherparse/src/transport/igmp/membership_query_type.rs create mode 100644 etherparse/src/transport/igmp/membership_query_with_sources_header.rs create mode 100644 etherparse/src/transport/igmp/membership_report_v1_type.rs create mode 100644 etherparse/src/transport/igmp/membership_report_v2_type.rs create mode 100644 etherparse/src/transport/igmp/membership_report_v3_header.rs create mode 100644 etherparse/src/transport/igmp/mod.rs create mode 100644 etherparse/src/transport/igmp/qrv.rs create mode 100644 etherparse/src/transport/igmp_header.rs create mode 100644 etherparse/src/transport/igmp_type.rs delete mode 100644 etherparse/src/transport/igmpv1_header.rs diff --git a/etherparse/src/err/igmp/header_error.rs b/etherparse/src/err/igmp/header_error.rs new file mode 100644 index 00000000..2f64b2b1 --- /dev/null +++ b/etherparse/src/err/igmp/header_error.rs @@ -0,0 +1,76 @@ +/// Errors that can be encountered while decoding an IGMP header. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum HeaderError { + /// Error when the IGMP type byte does not match any of the message + /// types defined in RFC 1112, RFC 2236 or RFC 3376 + /// (`0x11`, `0x12`, `0x16`, `0x17`, `0x22`). + UnknownType { type_u8: u8 }, +} + +impl core::fmt::Display for HeaderError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + use HeaderError::*; + match self { + UnknownType { type_u8 } => write!( + f, + "IGMP Header Error: Unknown IGMP message type {type_u8:#04x}. Expected one of 0x11 (Membership Query), 0x12 (IGMPv1 Membership Report), 0x16 (IGMPv2 Membership Report), 0x17 (IGMPv2 Leave Group) or 0x22 (IGMPv3 Membership Report)." + ), + } + } +} + +impl core::error::Error for HeaderError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + None + } +} + +#[cfg(test)] +mod tests { + use super::HeaderError::*; + use alloc::format; + use std::{ + collections::hash_map::DefaultHasher, + error::Error, + hash::{Hash, Hasher}, + }; + + #[test] + fn debug() { + assert_eq!( + "UnknownType { type_u8: 255 }", + format!("{:?}", UnknownType { type_u8: 0xff }) + ); + } + + #[test] + fn clone_eq_hash() { + let err = UnknownType { type_u8: 0xff }; + assert_eq!(err, err.clone()); + let hash_a = { + let mut hasher = DefaultHasher::new(); + err.hash(&mut hasher); + hasher.finish() + }; + let hash_b = { + let mut hasher = DefaultHasher::new(); + err.clone().hash(&mut hasher); + hasher.finish() + }; + assert_eq!(hash_a, hash_b); + } + + #[test] + fn fmt() { + assert_eq!( + "IGMP Header Error: Unknown IGMP message type 0xff. Expected one of 0x11 (Membership Query), 0x12 (IGMPv1 Membership Report), 0x16 (IGMPv2 Membership Report), 0x17 (IGMPv2 Leave Group) or 0x22 (IGMPv3 Membership Report).", + format!("{}", UnknownType { type_u8: 0xff }) + ); + } + + #[cfg(feature = "std")] + #[test] + fn source() { + assert!(UnknownType { type_u8: 0xff }.source().is_none()); + } +} diff --git a/etherparse/src/err/igmp/header_slice_error.rs b/etherparse/src/err/igmp/header_slice_error.rs new file mode 100644 index 00000000..897f293b --- /dev/null +++ b/etherparse/src/err/igmp/header_slice_error.rs @@ -0,0 +1,142 @@ +use super::HeaderError; +use crate::err::LenError; + +/// Error when decoding an [`crate::IgmpHeader`] from a slice. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum HeaderSliceError { + /// Error when not enough data is present in the slice to decode an + /// IGMP header. + Len(LenError), + + /// Error caused by the contents of the header. + Content(HeaderError), +} + +impl HeaderSliceError { + /// Adds an offset value to all slice length related fields. + #[inline] + pub const fn add_slice_offset(self, offset: usize) -> Self { + use HeaderSliceError::*; + match self { + Len(err) => Len(err.add_offset(offset)), + Content(err) => Content(err), + } + } +} + +impl core::fmt::Display for HeaderSliceError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + use HeaderSliceError::*; + match self { + Len(err) => err.fmt(f), + Content(err) => err.fmt(f), + } + } +} + +impl core::error::Error for HeaderSliceError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + HeaderSliceError::Len(err) => Some(err), + HeaderSliceError::Content(err) => Some(err), + } + } +} + +#[cfg(test)] +mod tests { + use super::{HeaderSliceError::*, *}; + use crate::{err::Layer, LenSource}; + use alloc::format; + use std::{ + collections::hash_map::DefaultHasher, + error::Error, + hash::{Hash, Hasher}, + }; + + #[test] + fn add_slice_offset() { + assert_eq!( + Len(LenError { + required_len: 1, + layer: Layer::Igmp, + len: 2, + len_source: LenSource::Slice, + layer_start_offset: 3 + }) + .add_slice_offset(200), + Len(LenError { + required_len: 1, + layer: Layer::Igmp, + len: 2, + len_source: LenSource::Slice, + layer_start_offset: 203 + }) + ); + assert_eq!( + Content(HeaderError::UnknownType { type_u8: 0xff }).add_slice_offset(200), + Content(HeaderError::UnknownType { type_u8: 0xff }) + ); + } + + #[test] + fn debug() { + let err = HeaderError::UnknownType { type_u8: 0xff }; + assert_eq!( + format!("Content({:?})", err.clone()), + format!("{:?}", Content(err)) + ); + } + + #[test] + fn clone_eq_hash() { + let err = Content(HeaderError::UnknownType { type_u8: 0xff }); + assert_eq!(err, err.clone()); + let hash_a = { + let mut hasher = DefaultHasher::new(); + err.hash(&mut hasher); + hasher.finish() + }; + let hash_b = { + let mut hasher = DefaultHasher::new(); + err.clone().hash(&mut hasher); + hasher.finish() + }; + assert_eq!(hash_a, hash_b); + } + + #[test] + fn fmt() { + { + let err = LenError { + required_len: 1, + layer: Layer::Igmp, + len: 2, + len_source: LenSource::Slice, + layer_start_offset: 3, + }; + assert_eq!(format!("{}", &err), format!("{}", Len(err))); + } + { + let err = HeaderError::UnknownType { type_u8: 0xff }; + assert_eq!(format!("{}", &err), format!("{}", Content(err.clone()))); + } + } + + #[cfg(feature = "std")] + #[test] + fn source() { + assert!(Len(LenError { + required_len: 1, + layer: Layer::Igmp, + len: 2, + len_source: LenSource::Slice, + layer_start_offset: 3 + }) + .source() + .is_some()); + assert!(Content(HeaderError::UnknownType { type_u8: 0xff }) + .source() + .is_some()); + } +} diff --git a/etherparse/src/err/igmp/mod.rs b/etherparse/src/err/igmp/mod.rs new file mode 100644 index 00000000..6c917fd7 --- /dev/null +++ b/etherparse/src/err/igmp/mod.rs @@ -0,0 +1,5 @@ +mod header_error; +pub use header_error::*; + +mod header_slice_error; +pub use header_slice_error::*; diff --git a/etherparse/src/err/layer.rs b/etherparse/src/err/layer.rs index 7bf4f331..44767bdf 100644 --- a/etherparse/src/err/layer.rs +++ b/etherparse/src/err/layer.rs @@ -50,8 +50,8 @@ pub enum Layer { Icmpv4TimestampReply, /// Error occurred while parsing an ICMPv6 packet. Icmpv6, - /// Error occurred while parsing an IGMPv1 packet. - Igmpv1, + /// Error occurred while parsing an IGMP packet. + Igmp, /// Error occurred while parsing an Address Resolution Protocol packet. Arp, } @@ -85,7 +85,7 @@ impl Layer { Icmpv4Timestamp => "ICMP Timestamp Error", Icmpv4TimestampReply => "ICMP Timestamp Reply Error", Icmpv6 => "ICMPv6 Packet Error", - Igmpv1 => "IGMPv1 Packet Error", + Igmp => "IGMP Packet Error", Arp => "Address Resolution Protocol Packet Error", } } @@ -119,7 +119,7 @@ impl core::fmt::Display for Layer { Icmpv4Timestamp => write!(f, "ICMP timestamp message"), Icmpv4TimestampReply => write!(f, "ICMP timestamp reply message"), Icmpv6 => write!(f, "ICMPv6 packet"), - Igmpv1 => write!(f, "IGMPv1 packet"), + Igmp => write!(f, "IGMP packet"), Arp => write!(f, "Address Resolution Protocol packet"), } } @@ -189,7 +189,7 @@ mod test { (Icmpv4Timestamp, "ICMP Timestamp Error"), (Icmpv4TimestampReply, "ICMP Timestamp Reply Error"), (Icmpv6, "ICMPv6 Packet Error"), - (Igmpv1, "IGMPv1 Packet Error"), + (Igmp, "IGMP Packet Error"), (Arp, "Address Resolution Protocol Packet Error"), ]; for test in tests { @@ -224,7 +224,7 @@ mod test { (Icmpv4Timestamp, "ICMP timestamp message"), (Icmpv4TimestampReply, "ICMP timestamp reply message"), (Icmpv6, "ICMPv6 packet"), - (Igmpv1, "IGMPv1 packet"), + (Igmp, "IGMP packet"), (Arp, "Address Resolution Protocol packet"), ]; for test in tests { diff --git a/etherparse/src/err/mod.rs b/etherparse/src/err/mod.rs index c955625c..b0752827 100644 --- a/etherparse/src/err/mod.rs +++ b/etherparse/src/err/mod.rs @@ -1,4 +1,5 @@ pub mod arp; +pub mod igmp; #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub mod io; diff --git a/etherparse/src/err/value_type.rs b/etherparse/src/err/value_type.rs index 15bfb372..27d716d3 100644 --- a/etherparse/src/err/value_type.rs +++ b/etherparse/src/err/value_type.rs @@ -43,6 +43,8 @@ pub enum ValueType { Icmpv6PayloadLength, /// Packet type of a Linux Cooked Capture v1 (SLL) LinuxSllType, + /// QRV (Querier's Robustness Variable) of a IGMPv3 membership query message. + IgmpQrv, } impl core::fmt::Display for ValueType { @@ -65,6 +67,7 @@ impl core::fmt::Display for ValueType { TcpPayloadLengthIpv6 => write!(f, "TCP Payload Length (in IPv6 checksum calculation)"), Icmpv6PayloadLength => write!(f, "ICMPv6 Payload Length"), LinuxSllType => write!(f, "Linux Cooked Capture v1 (SLL)"), + IgmpQrv => write!(f, "IGMPv3 QRV (Querier's Robustness Variable)"), } } } @@ -134,5 +137,9 @@ mod test { &format!("{}", TcpPayloadLengthIpv6) ); assert_eq!("ICMPv6 Payload Length", &format!("{}", Icmpv6PayloadLength)); + assert_eq!( + "IGMPv3 QRV (Querier's Robustness Variable)", + &format!("{}", IgmpQrv) + ); } } diff --git a/etherparse/src/transport/igmp/group_address.rs b/etherparse/src/transport/igmp/group_address.rs new file mode 100644 index 00000000..0634145d --- /dev/null +++ b/etherparse/src/transport/igmp/group_address.rs @@ -0,0 +1,102 @@ +/// A group address in an IGMP packet. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct GroupAddress { + pub octets: [u8; 4], +} + +impl GroupAddress { + pub fn new(address: [u8; 4]) -> Self { + Self { octets: address } + } + + pub fn is_zero(&self) -> bool { + [0, 0, 0, 0] == self.octets + } +} + +impl From for [u8; 4] { + fn from(value: GroupAddress) -> Self { + value.octets + } +} + +impl From<[u8; 4]> for GroupAddress { + fn from(value: [u8; 4]) -> Self { + GroupAddress { octets: value } + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +#[cfg(feature = "std")] +impl From for GroupAddress { + fn from(value: std::net::Ipv4Addr) -> Self { + GroupAddress { + octets: value.octets(), + } + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +#[cfg(feature = "std")] +impl From for std::net::Ipv4Addr { + fn from(value: GroupAddress) -> Self { + std::net::Ipv4Addr::new( + value.octets[0], + value.octets[1], + value.octets[2], + value.octets[3], + ) + } +} + +#[cfg(test)] +mod test { + use super::*; + use alloc::format; + use proptest::prelude::*; + + #[test] + fn test_is_zero() { + assert!(GroupAddress::new([0, 0, 0, 0]).is_zero()); + assert!(!GroupAddress::new([1, 0, 0, 0]).is_zero()); + assert!(!GroupAddress::new([0, 1, 0, 0]).is_zero()); + assert!(!GroupAddress::new([0, 0, 1, 0]).is_zero()); + assert!(!GroupAddress::new([0, 0, 0, 1]).is_zero()); + } + + proptest! { + #[test] + fn from_array_to_group_address_roundtrip(octets in any::<[u8;4]>()) { + let addr = GroupAddress::from(octets); + prop_assert_eq!(addr.octets, octets); + + let back: [u8;4] = addr.into(); + prop_assert_eq!(back, octets); + } + } + + proptest! { + #[test] + fn from_group_address_to_array_roundtrip(octets in any::<[u8;4]>()) { + let addr = GroupAddress { octets }; + let arr: [u8;4] = addr.into(); + prop_assert_eq!(arr, octets); + + let back = GroupAddress::from(arr); + prop_assert_eq!(back, addr); + } + } + + #[cfg(feature = "std")] + proptest! { + #[test] + fn from_ipv4addr_to_group_address_roundtrip(octets in any::<[u8;4]>()) { + let ip = std::net::Ipv4Addr::from(octets); + let addr = GroupAddress::from(ip); + prop_assert_eq!(addr.octets, octets); + + let back: std::net::Ipv4Addr = addr.into(); + prop_assert_eq!(back.octets(), octets); + } + } +} diff --git a/etherparse/src/transport/igmp/leave_group_type.rs b/etherparse/src/transport/igmp/leave_group_type.rs new file mode 100644 index 00000000..94d73df4 --- /dev/null +++ b/etherparse/src/transport/igmp/leave_group_type.rs @@ -0,0 +1,23 @@ +use crate::igmp::GroupAddress; + +/// A leave group message type (introduced in IGMPv2). +/// +/// ```text +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Type = 0x11 | 0 | Checksum | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Group Address | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct LeaveGroupType { + /// The IP multicast group address of the group being left. + pub group_address: GroupAddress, +} + +impl LeaveGroupType { + /// Number of bytes/octets an [`MembershipQueryV2`] takes up in serialized form. + pub const LEN: usize = 8; +} diff --git a/etherparse/src/transport/igmp/max_response_code.rs b/etherparse/src/transport/igmp/max_response_code.rs new file mode 100644 index 00000000..d363d7ea --- /dev/null +++ b/etherparse/src/transport/igmp/max_response_code.rs @@ -0,0 +1,60 @@ +/// Max response code (specifies the maximum time allowed before +/// sending a responding report in ICMPv3). +/// +/// The actual time allowed, called the Max +/// Resp Time, is represented in units of 1/10 second and is derived from +/// the Max Resp Code as follows: +/// +/// If Max Resp Code < 128, Max Resp Time = Max Resp Code +/// +/// If Max Resp Code >= 128, Max Resp Code represents a floating-point +/// value as follows: +/// +/// ```text +/// 0 1 2 3 4 5 6 7 +/// +-+-+-+-+-+-+-+-+ +/// |1| exp | mant | +/// +-+-+-+-+-+-+-+-+ +/// ``` +/// +/// Max Resp Time = (mant | 0x10) << (exp + 3) +/// +/// Small values of Max Resp Time allow IGMPv3 routers to tune the "leave +/// latency" (the time between the moment the last host leaves a group +/// and the moment the routing protocol is notified that there are no +/// more members). Larger values, especially in the exponential range, +/// allow tuning of the burstiness of IGMP traffic on a network. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct MaxResponseCode(pub u8); + +impl MaxResponseCode { + /// Returns the max response time in 10th seconds (converts raw value). + pub fn as_10th_secs(&self) -> u16 { + if 0 != self.0 & 0b1000_0000 { + u16::from((self.0 & 0b0000_1111) | 0x10) << u16::from(((self.0 & 0b0111_0000) >> 4) + 3) + } else { + u16::from(self.0) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use proptest::prelude::*; + use std::format; + + proptest! { + #[test] + fn as_10th_secs_linear_range(raw in 0u8..=0b0111_1111u8) { + prop_assert_eq!(MaxResponseCode(raw).as_10th_secs(), u16::from(raw)); + } + + #[test] + fn as_10th_secs_exponential_range(mant in 0u8..=0b1111, exp in 0u8..=0b111u8) { + let raw = 0b1000_0000 | (exp << 4) | mant; + let expected = u16::from(mant | 0x10) << u16::from(exp + 3); + prop_assert_eq!(MaxResponseCode(raw).as_10th_secs(), expected); + } + } +} diff --git a/etherparse/src/transport/igmp/membership_query_type.rs b/etherparse/src/transport/igmp/membership_query_type.rs new file mode 100644 index 00000000..3476d9c4 --- /dev/null +++ b/etherparse/src/transport/igmp/membership_query_type.rs @@ -0,0 +1,40 @@ +use crate::igmp::GroupAddress; + +/// A membership query message type (present in IGMPv1 and IGMPv2, but +/// the values are only filled for IGMPv2 with non zero values). +/// +/// ```text +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Type = 0x11 | Max Resp Time | Checksum | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Group Address | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct MembershipQueryType { + /// The maximum response time for the membership report + /// (only for IGMPv2, set to 0 for IGMPv1). + /// + /// Specifies the maximum allowed time before sending a + /// responding report in units of 1/10 second. + /// + /// For IGMPv1, this field is always set to zero. + pub max_response_time: u8, + + /// The group address being queried. + /// + /// Set to zero for general queries, to learn which groups + /// have members on an attached network. Filled for group-specific + /// queries to learn if a particular group has members on an + /// attached network. + /// + /// For IGMPv1, this field is always set to zero. + pub group_address: GroupAddress, +} + +impl MembershipQueryType { + /// Number of bytes/octets an [`MembershipQueryV2`] takes up in serialized form. + pub const LEN: usize = 8; +} diff --git a/etherparse/src/transport/igmp/membership_query_with_sources_header.rs b/etherparse/src/transport/igmp/membership_query_with_sources_header.rs new file mode 100644 index 00000000..ebc482a5 --- /dev/null +++ b/etherparse/src/transport/igmp/membership_query_with_sources_header.rs @@ -0,0 +1,57 @@ +use crate::igmp::{GroupAddress, MaxResponseCode, Qrv}; + +/// A membership report message type (IGMPv3 version) with source addresses. +/// +/// ```text +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// | Type = 0x11 | Max Resp Code | Checksum | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | part of header and +/// | Group Address | | this type +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +/// | Resv |S| QRV | QQIC | Number of Sources (N) | ↓ +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// | Source Address [1] | | +/// +- -+ | +/// | Source Address [2] | | +/// +- . -+ | part of payload +/// . . . | +/// . . . | +/// +- -+ | +/// | Source Address [N] | ↓ +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct MembershipQueryWithSourcesHeader { + /// The Max Resp Code field specifies the maximum time allowed before + /// sending a responding report. + pub max_response_code: MaxResponseCode, + + /// The group address being queried. + /// + /// Set to zero for general queries, to learn which groups + /// have members on an attached network. Filled for group-specific + /// queries to learn if a particular group has members on an + /// attached network. + /// + /// For IGMPv1, this field is always set to zero. + pub group_address: GroupAddress, + + /// S Flag (Suppress Router-Side Processing). + pub s_flag: bool, + + /// QRV (Querier's Robustness Variable) + pub qrv: Qrv, + + /// QQIC (Querier's Query Interval Code) + pub qqic: u8, + + /// Number of sources + pub num_of_sources: u16, +} + +impl MembershipQueryWithSourcesHeader { + /// Number of bytes/octets an [`MembershipReportV1`] takes up in serialized form. + pub const LEN: usize = 12; +} diff --git a/etherparse/src/transport/igmp/membership_report_v1_type.rs b/etherparse/src/transport/igmp/membership_report_v1_type.rs new file mode 100644 index 00000000..07e9bf3d --- /dev/null +++ b/etherparse/src/transport/igmp/membership_report_v1_type.rs @@ -0,0 +1,23 @@ +use crate::igmp::GroupAddress; + +/// IGMPv1 Membership Report Message. +/// +/// ```text +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Type = 0x12 | Unused | Checksum | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Group Address | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct MembershipReportV1Type { + /// IP multicast group address of the group being reported + pub group_address: GroupAddress, +} + +impl MembershipReportV1Type { + /// Number of bytes/octets an [`MembershipReportV1`] takes up in serialized form. + pub const LEN: usize = 8; +} diff --git a/etherparse/src/transport/igmp/membership_report_v2_type.rs b/etherparse/src/transport/igmp/membership_report_v2_type.rs new file mode 100644 index 00000000..ac58835d --- /dev/null +++ b/etherparse/src/transport/igmp/membership_report_v2_type.rs @@ -0,0 +1,22 @@ +use crate::igmp::GroupAddress; +/// IGMPv2 Membership Report Message. +/// +/// ```text +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Type = 0x16 | Max Resp Time | Checksum | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Group Address | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct MembershipReportV2Type { + /// IP multicast group address of the group being reported + pub group_address: GroupAddress, +} + +impl MembershipReportV2Type { + /// Number of bytes/octets an [`MembershipReportV2`] takes up in serialized form. + pub const LEN: usize = 8; +} diff --git a/etherparse/src/transport/igmp/membership_report_v3_header.rs b/etherparse/src/transport/igmp/membership_report_v3_header.rs new file mode 100644 index 00000000..f22c7c0a --- /dev/null +++ b/etherparse/src/transport/igmp/membership_report_v3_header.rs @@ -0,0 +1,43 @@ +/// IGMPv3 Membership Report Message header part (without checksum). +/// +/// ```text +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// | Type = 0x22 | Reserved | Checksum | | part of header & +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | this type +/// | Reserved | Number of Group Records (M) | ↓ +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// | | | +/// . . | +/// . Group Record [1] . | +/// . . | +/// | | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +/// | | | +/// . . | +/// . Group Record [2] . | part of payload +/// . . | +/// | | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +/// | . | | +/// . . . | +/// | . | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +/// | | | +/// . . | +/// . Group Record [M] . | +/// . . | +/// | | ↓ +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct MembershipReportV3Header { + /// The number of records in the membership report. + pub num_of_records: u16, +} + +impl MembershipReportV3Header { + /// Number of bytes/octets an [`MembershipReportV3Header`] takes up in serialized form. + pub const LEN: usize = 8; +} diff --git a/etherparse/src/transport/igmp/mod.rs b/etherparse/src/transport/igmp/mod.rs new file mode 100644 index 00000000..63aa2175 --- /dev/null +++ b/etherparse/src/transport/igmp/mod.rs @@ -0,0 +1,55 @@ +mod group_address; +pub use group_address::*; + +mod leave_group_type; +pub use leave_group_type::*; + +mod max_response_code; +pub use max_response_code::*; + +mod membership_query_type; +pub use membership_query_type::*; + +mod membership_query_with_sources_header; +pub use membership_query_with_sources_header::*; + +mod membership_report_v1_type; +pub use membership_report_v1_type::*; + +mod membership_report_v2_type; +pub use membership_report_v2_type::*; + +mod membership_report_v3_header; +pub use membership_report_v3_header::*; + +mod qrv; +pub use qrv::*; + +/// "Membership Query" message type (same in IGMPv1, IGMPv2, IGMPv3). +pub const IGMP_TYPE_MEMBERSHIP_QUERY: u8 = 0x11; + +/// IGMPv1 "Membership Report" message type. +pub const IGMPV1_TYPE_MEMBERSHIP_REPORT: u8 = 0x12; + +/// IGMPv2 "Membership Report" message type. +pub const IGMPV2_TYPE_MEMBERSHIP_REPORT: u8 = 0x16; + +/// IGMPv3 "Membership Report" message type. +pub const IGMPV3_TYPE_MEMBERSHIP_REPORT: u8 = 0x22; + +/// IGMPv2 "Leave Group" message type. +pub const IGMPV2_TYPE_LEAVE_GROUP: u8 = 0x17; + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn constants() { + assert_eq!(0x11, IGMP_TYPE_MEMBERSHIP_QUERY); + assert_eq!(0x12, IGMPV1_TYPE_MEMBERSHIP_REPORT); + assert_eq!(0x16, IGMPV2_TYPE_MEMBERSHIP_REPORT); + assert_eq!(0x17, IGMPV2_TYPE_LEAVE_GROUP); + assert_eq!(0x22, IGMPV3_TYPE_MEMBERSHIP_REPORT); + } +} diff --git a/etherparse/src/transport/igmp/qrv.rs b/etherparse/src/transport/igmp/qrv.rs new file mode 100644 index 00000000..5cc730a3 --- /dev/null +++ b/etherparse/src/transport/igmp/qrv.rs @@ -0,0 +1,270 @@ +use crate::err::ValueTooBigError; + +/// 3 bit unsigned integer containing the "Querier's Robustness Variable" +/// (present in the [`crate::igmp::MembershipQueryV3`]. +/// +/// Established in +/// [RFC-3376](https://datatracker.ietf.org/doc/html/rfc3376). +#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Qrv(u8); + +impl Qrv { + /// QRV with value 0. + pub const ZERO: Qrv = Qrv(0); + + /// Maximum value of an IGMPv3 Membership Query QRV. + pub const MAX_U8: u8 = 0b0000_0111; + + /// Maximum value of DSCP field (6 bits). + pub const MAX: Qrv = Qrv(Self::MAX_U8); + + /// Static array with all possible values. + pub const VALUES: [Qrv; 8] = [ + Qrv(0b000), + Qrv(0b001), + Qrv(0b010), + Qrv(0b011), + Qrv(0b100), + Qrv(0b101), + Qrv(0b110), + Qrv(0b111), + ]; + + /// Tries to create an [`Qrv`] and checks that the passed value + /// is smaller or equal than [`Qrv::MAX_U8`] (3 bit unsigned integer). + /// + /// In case the passed value is bigger then what can be represented in an 3 bit + /// integer an error is returned. Otherwise an `Ok` containing the [`Qrv`]. + /// + /// ``` + /// use etherparse::igmp::Qrv; + /// + /// let dscp = Qrv::try_new(3).unwrap(); + /// assert_eq!(dscp.value(), 3); + /// + /// // if a number that can not be represented in an 3 bit integer + /// // gets passed in an error is returned + /// use etherparse::err::{ValueTooBigError, ValueType}; + /// assert_eq!( + /// Qrv::try_new(Qrv::MAX_U8 + 1), + /// Err(ValueTooBigError{ + /// actual: Qrv::MAX_U8 + 1, + /// max_allowed: Qrv::MAX_U8, + /// value_type: ValueType::IgmpQrv, + /// }) + /// ); + /// ``` + #[inline] + pub const fn try_new(value: u8) -> Result> { + use crate::err::ValueType; + if value <= Self::MAX_U8 { + Ok(Qrv(value)) + } else { + Err(ValueTooBigError { + actual: value, + max_allowed: Qrv::MAX_U8, + value_type: ValueType::IgmpQrv, + }) + } + } + + /// Creates an [`Qrv`] without checking that the value + /// is smaller or equal than [`Qrv::MAX_U8`] (3 bit unsigned integer). + /// The caller must guarantee that `value <= Qrv::MAX_U8`. + /// + /// # Safety + /// + /// `value` must be smaller or equal than [`Qrv::MAX_U8`] + /// otherwise the behavior of functions or data structures relying + /// on this pre-requirement is undefined. + #[inline] + pub const unsafe fn new_unchecked(value: u8) -> Qrv { + debug_assert!(value <= Qrv::MAX_U8); + Qrv(value) + } + + /// Returns the underlying unsigned 3 bit value as an `u8` value. + #[inline] + pub const fn value(self) -> u8 { + self.0 + } +} + +impl core::fmt::Display for Qrv { + #[inline] + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.fmt(f) + } +} + +impl From for u8 { + #[inline] + fn from(value: Qrv) -> Self { + value.0 + } +} + +impl TryFrom for Qrv { + type Error = ValueTooBigError; + + #[inline] + fn try_from(value: u8) -> Result { + use crate::err::ValueType; + if value <= Qrv::MAX_U8 { + Ok(Qrv(value)) + } else { + Err(Self::Error { + actual: value, + max_allowed: Qrv::MAX_U8, + value_type: ValueType::IgmpQrv, + }) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use core::hash::{Hash, Hasher}; + use proptest::prelude::*; + use std::format; + + #[test] + fn derived_traits() { + // copy & clone + { + let a = Qrv(32); + let b = a; + assert_eq!(a, b); + assert_eq!(a.clone(), a); + } + + // default + { + let actual: Qrv = Default::default(); + assert_eq!(actual.value(), 0); + } + + // debug + { + let a = Qrv(32); + assert_eq!(format!("{:?}", a), format!("Qrv(32)")); + } + + // ord & partial ord + { + use core::cmp::Ordering; + let a = Qrv(32); + let b = a; + assert_eq!(a.cmp(&b), Ordering::Equal); + assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal)); + } + + // hash + { + use std::collections::hash_map::DefaultHasher; + let a = { + let mut hasher = DefaultHasher::new(); + Qrv(64).hash(&mut hasher); + hasher.finish() + }; + let b = { + let mut hasher = DefaultHasher::new(); + Qrv(64).hash(&mut hasher); + hasher.finish() + }; + assert_eq!(a, b); + } + } + + proptest! { + #[test] + fn try_new( + valid_value in 0..=0b0000_0111u8, + invalid_value in 0b0000_1000u8..=u8::MAX + ) { + use crate::err::{ValueType, ValueTooBigError}; + assert_eq!( + valid_value, + Qrv::try_new(valid_value).unwrap().value() + ); + assert_eq!( + Qrv::try_new(invalid_value).unwrap_err(), + ValueTooBigError{ + actual: invalid_value, + max_allowed: 0b0000_0111, + value_type: ValueType::IgmpQrv + } + ); + } + } + + proptest! { + #[test] + fn try_from( + valid_value in 0..=0b0000_0111u8, + invalid_value in 0b0000_1000u8..=u8::MAX + ) { + use crate::err::{ValueType, ValueTooBigError}; + // try_into + { + let actual: Qrv = valid_value.try_into().unwrap(); + assert_eq!(actual.value(), valid_value); + + let err: Result> = invalid_value.try_into(); + assert_eq!( + err.unwrap_err(), + ValueTooBigError{ + actual: invalid_value, + max_allowed: 0b0000_0111, + value_type: ValueType::IgmpQrv + } + ); + } + // try_from + { + assert_eq!( + Qrv::try_from(valid_value).unwrap().value(), + valid_value + ); + + assert_eq!( + Qrv::try_from(invalid_value).unwrap_err(), + ValueTooBigError{ + actual: invalid_value, + max_allowed: 0b0000_0111, + value_type: ValueType::IgmpQrv + } + ); + } + } + } + + proptest! { + #[test] + fn new_unchecked(valid_value in 0..=0b0000_0111u8) { + assert_eq!( + valid_value, + unsafe { + Qrv::new_unchecked(valid_value).value() + } + ); + } + } + + proptest! { + #[test] + fn fmt(valid_value in 0..=0b0000_0111u8) { + assert_eq!(format!("{}", Qrv(valid_value)), format!("{}", valid_value)); + } + } + + proptest! { + #[test] + fn from(valid_value in 0..=0b0000_0111u8) { + let dscp = Qrv::try_new(valid_value).unwrap(); + let actual: u8 = dscp.into(); + assert_eq!(actual, valid_value); + } + } +} diff --git a/etherparse/src/transport/igmp_header.rs b/etherparse/src/transport/igmp_header.rs new file mode 100644 index 00000000..6aafe2b9 --- /dev/null +++ b/etherparse/src/transport/igmp_header.rs @@ -0,0 +1,1030 @@ +use arrayvec::ArrayVec; + +use crate::{ + err::{igmp::HeaderSliceError, LenError}, + *, +}; + +/// A header of an IGMP packet. +/// +/// The header contains the static part of an IGMP +/// packet. +/// +/// The following packet types the header contains all data: +/// +/// - IGMP v1 & v2 membership query ([`crate::IgmpType::MembershipQuery`]) +/// - IGMP v1 membership report ([`crate::IgmpType::MembershipReportV1`]) +/// - IGMP v2 membership report ([`crate::IgmpType::MembershipReportV2`]) +/// - IGMP v2 & v3 leave group ([`crate::IgmpType::LeaveGroup`]) +/// +/// +/// and for the followng messages only the static part is contained +/// within the header: +/// +/// - IGMPv3 membership query ([`crate::IgmpType::MembershipQuery`]): +/// ```text +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// | Type = 0x11 | Max Resp Code | Checksum | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | part of header and +/// | Group Address | | this type +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +/// | Resv |S| QRV | QQIC | Number of Sources (N) | ↓ +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// | Source Address [1] | | +/// +- -+ | +/// | Source Address [2] | | +/// +- . -+ | part of payload +/// . . . | +/// . . . | +/// +- -+ | +/// | Source Address [N] | ↓ +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// ``` +/// - IGMPv3 membership report ([`crate::IgmpType::MembershipReportV3`]): +/// ```text +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// | Type = 0x11 | Max Resp Code | Checksum | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | part of header and +/// | Group Address | | this type +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +/// | Resv |S| QRV | QQIC | Number of Sources (N) | ↓ +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// | Source Address [1] | | +/// +- -+ | +/// | Source Address [2] | | +/// +- . -+ | part of payload +/// . . . | +/// . . . | +/// +- -+ | +/// | Source Address [N] | ↓ +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// ``` +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct IgmpHeader { + /// IGMP message type. + pub igmp_type: IgmpType, + /// Checksum in the IGMP header. + pub checksum: u16, +} + +impl IgmpHeader { + /// Number of bytes/octets an [`IgmpHeader`] takes up at minimum in serialized form. + pub const MIN_LEN: usize = 8; + + /// Number of bytes/octets an [`IgmpHeader`] takes up maximally in serialized form. + pub const MAX_LEN: usize = 12; + + /// Constructs an [`IgmpHeader`] with reserved & checksum set to 0. + #[inline] + pub fn new(igmp_type: IgmpType) -> IgmpHeader { + IgmpHeader { + igmp_type, + checksum: 0, + } + } + + /// Creates an [`IgmpHeader`] with a checksum calculated based on the + /// given IGMP type and payload. + /// + /// Per RFC 1112, RFC 2236 and RFC 3376 the checksum is calculated + /// over the entire IGMP message (header + payload) with the + /// checksum field set to zero, even for fields that are unused + /// (e.g. the "Max Resp Time" / reserved fields in IGMPv1 messages). + #[inline] + pub fn with_checksum(igmp_type: IgmpType, payload: &[u8]) -> IgmpHeader { + let mut result = IgmpHeader::new(igmp_type); + result.checksum = result.calc_checksum(payload); + result + } + + /// Reads an IGMP header from a slice and returns a tuple containing the + /// resulting header and the unused part of the slice. + /// + /// The IGMP message variant is determined by the type byte (and, for + /// `0x11` "Membership Query" messages, the slice length per RFC 3376 + /// §7.1: an exactly 8 byte slice is parsed as an IGMPv1/v2 query while + /// a slice of 12 or more bytes is parsed as an IGMPv3 query). For all + /// other recognized type bytes the fixed 8 byte header is consumed. + /// The returned slice is the part of the input that follows the fixed + /// header (e.g. the source addresses of an IGMPv3 query or the group + /// records of an IGMPv3 membership report). + /// + /// # Errors + /// + /// * [`err::igmp::HeaderSliceError::Len`] if the slice is too small + /// to contain a complete header (less than 8 bytes). + /// * [`err::igmp::HeaderSliceError::Content`] with + /// [`err::igmp::HeaderError::UnknownType`] if the IGMP type byte + /// does not match any of the message types defined in + /// RFC 1112, RFC 2236 or RFC 3376. + pub fn from_slice(slice: &[u8]) -> Result<(IgmpHeader, &[u8]), err::igmp::HeaderSliceError> { + use err::igmp::HeaderSliceError::*; + + if slice.len() < Self::MIN_LEN { + return Err(Len(err::LenError { + required_len: Self::MIN_LEN, + len: slice.len(), + len_source: LenSource::Slice, + layer: err::Layer::Igmp, + layer_start_offset: 0, + })); + } + + // SAFETY: length checked above to be >= MIN_LEN (8). + let type_u8 = unsafe { *slice.get_unchecked(0) }; + let max_resp = unsafe { *slice.get_unchecked(1) }; + let checksum = + u16::from_be_bytes(unsafe { [*slice.get_unchecked(2), *slice.get_unchecked(3)] }); + let group_address: [u8; 4] = unsafe { + [ + *slice.get_unchecked(4), + *slice.get_unchecked(5), + *slice.get_unchecked(6), + *slice.get_unchecked(7), + ] + }; + + match type_u8 { + igmp::IGMP_TYPE_MEMBERSHIP_QUERY => { + if igmp::MembershipQueryType::LEN == slice.len() { + // if the length is bellow 12 bytes fall back to the IGMPv1 or + // v2 variant of the query + Ok(( + IgmpHeader { + igmp_type: IgmpType::MembershipQuery(igmp::MembershipQueryType { + max_response_time: max_resp, + group_address: group_address.into(), + }), + checksum, + }, + // SAFETY: Safe as the slice was previously verified to have at least the length + // Self::MIN_LEN. + unsafe { + core::slice::from_raw_parts( + slice.as_ptr().add(Self::MIN_LEN), + slice.len() - Self::MIN_LEN, + ) + }, + )) + } else if slice.len() >= igmp::MembershipQueryWithSourcesHeader::LEN { + // IGMPv3 query messages additionally contain source addresses + // SAFETY: length checked above to be >= igmp::MembershipQueryWithSourcesHeader::LEN (12). + let resv_s_qrv = unsafe { *slice.get_unchecked(8) }; + let qqic = unsafe { *slice.get_unchecked(9) }; + let num_of_sources = u16::from_be_bytes(unsafe { + [*slice.get_unchecked(10), *slice.get_unchecked(11)] + }); + let s_flag = (resv_s_qrv & 0b1000) != 0; + + // SAFETY: only the lower 3 bits are kept which is always + // within `[0, Qrv::MAX_U8]`. + let qrv = unsafe { igmp::Qrv::new_unchecked(resv_s_qrv & 0b111) }; + Ok(( + IgmpHeader { + igmp_type: IgmpType::MembershipQueryWithSources( + igmp::MembershipQueryWithSourcesHeader { + max_response_code: igmp::MaxResponseCode(max_resp), + group_address: group_address.into(), + s_flag, + qrv, + qqic, + num_of_sources, + }, + ), + checksum, + }, + // SAFETY: Safe as the slice was previously verified to have at least the length + // Self::MIN_LEN. + unsafe { + core::slice::from_raw_parts( + slice + .as_ptr() + .add(igmp::MembershipQueryWithSourcesHeader::LEN), + slice.len() - igmp::MembershipQueryWithSourcesHeader::LEN, + ) + }, + )) + } else { + Err(HeaderSliceError::Len(LenError { + required_len: igmp::MembershipQueryWithSourcesHeader::LEN, + len: slice.len(), + len_source: LenSource::Slice, + layer: err::Layer::Igmp, + layer_start_offset: 0, + })) + } + } + igmp::IGMPV1_TYPE_MEMBERSHIP_REPORT => Ok(( + IgmpHeader { + igmp_type: IgmpType::MembershipReportV1(igmp::MembershipReportV1Type { + group_address: group_address.into(), + }), + checksum, + }, + // SAFETY: Safe as the slice was previously verified to have at least the length + // Self::MIN_LEN. + unsafe { + core::slice::from_raw_parts( + slice.as_ptr().add(Self::MIN_LEN), + slice.len() - Self::MIN_LEN, + ) + }, + )), + igmp::IGMPV2_TYPE_MEMBERSHIP_REPORT => Ok(( + IgmpHeader { + igmp_type: IgmpType::MembershipReportV2(igmp::MembershipReportV2Type { + group_address: group_address.into(), + }), + checksum, + }, + // SAFETY: Safe as the slice was previously verified to have at least the length + // Self::MIN_LEN. + unsafe { + core::slice::from_raw_parts( + slice.as_ptr().add(Self::MIN_LEN), + slice.len() - Self::MIN_LEN, + ) + }, + )), + igmp::IGMPV2_TYPE_LEAVE_GROUP => Ok(( + IgmpHeader { + igmp_type: IgmpType::LeaveGroup(igmp::LeaveGroupType { + group_address: group_address.into(), + }), + checksum, + }, + // SAFETY: Safe as the slice was previously verified to have at least the length + // Self::MIN_LEN. + unsafe { + core::slice::from_raw_parts( + slice.as_ptr().add(Self::MIN_LEN), + slice.len() - Self::MIN_LEN, + ) + }, + )), + igmp::IGMPV3_TYPE_MEMBERSHIP_REPORT => { + // bytes 4-5 are reserved, bytes 6-7 are num_of_records. + let num_of_records = u16::from_be_bytes(unsafe { + [*slice.get_unchecked(6), *slice.get_unchecked(7)] + }); + Ok(( + IgmpHeader { + igmp_type: IgmpType::MembershipReportV3(igmp::MembershipReportV3Header { + num_of_records, + }), + checksum, + }, + // SAFETY: Safe as the slice was previously verified to have at least the length + // Self::MIN_LEN. + unsafe { + core::slice::from_raw_parts( + slice.as_ptr().add(Self::MIN_LEN), + slice.len() - Self::MIN_LEN, + ) + }, + )) + } + _ => Err(Content(err::igmp::HeaderError::UnknownType { type_u8 })), + } + } + + /// Calculates the IGMP checksum (16-bit one's complement of the + /// one's complement sum of the entire IGMP message with the checksum + /// field set to zero). + /// + /// `payload` is the part of the IGMP message that comes after the + /// fixed header part covered by [`IgmpHeader`] (for example the + /// source addresses of an IGMPv3 membership query or the group + /// records of an IGMPv3 membership report). + /// + /// RFC 1112, RFC 2236 and RFC 3376 specifies that the checksum must be + /// computed over the whole message even over the bytes that are + /// otherwise ignored by the receiver (e.g. additional unused bytes after + /// the header). + pub fn calc_checksum(&self, payload: &[u8]) -> u16 { + use IgmpType::*; + let sum = match &self.igmp_type { + MembershipQuery(t) => checksum::Sum16BitWords::new() + .add_2bytes([igmp::IGMP_TYPE_MEMBERSHIP_QUERY, t.max_response_time]) + .add_4bytes(t.group_address.octets), + MembershipQueryWithSources(t) => checksum::Sum16BitWords::new() + .add_2bytes([igmp::IGMP_TYPE_MEMBERSHIP_QUERY, t.max_response_code.0]) + .add_4bytes(t.group_address.octets) + .add_2bytes([((t.s_flag as u8) << 3) | (t.qrv.value() & 0b111), t.qqic]) + .add_2bytes(t.num_of_sources.to_be_bytes()), + MembershipReportV1(t) => checksum::Sum16BitWords::new() + .add_2bytes([igmp::IGMPV1_TYPE_MEMBERSHIP_REPORT, 0]) + .add_4bytes(t.group_address.octets), + MembershipReportV2(t) => checksum::Sum16BitWords::new() + .add_2bytes([igmp::IGMPV2_TYPE_MEMBERSHIP_REPORT, 0]) + .add_4bytes(t.group_address.octets), + MembershipReportV3(t) => checksum::Sum16BitWords::new() + .add_2bytes([igmp::IGMPV3_TYPE_MEMBERSHIP_REPORT, 0]) + .add_2bytes([0, 0]) + .add_2bytes(t.num_of_records.to_be_bytes()), + LeaveGroup(t) => checksum::Sum16BitWords::new() + .add_2bytes([igmp::IGMPV2_TYPE_LEAVE_GROUP, 0]) + .add_4bytes(t.group_address.octets), + }; + sum.add_slice(payload).ones_complement().to_be() + } + + /// Length in bytes/octets of this header type. + #[inline] + pub const fn header_len(&self) -> usize { + use IgmpType::*; + match self.igmp_type { + MembershipQuery(_) => igmp::MembershipQueryType::LEN, + MembershipQueryWithSources(_) => igmp::MembershipQueryWithSourcesHeader::LEN, + MembershipReportV1(_) => igmp::MembershipReportV1Type::LEN, + MembershipReportV2(_) => igmp::MembershipReportV2Type::LEN, + MembershipReportV3(_) => igmp::MembershipReportV3Header::LEN, + LeaveGroup(_) => igmp::LeaveGroupType::LEN, + } + } + + /// Converts the header to on-the-wire bytes. + pub fn to_bytes(&self) -> ArrayVec { + use IgmpType::*; + let c = self.checksum.to_be_bytes(); + match &self.igmp_type { + MembershipQuery(t) => { + let mut bytes = ArrayVec::from([ + igmp::IGMP_TYPE_MEMBERSHIP_QUERY, + t.max_response_time, + c[0], + c[1], + t.group_address.octets[0], + t.group_address.octets[1], + t.group_address.octets[2], + t.group_address.octets[3], + 0, + 0, + 0, + 0, + ]); + // SAFETY: Safe as u8 has no destruction behavior and as 8 is smaller then 12. + unsafe { + bytes.set_len(8); + } + bytes + } + MembershipQueryWithSources(t) => { + let num_sources_be = t.num_of_sources.to_be_bytes(); + ArrayVec::from([ + igmp::IGMP_TYPE_MEMBERSHIP_QUERY, + t.max_response_code.0, + c[0], + c[1], + t.group_address.octets[0], + t.group_address.octets[1], + t.group_address.octets[2], + t.group_address.octets[3], + ((t.s_flag as u8) << 3) | (t.qrv.value() & 0b111), + t.qqic, + num_sources_be[0], + num_sources_be[1], + ]) + } + MembershipReportV1(t) => { + let mut bytes = ArrayVec::from([ + igmp::IGMPV1_TYPE_MEMBERSHIP_REPORT, + 0, // unused + c[0], + c[1], + t.group_address.octets[0], + t.group_address.octets[1], + t.group_address.octets[2], + t.group_address.octets[3], + 0, + 0, + 0, + 0, + ]); + // SAFETY: Safe as u8 has no destruction behavior and as 8 is smaller then 12. + unsafe { + bytes.set_len(8); + } + bytes + } + MembershipReportV2(t) => { + let mut bytes = ArrayVec::from([ + igmp::IGMPV2_TYPE_MEMBERSHIP_REPORT, + 0, // "Max Resp Time" field is unused in Membership Report messages + c[0], + c[1], + t.group_address.octets[0], + t.group_address.octets[1], + t.group_address.octets[2], + t.group_address.octets[3], + 0, + 0, + 0, + 0, + ]); + // SAFETY: Safe as u8 has no destruction behavior and as 8 is smaller then 12. + unsafe { + bytes.set_len(8); + } + bytes + } + MembershipReportV3(t) => { + let num_recs_be = t.num_of_records.to_be_bytes(); + let mut bytes = ArrayVec::from([ + igmp::IGMPV3_TYPE_MEMBERSHIP_REPORT, + 0, // reserved + c[0], + c[1], + 0, + 0, + num_recs_be[0], + num_recs_be[1], + 0, + 0, + 0, + 0, + ]); + // SAFETY: Safe as u8 has no destruction behavior and as 8 is smaller then 12. + unsafe { + bytes.set_len(8); + } + bytes + } + LeaveGroup(t) => { + let mut bytes = ArrayVec::from([ + igmp::IGMPV2_TYPE_LEAVE_GROUP, + 0, // "Max Resp Time" field is unused in leave group messages + c[0], + c[1], + t.group_address.octets[0], + t.group_address.octets[1], + t.group_address.octets[2], + t.group_address.octets[3], + 0, + 0, + 0, + 0, + ]); + // SAFETY: Safe as u8 has no destruction behavior and as 8 is smaller then 12. + unsafe { + bytes.set_len(8); + } + bytes + } + } + } +} + +#[cfg(test)] +mod test { + use crate::*; + use alloc::{format, vec, vec::Vec}; + use proptest::prelude::*; + + #[test] + fn constants() { + assert_eq!(8, IgmpHeader::MIN_LEN); + assert_eq!(12, IgmpHeader::MAX_LEN); + } + + proptest! { + #[test] + fn from_slice( + max_response_time in any::(), + max_response_code in any::(), + group_address in any::<[u8;4]>(), + s_flag in any::(), + qrv_raw in 0u8..=igmp::Qrv::MAX_U8, + qqic in any::(), + num_of_sources in any::(), + num_of_records in any::(), + checksum in any::(), + // arbitrary trailing bytes that should be returned as `rest` + // for variants whose fixed header consumes only 8 bytes. + suffix in proptest::collection::vec(any::(), 0..256usize), + // an arbitrary unknown IGMP type byte (filtered to exclude + // the five message types defined in the RFCs). + unknown_type in any::().prop_filter( + "must not be a known IGMP type", + |t| ![ + igmp::IGMP_TYPE_MEMBERSHIP_QUERY, + igmp::IGMPV1_TYPE_MEMBERSHIP_REPORT, + igmp::IGMPV2_TYPE_MEMBERSHIP_REPORT, + igmp::IGMPV2_TYPE_LEAVE_GROUP, + igmp::IGMPV3_TYPE_MEMBERSHIP_REPORT, + ].contains(t), + ), + ) { + let qrv = igmp::Qrv::try_new(qrv_raw).unwrap(); + let cs_be = checksum.to_be_bytes(); + + // membership query + { + let bytes = [ + igmp::IGMP_TYPE_MEMBERSHIP_QUERY, + max_response_time, + cs_be[0], cs_be[1], + group_address[0], group_address[1], + group_address[2], group_address[3], + ]; + let (header, rest) = IgmpHeader::from_slice(&bytes).unwrap(); + prop_assert_eq!( + header, + IgmpHeader { + igmp_type: IgmpType::MembershipQuery(igmp::MembershipQueryType { + max_response_time, + group_address: group_address.into(), + }), + checksum, + } + ); + prop_assert!(rest.is_empty()); + } + + // membership query with sources + { + // top 4 reserved bits set arbitrarily so we can also + // verify that the parser ignores them. + let resv_bits = 0b1010_0000u8; + let mut head = [0u8; 12]; + head[0] = igmp::IGMP_TYPE_MEMBERSHIP_QUERY; + head[1] = max_response_code; + head[2] = cs_be[0]; + head[3] = cs_be[1]; + head[4..8].copy_from_slice(&group_address); + head[8] = resv_bits | ((s_flag as u8) << 3) | (qrv_raw & 0b111); + head[9] = qqic; + head[10..12].copy_from_slice(&num_of_sources.to_be_bytes()); + + let mut full = Vec::with_capacity(head.len() + suffix.len()); + full.extend_from_slice(&head); + full.extend_from_slice(&suffix); + + let (header, rest) = IgmpHeader::from_slice(&full).unwrap(); + prop_assert_eq!( + header, + IgmpHeader { + igmp_type: IgmpType::MembershipQueryWithSources( + igmp::MembershipQueryWithSourcesHeader { + max_response_code: igmp::MaxResponseCode(max_response_code), + group_address: group_address.into(), + s_flag, + qrv, + qqic, + num_of_sources, + }, + ), + checksum, + } + ); + prop_assert_eq!(rest, suffix.as_slice()); + } + + // membership report v1 + { + let head = [ + igmp::IGMPV1_TYPE_MEMBERSHIP_REPORT, + 0, + cs_be[0], cs_be[1], + group_address[0], group_address[1], + group_address[2], group_address[3], + ]; + let mut full = Vec::with_capacity(head.len() + suffix.len()); + full.extend_from_slice(&head); + full.extend_from_slice(&suffix); + + let (header, rest) = IgmpHeader::from_slice(&full).unwrap(); + prop_assert_eq!( + header, + IgmpHeader { + igmp_type: IgmpType::MembershipReportV1(igmp::MembershipReportV1Type { + group_address: group_address.into(), + }), + checksum, + } + ); + prop_assert_eq!(rest, suffix.as_slice()); + } + + // membership report v2 + { + let head = [ + igmp::IGMPV2_TYPE_MEMBERSHIP_REPORT, + 0, + cs_be[0], cs_be[1], + group_address[0], group_address[1], + group_address[2], group_address[3], + ]; + let mut full = Vec::with_capacity(head.len() + suffix.len()); + full.extend_from_slice(&head); + full.extend_from_slice(&suffix); + + let (header, rest) = IgmpHeader::from_slice(&full).unwrap(); + prop_assert_eq!( + header, + IgmpHeader { + igmp_type: IgmpType::MembershipReportV2(igmp::MembershipReportV2Type { + group_address: group_address.into(), + }), + checksum, + } + ); + prop_assert_eq!(rest, suffix.as_slice()); + } + + // leave group + { + let head = [ + igmp::IGMPV2_TYPE_LEAVE_GROUP, + 0, + cs_be[0], cs_be[1], + group_address[0], group_address[1], + group_address[2], group_address[3], + ]; + let mut full = Vec::with_capacity(head.len() + suffix.len()); + full.extend_from_slice(&head); + full.extend_from_slice(&suffix); + + let (header, rest) = IgmpHeader::from_slice(&full).unwrap(); + prop_assert_eq!( + header, + IgmpHeader { + igmp_type: IgmpType::LeaveGroup(igmp::LeaveGroupType { + group_address: group_address.into(), + }), + checksum, + } + ); + prop_assert_eq!(rest, suffix.as_slice()); + } + + // membership report v3 + { + let nr_be = num_of_records.to_be_bytes(); + let head = [ + igmp::IGMPV3_TYPE_MEMBERSHIP_REPORT, + 0, + cs_be[0], cs_be[1], + 0, 0, + nr_be[0], nr_be[1], + ]; + let mut full = Vec::with_capacity(head.len() + suffix.len()); + full.extend_from_slice(&head); + full.extend_from_slice(&suffix); + + let (header, rest) = IgmpHeader::from_slice(&full).unwrap(); + prop_assert_eq!( + header, + IgmpHeader { + igmp_type: IgmpType::MembershipReportV3(igmp::MembershipReportV3Header { + num_of_records, + }), + checksum, + } + ); + prop_assert_eq!(rest, suffix.as_slice()); + } + + // serialize & deserialize all types + { + let cases: [IgmpType; 6] = [ + IgmpType::MembershipQuery(igmp::MembershipQueryType { + max_response_time, + group_address: group_address.into(), + }), + IgmpType::MembershipQueryWithSources(igmp::MembershipQueryWithSourcesHeader { + max_response_code: igmp::MaxResponseCode(max_response_code), + group_address: group_address.into(), + s_flag, + qrv, + qqic, + num_of_sources, + }), + IgmpType::MembershipReportV1(igmp::MembershipReportV1Type { + group_address: group_address.into(), + }), + IgmpType::MembershipReportV2(igmp::MembershipReportV2Type { + group_address: group_address.into(), + }), + IgmpType::MembershipReportV3(igmp::MembershipReportV3Header { + num_of_records, + }), + IgmpType::LeaveGroup(igmp::LeaveGroupType { + group_address: group_address.into(), + }), + ]; + + for igmp_type in cases { + let original = IgmpHeader { igmp_type, checksum }; + let bytes = original.to_bytes(); + let (parsed, rest) = IgmpHeader::from_slice(bytes.as_slice()).unwrap(); + prop_assert_eq!(parsed, original); + prop_assert!(rest.is_empty()); + } + } + + // length error less then 8 bytes + { + let buf = [0u8; IgmpHeader::MIN_LEN]; + for bad_len in 0..IgmpHeader::MIN_LEN { + prop_assert_eq!( + IgmpHeader::from_slice(&buf[..bad_len]), + Err(err::igmp::HeaderSliceError::Len(err::LenError { + required_len: IgmpHeader::MIN_LEN, + len: bad_len, + len_source: LenSource::Slice, + layer: err::Layer::Igmp, + layer_start_offset: 0, + })) + ); + } + } + + // length error less then 9-11 bytes (membership query) + { + for bad_len in (IgmpHeader::MIN_LEN + 1)..IgmpHeader::MAX_LEN { + let mut buf = vec![0u8; bad_len]; + buf[0] = igmp::IGMP_TYPE_MEMBERSHIP_QUERY; + prop_assert_eq!( + IgmpHeader::from_slice(&buf), + Err(err::igmp::HeaderSliceError::Len(err::LenError { + required_len: IgmpHeader::MAX_LEN, + len: bad_len, + len_source: LenSource::Slice, + layer: err::Layer::Igmp, + layer_start_offset: 0, + })) + ); + } + } + + // unknown type + { + let mut buf = [0u8; IgmpHeader::MIN_LEN]; + buf[0] = unknown_type; + prop_assert_eq!( + IgmpHeader::from_slice(&buf), + Err(err::igmp::HeaderSliceError::Content( + err::igmp::HeaderError::UnknownType { type_u8: unknown_type } + )) + ); + } + } + } + + fn assert_rfc_verifies(header: &IgmpHeader, payload: &[u8]) { + let bytes = header.to_bytes(); + let zero = checksum::Sum16BitWords::new() + .add_slice(bytes.as_slice()) + .add_slice(payload) + .ones_complement(); + assert_eq!( + 0, zero, + "expected one's complement sum to be 0 for header {:?} and payload {:?}, got {:#06x}", + header, payload, zero + ); + } + + proptest! { + #[test] + fn calc_checksum( + max_response_time in any::(), + max_response_code in any::(), + group_address in any::<[u8;4]>(), + s_flag in any::(), + qrv_raw in 0u8..=igmp::Qrv::MAX_U8, + qqic in any::(), + num_of_sources in any::(), + num_of_records in any::(), + payload in proptest::collection::vec(any::(), 0..1024usize), + ) { + let qrv = igmp::Qrv::try_new(qrv_raw).unwrap(); + + // membership query + { + let igmp_type = IgmpType::MembershipQuery(igmp::MembershipQueryType { + max_response_time, + group_address: group_address.into(), + }); + let header = IgmpHeader { igmp_type: igmp_type.clone(), checksum: 0 }; + + let expected = checksum::Sum16BitWords::new() + .add_2bytes([igmp::IGMP_TYPE_MEMBERSHIP_QUERY, max_response_time]) + .add_4bytes(group_address) + .add_slice(&payload) + .ones_complement() + .to_be(); + prop_assert_eq!(expected, header.calc_checksum(&payload)); + assert_rfc_verifies(&IgmpHeader::with_checksum(igmp_type, &payload), &payload); + } + + // membership query with sources + { + let igmp_type = IgmpType::MembershipQueryWithSources(igmp::MembershipQueryWithSourcesHeader { + max_response_code: igmp::MaxResponseCode(max_response_code), + group_address: group_address.into(), + s_flag, + qrv, + qqic, + num_of_sources, + }); + let header = IgmpHeader { igmp_type: igmp_type.clone(), checksum: 0 }; + + let expected = checksum::Sum16BitWords::new() + .add_2bytes([igmp::IGMP_TYPE_MEMBERSHIP_QUERY, max_response_code]) + .add_4bytes(group_address) + .add_2bytes([ + ((s_flag as u8) << 3) | (qrv_raw & 0b111), + qqic, + ]) + .add_2bytes(num_of_sources.to_be_bytes()) + .add_slice(&payload) + .ones_complement() + .to_be(); + prop_assert_eq!(expected, header.calc_checksum(&payload)); + assert_rfc_verifies(&IgmpHeader::with_checksum(igmp_type, &payload), &payload); + } + + // membership report v1 + { + let igmp_type = IgmpType::MembershipReportV1(igmp::MembershipReportV1Type { + group_address: group_address.into(), + }); + let header = IgmpHeader { igmp_type: igmp_type.clone(), checksum: 0 }; + + let expected = checksum::Sum16BitWords::new() + .add_2bytes([igmp::IGMPV1_TYPE_MEMBERSHIP_REPORT, 0]) + .add_4bytes(group_address) + .add_slice(&payload) + .ones_complement() + .to_be(); + prop_assert_eq!(expected, header.calc_checksum(&payload)); + assert_rfc_verifies(&IgmpHeader::with_checksum(igmp_type, &payload), &payload); + } + + // membership report v2 + { + let igmp_type = IgmpType::MembershipReportV2(igmp::MembershipReportV2Type { + group_address: group_address.into(), + }); + let header = IgmpHeader { igmp_type: igmp_type.clone(), checksum: 0 }; + + let expected = checksum::Sum16BitWords::new() + .add_2bytes([igmp::IGMPV2_TYPE_MEMBERSHIP_REPORT, 0]) + .add_4bytes(group_address) + .add_slice(&payload) + .ones_complement() + .to_be(); + prop_assert_eq!(expected, header.calc_checksum(&payload)); + assert_rfc_verifies(&IgmpHeader::with_checksum(igmp_type, &payload), &payload); + } + + // membership report v3 + { + let igmp_type = IgmpType::MembershipReportV3(igmp::MembershipReportV3Header { + num_of_records, + }); + let header = IgmpHeader { igmp_type: igmp_type.clone(), checksum: 0 }; + + let expected = checksum::Sum16BitWords::new() + .add_2bytes([igmp::IGMPV3_TYPE_MEMBERSHIP_REPORT, 0]) + .add_2bytes([0, 0]) + .add_2bytes(num_of_records.to_be_bytes()) + .add_slice(&payload) + .ones_complement() + .to_be(); + prop_assert_eq!(expected, header.calc_checksum(&payload)); + assert_rfc_verifies(&IgmpHeader::with_checksum(igmp_type, &payload), &payload); + } + + // leave group + { + let igmp_type = IgmpType::LeaveGroup(igmp::LeaveGroupType { + group_address: group_address.into(), + }); + let header = IgmpHeader { igmp_type: igmp_type.clone(), checksum: 0 }; + + let expected = checksum::Sum16BitWords::new() + .add_2bytes([igmp::IGMPV2_TYPE_LEAVE_GROUP, 0]) + .add_4bytes(group_address) + .add_slice(&payload) + .ones_complement() + .to_be(); + prop_assert_eq!(expected, header.calc_checksum(&payload)); + assert_rfc_verifies(&IgmpHeader::with_checksum(igmp_type, &payload), &payload); + } + + // Hand-rolled IGMPv2 Membership Report example with an externally + // computed checksum. Verifies that we produce the exact same + // checksum as the RFC formula applied byte-for-byte. + // + // Type = 0x16, Max Resp Time = 0x00, Checksum = 0x0000, + // Group Address = 224.0.0.1 (0xe0000001) + // + // Manual computation: + // 0x1600 + 0x0000 + 0xe000 + 0x0001 = 0xf601 + // one's complement = 0x09fe + // Final wire bytes are big-endian: 0x09, 0xfe. + { + let header = IgmpHeader { + igmp_type: IgmpType::MembershipReportV2(igmp::MembershipReportV2Type { + group_address: [0xe0, 0x00, 0x00, 0x01].into(), + }), + checksum: 0, + }; + prop_assert_eq!(header.calc_checksum(&[]).to_be_bytes(), [0x09, 0xfe]); + } + + // Different payload bytes must yield a different checksum + // (and both versions still satisfy the RFC verification + // property when paired with their respective payloads). + { + let igmp_type = IgmpType::MembershipReportV3(igmp::MembershipReportV3Header { + num_of_records: 1, + }); + let header_no_payload = IgmpHeader::with_checksum(igmp_type.clone(), &[]); + let header_with_payload = + IgmpHeader::with_checksum(igmp_type, &[0x01, 0x02, 0x03, 0x04]); + prop_assert_ne!(header_no_payload.checksum, header_with_payload.checksum); + assert_rfc_verifies(&header_no_payload, &[]); + assert_rfc_verifies(&header_with_payload, &[0x01, 0x02, 0x03, 0x04]); + } + } + } + + proptest! { + #[test] + fn with_checksum( + max_response_time in any::(), + max_response_code in any::(), + group_address in any::<[u8;4]>(), + s_flag in any::(), + qrv_raw in 0u8..=igmp::Qrv::MAX_U8, + qqic in any::(), + num_of_sources in any::(), + num_of_records in any::(), + payload in proptest::collection::vec(any::(), 0..1024usize), + ) { + let qrv = igmp::Qrv::try_new(qrv_raw).unwrap(); + + // For every IGMP variant, with_checksum must + // 1) preserve the supplied IgmpType verbatim, and + // 2) populate the checksum field with calc_checksum's result. + let cases: [IgmpType; 6] = [ + IgmpType::MembershipQuery(igmp::MembershipQueryType { + max_response_time, + group_address: group_address.into(), + }), + IgmpType::MembershipQueryWithSources(igmp::MembershipQueryWithSourcesHeader { + max_response_code: igmp::MaxResponseCode(max_response_code), + group_address: group_address.into(), + s_flag, + qrv, + qqic, + num_of_sources, + }), + IgmpType::MembershipReportV1(igmp::MembershipReportV1Type { + group_address: group_address.into(), + }), + IgmpType::MembershipReportV2(igmp::MembershipReportV2Type { + group_address: group_address.into(), + }), + IgmpType::MembershipReportV3(igmp::MembershipReportV3Header { + num_of_records, + }), + IgmpType::LeaveGroup(igmp::LeaveGroupType { + group_address: group_address.into(), + }), + ]; + + for igmp_type in cases { + // type is preserved & checksum equals calc_checksum + { + let header = IgmpHeader::with_checksum(igmp_type.clone(), &payload); + prop_assert_eq!(&igmp_type, &header.igmp_type); + + let zero_checksum_header = IgmpHeader { + igmp_type: igmp_type.clone(), + checksum: 0, + }; + prop_assert_eq!( + zero_checksum_header.calc_checksum(&payload), + header.checksum + ); + } + + // RFC verification: a header built with with_checksum + // must produce a one's complement sum of zero over the + // entire IGMP message (header + payload). + { + let header = IgmpHeader::with_checksum(igmp_type, &payload); + assert_rfc_verifies(&header, &payload); + } + } + } + } +} diff --git a/etherparse/src/transport/igmp_type.rs b/etherparse/src/transport/igmp_type.rs new file mode 100644 index 00000000..f9d2a661 --- /dev/null +++ b/etherparse/src/transport/igmp_type.rs @@ -0,0 +1,23 @@ +use crate::igmp; + +/// IGMP message types specific data (excluding checksum). +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum IgmpType { + /// Membership Query message type (IGMPv1 & IGMPv2 compatible, type = 0x11 and static size). + MembershipQuery(igmp::MembershipQueryType), + + /// Membership Query message type (IGMPv3 version, type = 0x11 and dynamic size) with sources. + MembershipQueryWithSources(igmp::MembershipQueryWithSourcesHeader), + + /// Membership Report message type (introduced in IGMPv1, type = 0x12). + MembershipReportV1(igmp::MembershipReportV1Type), + + /// Membership Report message type (introduced in IGMPv2, type = 0x16 & fixed size). + MembershipReportV2(igmp::MembershipReportV2Type), + + /// Membership Report message type (introduced in IGMPv2, type = 0x16 & dynamic size). + MembershipReportV3(igmp::MembershipReportV3Header), + + /// Leave Group message type (introduced in IGMPv2, type = 0x17). + LeaveGroup(igmp::LeaveGroupType), +} diff --git a/etherparse/src/transport/igmpv1_header.rs b/etherparse/src/transport/igmpv1_header.rs deleted file mode 100644 index 1cc41c0b..00000000 --- a/etherparse/src/transport/igmpv1_header.rs +++ /dev/null @@ -1,457 +0,0 @@ -use crate::*; - -/// Membership Query message type. -pub const IGMPV1_TYPE_MEMBERSHIP_QUERY: u8 = 0x11; -/// Version 1 Membership Report message type. -pub const IGMPV1_TYPE_MEMBERSHIP_REPORT: u8 = 0x12; - -/// A header of an IGMPv1 packet. -/// -/// IGMPv1 has a fixed header size of 8 bytes: -/// type, reserved, checksum and group address. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Igmpv1Header { - /// IGMP message type. - pub igmp_type: u8, - /// Reserved/unused octet. - pub reserved: u8, - /// Checksum in the IGMP header. - pub checksum: u16, - /// Group address. - pub group_address: [u8; 4], -} - -impl Igmpv1Header { - /// Number of bytes/octets an [`Igmpv1Header`] takes up in serialized form. - pub const LEN: usize = 8; - - /// Constructs an [`Igmpv1Header`] with reserved & checksum set to 0. - #[inline] - pub fn new(igmp_type: u8, group_address: [u8; 4]) -> Igmpv1Header { - Igmpv1Header { - igmp_type, - reserved: 0, - checksum: 0, - group_address, - } - } - - /// Creates an [`Igmpv1Header`] with a checksum calculated from the header values. - #[inline] - pub fn with_checksum(igmp_type: u8, group_address: [u8; 4]) -> Igmpv1Header { - let mut result = Igmpv1Header::new(igmp_type, group_address); - result.update_checksum(); - result - } - - /// Reads an IGMPv1 header from a slice directly and returns a tuple containing - /// the resulting header & unused part of the slice. - #[inline] - pub fn from_slice(slice: &[u8]) -> Result<(Igmpv1Header, &[u8]), err::LenError> { - if slice.len() < Self::LEN { - return Err(err::LenError { - required_len: Self::LEN, - len: slice.len(), - len_source: LenSource::Slice, - layer: err::Layer::Igmpv1, - layer_start_offset: 0, - }); - } - - Ok(( - Igmpv1Header::from_bytes([ - slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7], - ]), - &slice[Self::LEN..], - )) - } - - /// Read an [`Igmpv1Header`] from a static sized byte array. - #[inline] - pub fn from_bytes(bytes: [u8; 8]) -> Igmpv1Header { - Igmpv1Header { - igmp_type: bytes[0], - reserved: bytes[1], - checksum: u16::from_be_bytes([bytes[2], bytes[3]]), - group_address: [bytes[4], bytes[5], bytes[6], bytes[7]], - } - } - - /// Reads an IGMPv1 header from the given reader. - #[cfg(feature = "std")] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - pub fn read( - reader: &mut T, - ) -> Result { - let mut bytes = [0u8; Self::LEN]; - reader.read_exact(&mut bytes)?; - Ok(Igmpv1Header::from_bytes(bytes)) - } - - /// Write the IGMPv1 header to the given writer. - #[cfg(feature = "std")] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - pub fn write(&self, writer: &mut T) -> Result<(), std::io::Error> { - writer.write_all(&self.to_bytes()) - } - - /// Length in bytes/octets of this header type. - #[inline] - pub const fn header_len(&self) -> usize { - Self::LEN - } - - /// Calculates and returns the checksum based on the current header values. - #[inline] - pub fn calc_checksum(&self) -> u16 { - checksum::Sum16BitWords::new() - .add_2bytes([self.igmp_type, self.reserved]) - .add_4bytes(self.group_address) - .ones_complement() - .to_be() - } - - /// Calculates and updates the checksum in the header. - #[inline] - pub fn update_checksum(&mut self) { - self.checksum = self.calc_checksum(); - } - - /// Converts the header to on-the-wire bytes. - #[inline] - pub fn to_bytes(&self) -> [u8; 8] { - let checksum_be = self.checksum.to_be_bytes(); - [ - self.igmp_type, - self.reserved, - checksum_be[0], - checksum_be[1], - self.group_address[0], - self.group_address[1], - self.group_address[2], - self.group_address[3], - ] - } -} - -#[cfg(test)] -mod test { - use crate::{ - err::{Layer, LenError}, - *, - }; - use alloc::{format, vec, vec::Vec}; - use proptest::prelude::*; - #[cfg(feature = "std")] - use std::io::Cursor; - - #[test] - fn constants() { - assert_eq!(8, Igmpv1Header::LEN); - assert_eq!(0x11, IGMPV1_TYPE_MEMBERSHIP_QUERY); - assert_eq!(0x12, IGMPV1_TYPE_MEMBERSHIP_REPORT); - } - - proptest! { - #[test] - fn new(igmp_type in any::(), group_address in any::<[u8;4]>()) { - assert_eq!( - Igmpv1Header { - igmp_type, - reserved: 0, - checksum: 0, - group_address, - }, - Igmpv1Header::new(igmp_type, group_address) - ); - } - } - - proptest! { - #[test] - fn with_checksum(igmp_type in any::(), group_address in any::<[u8;4]>()) { - let header = Igmpv1Header::with_checksum(igmp_type, group_address); - assert_eq!(igmp_type, header.igmp_type); - assert_eq!(0, header.reserved); - assert_eq!(group_address, header.group_address); - assert_eq!(header.calc_checksum(), header.checksum); - } - } - - proptest! { - #[test] - fn from_slice( - igmp_type in any::(), - reserved in any::(), - checksum in any::(), - group_address in any::<[u8;4]>(), - suffix in proptest::collection::vec(any::(), 0..16) - ) { - let checksum_be = checksum.to_be_bytes(); - let mut bytes = vec![ - igmp_type, - reserved, - checksum_be[0], - checksum_be[1], - group_address[0], - group_address[1], - group_address[2], - group_address[3], - ]; - bytes.extend_from_slice(&suffix); - - let (actual, rest) = Igmpv1Header::from_slice(&bytes).unwrap(); - assert_eq!( - Igmpv1Header { - igmp_type, - reserved, - checksum, - group_address, - }, - actual - ); - assert_eq!(suffix.as_slice(), rest); - - for bad_len in 0..Igmpv1Header::LEN { - assert_eq!( - Igmpv1Header::from_slice(&bytes[..bad_len]), - Err(LenError{ - required_len: Igmpv1Header::LEN, - len: bad_len, - len_source: LenSource::Slice, - layer: Layer::Igmpv1, - layer_start_offset: 0, - }) - ); - } - } - } - - proptest! { - #[test] - fn from_bytes( - igmp_type in any::(), - reserved in any::(), - checksum in any::(), - group_address in any::<[u8;4]>(), - ) { - let checksum_be = checksum.to_be_bytes(); - let bytes = [ - igmp_type, - reserved, - checksum_be[0], - checksum_be[1], - group_address[0], - group_address[1], - group_address[2], - group_address[3], - ]; - - assert_eq!( - Igmpv1Header { - igmp_type, - reserved, - checksum, - group_address, - }, - Igmpv1Header::from_bytes(bytes) - ); - } - } - - proptest! { - #[test] - #[cfg(feature = "std")] - fn read( - igmp_type in any::(), - reserved in any::(), - checksum in any::(), - group_address in any::<[u8;4]>(), - suffix in proptest::collection::vec(any::(), 0..16) - ) { - let input = Igmpv1Header { - igmp_type, - reserved, - checksum, - group_address, - }; - let mut bytes = input.to_bytes().to_vec(); - bytes.extend_from_slice(&suffix); - - let mut cursor = Cursor::new(&bytes); - let actual = Igmpv1Header::read(&mut cursor).unwrap(); - assert_eq!(input, actual); - assert_eq!(Igmpv1Header::LEN as u64, cursor.position()); - - for bad_len in 0..Igmpv1Header::LEN { - let mut c = Cursor::new(&bytes[..bad_len]); - assert!(Igmpv1Header::read(&mut c).is_err()); - } - } - } - - proptest! { - #[test] - #[cfg(feature = "std")] - fn write( - igmp_type in any::(), - reserved in any::(), - checksum in any::(), - group_address in any::<[u8;4]>() - ) { - let input = Igmpv1Header { - igmp_type, - reserved, - checksum, - group_address, - }; - - let mut out = Vec::new(); - input.write(&mut out).unwrap(); - assert_eq!(input.to_bytes().as_slice(), out.as_slice()); - - for bad_len in 0..Igmpv1Header::LEN { - let mut buf = [0u8; Igmpv1Header::LEN]; - let mut c = Cursor::new(&mut buf[..bad_len]); - assert!(input.write(&mut c).is_err()); - } - } - } - - proptest! { - #[test] - fn header_len( - igmp_type in any::(), - reserved in any::(), - checksum in any::(), - group_address in any::<[u8;4]>() - ) { - let input = Igmpv1Header { - igmp_type, - reserved, - checksum, - group_address, - }; - assert_eq!(Igmpv1Header::LEN, input.header_len()); - } - } - - proptest! { - #[test] - fn calc_checksum( - igmp_type in any::(), - reserved in any::(), - checksum in any::(), - group_address in any::<[u8;4]>() - ) { - let input = Igmpv1Header { - igmp_type, - reserved, - checksum, - group_address, - }; - - let expected = checksum::Sum16BitWords::new() - .add_2bytes([igmp_type, reserved]) - .add_4bytes(group_address) - .ones_complement() - .to_be(); - assert_eq!(expected, input.calc_checksum()); - } - } - - proptest! { - #[test] - fn update_checksum( - igmp_type in any::(), - reserved in any::(), - checksum in any::(), - group_address in any::<[u8;4]>() - ) { - let mut input = Igmpv1Header { - igmp_type, - reserved, - checksum, - group_address, - }; - input.update_checksum(); - assert_eq!(input.calc_checksum(), input.checksum); - } - } - - proptest! { - #[test] - fn to_bytes( - igmp_type in any::(), - reserved in any::(), - checksum in any::(), - group_address in any::<[u8;4]>() - ) { - let input = Igmpv1Header { - igmp_type, - reserved, - checksum, - group_address, - }; - let checksum_be = checksum.to_be_bytes(); - assert_eq!( - [ - igmp_type, - reserved, - checksum_be[0], - checksum_be[1], - group_address[0], - group_address[1], - group_address[2], - group_address[3], - ], - input.to_bytes() - ); - } - } - - proptest! { - #[test] - fn clone_eq( - igmp_type in any::(), - reserved in any::(), - checksum in any::(), - group_address in any::<[u8;4]>() - ) { - let input = Igmpv1Header { - igmp_type, - reserved, - checksum, - group_address, - }; - assert_eq!(input, input.clone()); - } - } - - proptest! { - #[test] - fn debug( - igmp_type in any::(), - reserved in any::(), - checksum in any::(), - group_address in any::<[u8;4]>() - ) { - let input = Igmpv1Header { - igmp_type, - reserved, - checksum, - group_address, - }; - assert_eq!( - format!( - "Igmpv1Header {{ igmp_type: {}, reserved: {}, checksum: {}, group_address: {:?} }}", - igmp_type, - reserved, - checksum, - group_address, - ), - format!("{:?}", input) - ); - } - } -} diff --git a/etherparse/src/transport/mod.rs b/etherparse/src/transport/mod.rs index e04c99bc..c104b4b4 100644 --- a/etherparse/src/transport/mod.rs +++ b/etherparse/src/transport/mod.rs @@ -4,6 +4,9 @@ pub mod icmpv4; /// Module containing ICMPv6 related types and constants pub mod icmpv6; +/// Module containing IGMP related types and constants. +pub mod igmp; + mod icmp_echo_header; pub use icmp_echo_header::*; @@ -25,8 +28,11 @@ pub use icmpv6_slice::*; mod icmpv6_type; pub use icmpv6_type::*; -mod igmpv1_header; -pub use igmpv1_header::*; +mod igmp_type; +pub use igmp_type::*; + +mod igmp_header; +pub use igmp_header::*; mod tcp_header; pub use tcp_header::*; @@ -69,4 +75,3 @@ pub use udp_header_slice::*; mod udp_slice; pub use udp_slice::*; - From 507c3ccc24df8c27630756e7b89da64e78442e74 Mon Sep 17 00:00:00 2001 From: Julian Schmid Date: Fri, 1 May 2026 21:21:09 +0200 Subject: [PATCH 05/15] Resolved review comments --- .../src/transport/igmp/leave_group_type.rs | 2 +- .../src/transport/igmp/max_response_code.rs | 2 +- .../membership_query_with_sources_header.rs | 2 +- etherparse/src/transport/igmp/qrv.rs | 4 +-- etherparse/src/transport/igmp_header.rs | 35 ++++++++++++------- etherparse/src/transport/igmp_type.rs | 2 +- 6 files changed, 29 insertions(+), 18 deletions(-) diff --git a/etherparse/src/transport/igmp/leave_group_type.rs b/etherparse/src/transport/igmp/leave_group_type.rs index 94d73df4..a7c73d4c 100644 --- a/etherparse/src/transport/igmp/leave_group_type.rs +++ b/etherparse/src/transport/igmp/leave_group_type.rs @@ -18,6 +18,6 @@ pub struct LeaveGroupType { } impl LeaveGroupType { - /// Number of bytes/octets an [`MembershipQueryV2`] takes up in serialized form. + /// Number of bytes/octets an [`LeaveGroupType`] takes up in serialized form. pub const LEN: usize = 8; } diff --git a/etherparse/src/transport/igmp/max_response_code.rs b/etherparse/src/transport/igmp/max_response_code.rs index d363d7ea..33166834 100644 --- a/etherparse/src/transport/igmp/max_response_code.rs +++ b/etherparse/src/transport/igmp/max_response_code.rs @@ -1,5 +1,5 @@ /// Max response code (specifies the maximum time allowed before -/// sending a responding report in ICMPv3). +/// sending a responding report in IGMPv3). /// /// The actual time allowed, called the Max /// Resp Time, is represented in units of 1/10 second and is derived from diff --git a/etherparse/src/transport/igmp/membership_query_with_sources_header.rs b/etherparse/src/transport/igmp/membership_query_with_sources_header.rs index ebc482a5..56f4498b 100644 --- a/etherparse/src/transport/igmp/membership_query_with_sources_header.rs +++ b/etherparse/src/transport/igmp/membership_query_with_sources_header.rs @@ -52,6 +52,6 @@ pub struct MembershipQueryWithSourcesHeader { } impl MembershipQueryWithSourcesHeader { - /// Number of bytes/octets an [`MembershipReportV1`] takes up in serialized form. + /// Number of bytes/octets an [`MembershipQueryWithSourcesHeader`] takes up in serialized form. pub const LEN: usize = 12; } diff --git a/etherparse/src/transport/igmp/qrv.rs b/etherparse/src/transport/igmp/qrv.rs index 5cc730a3..837598c5 100644 --- a/etherparse/src/transport/igmp/qrv.rs +++ b/etherparse/src/transport/igmp/qrv.rs @@ -12,10 +12,10 @@ impl Qrv { /// QRV with value 0. pub const ZERO: Qrv = Qrv(0); - /// Maximum value of an IGMPv3 Membership Query QRV. + /// Maximum value of the IGMPv3 Membership Query QRV. pub const MAX_U8: u8 = 0b0000_0111; - /// Maximum value of DSCP field (6 bits). + /// Maximum value of the IGMPv3 Membership Query QRV. pub const MAX: Qrv = Qrv(Self::MAX_U8); /// Static array with all possible values. diff --git a/etherparse/src/transport/igmp_header.rs b/etherparse/src/transport/igmp_header.rs index 6aafe2b9..56af5540 100644 --- a/etherparse/src/transport/igmp_header.rs +++ b/etherparse/src/transport/igmp_header.rs @@ -47,20 +47,31 @@ use crate::{ /// 0 1 2 3 /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -/// | Type = 0x11 | Max Resp Code | Checksum | | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | part of header and -/// | Group Address | | this type -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | -/// | Resv |S| QRV | QQIC | Number of Sources (N) | ↓ +/// | Type = 0x22 | Reserved | Checksum | | part of header & +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | this type +/// | Reserved | Number of Group Records (M) | ↓ /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -/// | Source Address [1] | | -/// +- -+ | -/// | Source Address [2] | | -/// +- . -+ | part of payload -/// . . . | +/// | | | +/// . . | +/// . Group Record [1] . | +/// . . | +/// | | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +/// | | | +/// . . | +/// . Group Record [2] . | part of payload +/// . . | +/// | | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +/// | . | | /// . . . | -/// +- -+ | -/// | Source Address [N] | ↓ +/// | . | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +/// | | | +/// . . | +/// . Group Record [M] . | +/// . . | +/// | | ↓ /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// ``` #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/etherparse/src/transport/igmp_type.rs b/etherparse/src/transport/igmp_type.rs index f9d2a661..ec2f60fd 100644 --- a/etherparse/src/transport/igmp_type.rs +++ b/etherparse/src/transport/igmp_type.rs @@ -15,7 +15,7 @@ pub enum IgmpType { /// Membership Report message type (introduced in IGMPv2, type = 0x16 & fixed size). MembershipReportV2(igmp::MembershipReportV2Type), - /// Membership Report message type (introduced in IGMPv2, type = 0x16 & dynamic size). + /// Membership Report message type (introduced in IGMPv3, type = 0x22 & dynamic size). MembershipReportV3(igmp::MembershipReportV3Header), /// Leave Group message type (introduced in IGMPv2, type = 0x17). From 2450868834c10235440eb57b448b1047334c01cf Mon Sep 17 00:00:00 2001 From: Julian Schmid Date: Fri, 1 May 2026 21:23:46 +0200 Subject: [PATCH 06/15] Resolved cargo doc warnings --- etherparse/src/transport/icmpv6/router_advertisement_header.rs | 2 +- etherparse/src/transport/igmp/membership_query_type.rs | 2 +- etherparse/src/transport/igmp/membership_report_v1_type.rs | 2 +- etherparse/src/transport/igmp/membership_report_v2_type.rs | 2 +- etherparse/src/transport/igmp/qrv.rs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/etherparse/src/transport/icmpv6/router_advertisement_header.rs b/etherparse/src/transport/icmpv6/router_advertisement_header.rs index ffd55539..a28aeb39 100644 --- a/etherparse/src/transport/icmpv6/router_advertisement_header.rs +++ b/etherparse/src/transport/icmpv6/router_advertisement_header.rs @@ -11,7 +11,7 @@ pub struct RouterAdvertisementHeader { /// "Managed address configuration" flag. /// /// When set, it indicates that addresses are available via - /// Dynamic Host Configuration Protocol [DHCPv6]. + /// Dynamic Host Configuration Protocol \[DHCPv6\]. /// /// If the M flag is set, the O flag is redundant and /// can be ignored because DHCPv6 will return all diff --git a/etherparse/src/transport/igmp/membership_query_type.rs b/etherparse/src/transport/igmp/membership_query_type.rs index 3476d9c4..4b84ca59 100644 --- a/etherparse/src/transport/igmp/membership_query_type.rs +++ b/etherparse/src/transport/igmp/membership_query_type.rs @@ -35,6 +35,6 @@ pub struct MembershipQueryType { } impl MembershipQueryType { - /// Number of bytes/octets an [`MembershipQueryV2`] takes up in serialized form. + /// Number of bytes/octets an [`MembershipQueryType`] takes up in serialized form. pub const LEN: usize = 8; } diff --git a/etherparse/src/transport/igmp/membership_report_v1_type.rs b/etherparse/src/transport/igmp/membership_report_v1_type.rs index 07e9bf3d..9c04a970 100644 --- a/etherparse/src/transport/igmp/membership_report_v1_type.rs +++ b/etherparse/src/transport/igmp/membership_report_v1_type.rs @@ -18,6 +18,6 @@ pub struct MembershipReportV1Type { } impl MembershipReportV1Type { - /// Number of bytes/octets an [`MembershipReportV1`] takes up in serialized form. + /// Number of bytes/octets an [`MembershipReportV1Type`] takes up in serialized form. pub const LEN: usize = 8; } diff --git a/etherparse/src/transport/igmp/membership_report_v2_type.rs b/etherparse/src/transport/igmp/membership_report_v2_type.rs index ac58835d..d18b2e1a 100644 --- a/etherparse/src/transport/igmp/membership_report_v2_type.rs +++ b/etherparse/src/transport/igmp/membership_report_v2_type.rs @@ -17,6 +17,6 @@ pub struct MembershipReportV2Type { } impl MembershipReportV2Type { - /// Number of bytes/octets an [`MembershipReportV2`] takes up in serialized form. + /// Number of bytes/octets an [`MembershipReportV2Type`] takes up in serialized form. pub const LEN: usize = 8; } diff --git a/etherparse/src/transport/igmp/qrv.rs b/etherparse/src/transport/igmp/qrv.rs index 837598c5..5fb1dba4 100644 --- a/etherparse/src/transport/igmp/qrv.rs +++ b/etherparse/src/transport/igmp/qrv.rs @@ -1,7 +1,7 @@ use crate::err::ValueTooBigError; /// 3 bit unsigned integer containing the "Querier's Robustness Variable" -/// (present in the [`crate::igmp::MembershipQueryV3`]. +/// (present in the [`crate::igmp::MembershipQueryWithSourcesHeader`]. /// /// Established in /// [RFC-3376](https://datatracker.ietf.org/doc/html/rfc3376). From 09d91cf88b91d555b433d5e2c20eda2a0a1eca2a Mon Sep 17 00:00:00 2001 From: Julian Schmid Date: Fri, 1 May 2026 22:01:02 +0200 Subject: [PATCH 07/15] Replaced RFC 3376 refernces with RFC 9776 --- README.md | 2 ++ etherparse/src/err/igmp/header_error.rs | 2 +- etherparse/src/lib.rs | 4 +++- etherparse/src/transport/igmp/qrv.rs | 2 +- etherparse/src/transport/igmp_header.rs | 8 ++++---- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fe402aea..013ac3a3 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,8 @@ Read the documentations of the different methods for a more details: * ["IEEE Standard for Local and metropolitan area networks-Media Access Control (MAC) Security," in IEEE Std 802.1AE-2018 (Revision of IEEE Std 802.1AE-2006) , vol., no., pp.1-239, 26 Dec. 2018, doi: 10.1109/IEEESTD.2018.8585421.](https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8585421&isnumber=8585420) * ["IEEE Standard for Local and metropolitan area networks--Media Access Control (MAC) Security Corrigendum 1: Tag Control Information Figure," in IEEE Std 802.1AE-2018/Cor 1-2020 (Corrigendum to IEEE Std 802.1AE-2018) , vol., no., pp.1-14, 21 July 2020, doi: 10.1109/IEEESTD.2020.9144679.](https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=9144679&isnumber=9144678) * Host Extensions for IP Multicasting (IGMPv1) [RFC 1112](https://tools.ietf.org/html/rfc1112) +* Internet Group Management Protocol, Version 2 [RFC 2236](https://tools.ietf.org/html/rfc2236) +* Internet Group Management Protocol, Version 3 [RFC 9776](https://tools.ietf.org/html/rfc9776) ## License Licensed under either of Apache License, Version 2.0 or MIT license at your option. The corresponding license texts can be found in the LICENSE-APACHE file and the LICENSE-MIT file. diff --git a/etherparse/src/err/igmp/header_error.rs b/etherparse/src/err/igmp/header_error.rs index 2f64b2b1..666d7dbe 100644 --- a/etherparse/src/err/igmp/header_error.rs +++ b/etherparse/src/err/igmp/header_error.rs @@ -2,7 +2,7 @@ #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub enum HeaderError { /// Error when the IGMP type byte does not match any of the message - /// types defined in RFC 1112, RFC 2236 or RFC 3376 + /// types defined in RFC 1112, RFC 2236 or RFC 9776 /// (`0x11`, `0x12`, `0x16`, `0x17`, `0x22`). UnknownType { type_u8: u8 }, } diff --git a/etherparse/src/lib.rs b/etherparse/src/lib.rs index 144c8e7b..67f40d1f 100644 --- a/etherparse/src/lib.rs +++ b/etherparse/src/lib.rs @@ -281,7 +281,9 @@ //! * [Arp hardware identifiers definitions](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/plain/include/uapi/linux/if_arp.h?id=e33c4963bf536900f917fb65a687724d5539bc21) on the Linux kernel //! * ["IEEE Standard for Local and metropolitan area networks-Media Access Control (MAC) Security," in IEEE Std 802.1AE-2018 (Revision of IEEE Std 802.1AE-2006) , vol., no., pp.1-239, 26 Dec. 2018, doi: 10.1109/IEEESTD.2018.8585421.](https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8585421&isnumber=8585420) //! * ["IEEE Standard for Local and metropolitan area networks--Media Access Control (MAC) Security Corrigendum 1: Tag Control Information Figure," in IEEE Std 802.1AE-2018/Cor 1-2020 (Corrigendum to IEEE Std 802.1AE-2018) , vol., no., pp.1-14, 21 July 2020, doi: 10.1109/IEEESTD.2020.9144679.](https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=9144679&isnumber=9144678) -//! * * Host Extensions for IP Multicasting (IGMPv1) [RFC 1112](https://tools.ietf.org/html/rfc1112) +//! * Host Extensions for IP Multicasting (IGMPv1) [RFC 1112](https://tools.ietf.org/html/rfc1112) +//! * Internet Group Management Protocol, Version 2 [RFC 2236](https://tools.ietf.org/html/rfc2236) +//! * Internet Group Management Protocol, Version 3 [RFC 9776](https://tools.ietf.org/html/rfc9776) // // # Reason for 'bool_comparison' disable: // diff --git a/etherparse/src/transport/igmp/qrv.rs b/etherparse/src/transport/igmp/qrv.rs index 5fb1dba4..7dfddd9e 100644 --- a/etherparse/src/transport/igmp/qrv.rs +++ b/etherparse/src/transport/igmp/qrv.rs @@ -4,7 +4,7 @@ use crate::err::ValueTooBigError; /// (present in the [`crate::igmp::MembershipQueryWithSourcesHeader`]. /// /// Established in -/// [RFC-3376](https://datatracker.ietf.org/doc/html/rfc3376). +/// [RFC-9776](https://datatracker.ietf.org/doc/html/rfc9776). #[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Qrv(u8); diff --git a/etherparse/src/transport/igmp_header.rs b/etherparse/src/transport/igmp_header.rs index 56af5540..bd91215d 100644 --- a/etherparse/src/transport/igmp_header.rs +++ b/etherparse/src/transport/igmp_header.rs @@ -101,7 +101,7 @@ impl IgmpHeader { /// Creates an [`IgmpHeader`] with a checksum calculated based on the /// given IGMP type and payload. /// - /// Per RFC 1112, RFC 2236 and RFC 3376 the checksum is calculated + /// Per RFC 1112, RFC 2236 and RFC 9776 the checksum is calculated /// over the entire IGMP message (header + payload) with the /// checksum field set to zero, even for fields that are unused /// (e.g. the "Max Resp Time" / reserved fields in IGMPv1 messages). @@ -116,7 +116,7 @@ impl IgmpHeader { /// resulting header and the unused part of the slice. /// /// The IGMP message variant is determined by the type byte (and, for - /// `0x11` "Membership Query" messages, the slice length per RFC 3376 + /// `0x11` "Membership Query" messages, the slice length per RFC 9776 /// §7.1: an exactly 8 byte slice is parsed as an IGMPv1/v2 query while /// a slice of 12 or more bytes is parsed as an IGMPv3 query). For all /// other recognized type bytes the fixed 8 byte header is consumed. @@ -131,7 +131,7 @@ impl IgmpHeader { /// * [`err::igmp::HeaderSliceError::Content`] with /// [`err::igmp::HeaderError::UnknownType`] if the IGMP type byte /// does not match any of the message types defined in - /// RFC 1112, RFC 2236 or RFC 3376. + /// RFC 1112, RFC 2236 or RFC 9776. pub fn from_slice(slice: &[u8]) -> Result<(IgmpHeader, &[u8]), err::igmp::HeaderSliceError> { use err::igmp::HeaderSliceError::*; @@ -312,7 +312,7 @@ impl IgmpHeader { /// source addresses of an IGMPv3 membership query or the group /// records of an IGMPv3 membership report). /// - /// RFC 1112, RFC 2236 and RFC 3376 specifies that the checksum must be + /// RFC 1112, RFC 2236 and RFC 9776 specifies that the checksum must be /// computed over the whole message even over the bytes that are /// otherwise ignored by the receiver (e.g. additional unused bytes after /// the header). From b863d90adc500155fb02b0accc8d8b6fec64a561 Mon Sep 17 00:00:00 2001 From: Julian Schmid Date: Sat, 2 May 2026 09:28:19 +0200 Subject: [PATCH 08/15] Adapt the implementation to rfc 9776 --- .../transport/igmp/membership_query_type.rs | 8 +- .../membership_query_with_sources_header.rs | 218 +++++++++++++++++- .../igmp/membership_report_v3_header.rs | 13 +- etherparse/src/transport/igmp_header.rs | 112 +++++---- 4 files changed, 293 insertions(+), 58 deletions(-) diff --git a/etherparse/src/transport/igmp/membership_query_type.rs b/etherparse/src/transport/igmp/membership_query_type.rs index 4b84ca59..b66ff73e 100644 --- a/etherparse/src/transport/igmp/membership_query_type.rs +++ b/etherparse/src/transport/igmp/membership_query_type.rs @@ -1,7 +1,11 @@ use crate::igmp::GroupAddress; -/// A membership query message type (present in IGMPv1 and IGMPv2, but -/// the values are only filled for IGMPv2 with non zero values). +/// IGMPv1/IGMPv2 Membership Query message type. +/// +/// IGMPv1 & IGMPv2 can be distinguished via the `max_response_time` field: +/// +/// * For IGMPv1 the `max_response_time` field is set to zero +/// * For IGMPv2 the `max_response_time` field is set to NOT zero /// /// ```text /// 0 1 2 3 diff --git a/etherparse/src/transport/igmp/membership_query_with_sources_header.rs b/etherparse/src/transport/igmp/membership_query_with_sources_header.rs index 56f4498b..f25b7a54 100644 --- a/etherparse/src/transport/igmp/membership_query_with_sources_header.rs +++ b/etherparse/src/transport/igmp/membership_query_with_sources_header.rs @@ -10,7 +10,7 @@ use crate::igmp::{GroupAddress, MaxResponseCode, Qrv}; /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | part of header and /// | Group Address | | this type /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | -/// | Resv |S| QRV | QQIC | Number of Sources (N) | ↓ +/// | Flags |S| QRV | QQIC | Number of Sources (N) | ↓ /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Source Address [1] | | /// +- -+ | @@ -35,23 +35,223 @@ pub struct MembershipQueryWithSourcesHeader { /// queries to learn if a particular group has members on an /// attached network. /// - /// For IGMPv1, this field is always set to zero. + /// For IGMPv1, this field is ignored. pub group_address: GroupAddress, - /// S Flag (Suppress Router-Side Processing). - pub s_flag: bool, + /// Raw byte containing "flags", "s" & "QRV" (the getters & setters + /// methods can be used to get & set the different values). + pub raw_byte_8: u8, - /// QRV (Querier's Robustness Variable) - pub qrv: Qrv, - - /// QQIC (Querier's Query Interval Code) + /// QQIC (Querier's Query Interval Code). pub qqic: u8, - /// Number of sources + /// Number of source addresses present in the query. + /// + /// The actual addresses are seperated into the + /// payload part of the message. pub num_of_sources: u16, } impl MembershipQueryWithSourcesHeader { /// Number of bytes/octets an [`MembershipQueryWithSourcesHeader`] takes up in serialized form. pub const LEN: usize = 12; + + /// Bitmask identifying the "flags" part of [`MembershipQueryWithSourcesHeader::raw_byte_8`]. + pub const RAW_BYTE_8_MASK_FLAGS: u8 = 0b1111_0000; + + /// Bitshift needed to get to the "flags" part of [`MembershipQueryWithSourcesHeader::raw_byte_8`]. + pub const RAW_BYTE_8_OFFSET_FLAGS: u8 = 4; + + /// Bitmask identifying the "s flag" part of [`MembershipQueryWithSourcesHeader::raw_byte_8`]. + pub const RAW_BYTE_8_MASK_S_FLAG: u8 = 0b0000_1000; + + /// Bitmask identifying the "QRV" part of [`MembershipQueryWithSourcesHeader::raw_byte_8`]. + pub const RAW_BYTE_8_MASK_QRV: u8 = 0b0000_0111; + + /// Extracts the "flags" from the `raw_byte_8` field. + pub fn flags(&self) -> u8 { + (self.raw_byte_8 & Self::RAW_BYTE_8_MASK_FLAGS) >> Self::RAW_BYTE_8_OFFSET_FLAGS + } + + /// Sets the "flags" in the `raw_byte_8` field. + pub fn set_flags(&mut self, value: u8) { + self.raw_byte_8 = (self.raw_byte_8 & (!Self::RAW_BYTE_8_MASK_FLAGS)) + | ((value << Self::RAW_BYTE_8_OFFSET_FLAGS) & Self::RAW_BYTE_8_MASK_FLAGS); + } + + /// Extract the S flag (Suppress Router-Side Processing) from + /// the `raw_byte_8` field. + pub fn s_flag(&self) -> bool { + 0 != (self.raw_byte_8 & Self::RAW_BYTE_8_MASK_S_FLAG) + } + + /// Sets the S flag (Suppress Router-Side Processing) in + /// the `raw_byte_8` field. + pub fn set_s_flag(&mut self, value: bool) { + if value { + self.raw_byte_8 |= Self::RAW_BYTE_8_MASK_S_FLAG; + } else { + self.raw_byte_8 &= !Self::RAW_BYTE_8_MASK_S_FLAG; + } + } + + /// Extracst the QRV (Querier's Robustness Variable) from + /// the `raw_byte_8` field. + pub fn qrv(&self) -> Qrv { + // SAFETY: Safe as the value is guranteed to been within range + // after the mask is applied. + unsafe { Qrv::new_unchecked(self.raw_byte_8 & Self::RAW_BYTE_8_MASK_QRV) } + } + + /// Sets the QRV (Querier's Robustness Variable) in + /// the `raw_byte_8` field. + pub fn set_qrv(&mut self, value: Qrv) { + self.raw_byte_8 = + (self.raw_byte_8 & (!Self::RAW_BYTE_8_MASK_QRV)) | (value.value() & Self::RAW_BYTE_8_MASK_QRV); + } } + +#[cfg(test)] +mod test { + use super::*; + use alloc::format; + use proptest::prelude::*; + + proptest! { + #[test] + fn flags_get(raw_byte_8 in any::()) { + let header = MembershipQueryWithSourcesHeader { + max_response_code: MaxResponseCode(0), + group_address: GroupAddress::new([0, 0, 0, 0]), + raw_byte_8, + qqic: 0, + num_of_sources: 0, + }; + prop_assert_eq!(header.flags(), raw_byte_8 >> 4); + } + } + + proptest! { + #[test] + fn flags_set( + raw_byte_8 in any::(), + value in any::(), + ) { + let mut header = MembershipQueryWithSourcesHeader { + max_response_code: MaxResponseCode(0), + group_address: GroupAddress::new([0, 0, 0, 0]), + raw_byte_8, + qqic: 0, + num_of_sources: 0, + }; + header.set_flags(value); + // "flags" (top 4 bits) should match the lower 4 bits of `value` + prop_assert_eq!(header.flags(), value & 0b0000_1111); + // bits below the "flags" section must be preserved + prop_assert_eq!( + header.raw_byte_8 & !MembershipQueryWithSourcesHeader::RAW_BYTE_8_MASK_FLAGS, + raw_byte_8 & !MembershipQueryWithSourcesHeader::RAW_BYTE_8_MASK_FLAGS + ); + } + } + + proptest! { + #[test] + fn flags_roundtrip( + raw_byte_8 in any::(), + value in 0u8..=0b0000_1111, + ) { + let mut header = MembershipQueryWithSourcesHeader { + max_response_code: MaxResponseCode(0), + group_address: GroupAddress::new([0, 0, 0, 0]), + raw_byte_8, + qqic: 0, + num_of_sources: 0, + }; + header.set_flags(value); + prop_assert_eq!(header.flags(), value); + } + } + + proptest! { + #[test] + fn s_flag_get(raw_byte_8 in any::()) { + let header = MembershipQueryWithSourcesHeader { + max_response_code: MaxResponseCode(0), + group_address: GroupAddress::new([0, 0, 0, 0]), + raw_byte_8, + qqic: 0, + num_of_sources: 0, + }; + prop_assert_eq!( + header.s_flag(), + 0 != (raw_byte_8 & MembershipQueryWithSourcesHeader::RAW_BYTE_8_MASK_S_FLAG) + ); + } + } + + proptest! { + #[test] + fn s_flag_set( + raw_byte_8 in any::(), + value in any::(), + ) { + let mut header = MembershipQueryWithSourcesHeader { + max_response_code: MaxResponseCode(0), + group_address: GroupAddress::new([0, 0, 0, 0]), + raw_byte_8, + qqic: 0, + num_of_sources: 0, + }; + header.set_s_flag(value); + prop_assert_eq!(header.s_flag(), value); + // all other bits must be preserved + prop_assert_eq!( + header.raw_byte_8 & !MembershipQueryWithSourcesHeader::RAW_BYTE_8_MASK_S_FLAG, + raw_byte_8 & !MembershipQueryWithSourcesHeader::RAW_BYTE_8_MASK_S_FLAG + ); + } + } + + proptest! { + #[test] + fn qrv_get(raw_byte_8 in any::()) { + let header = MembershipQueryWithSourcesHeader { + max_response_code: MaxResponseCode(0), + group_address: GroupAddress::new([0, 0, 0, 0]), + raw_byte_8, + qqic: 0, + num_of_sources: 0, + }; + prop_assert_eq!( + header.qrv().value(), + raw_byte_8 & MembershipQueryWithSourcesHeader::RAW_BYTE_8_MASK_QRV + ); + } + } + + proptest! { + #[test] + fn qrv_set( + raw_byte_8 in any::(), + value in 0u8..=Qrv::MAX_U8, + ) { + let qrv = Qrv::try_new(value).unwrap(); + let mut header = MembershipQueryWithSourcesHeader { + max_response_code: MaxResponseCode(0), + group_address: GroupAddress::new([0, 0, 0, 0]), + raw_byte_8, + qqic: 0, + num_of_sources: 0, + }; + header.set_qrv(qrv); + prop_assert_eq!(header.qrv().value(), value); + // bits outside of the QRV section must be preserved + prop_assert_eq!( + header.raw_byte_8 & !MembershipQueryWithSourcesHeader::RAW_BYTE_8_MASK_QRV, + raw_byte_8 & !MembershipQueryWithSourcesHeader::RAW_BYTE_8_MASK_QRV + ); + } + } +} + diff --git a/etherparse/src/transport/igmp/membership_report_v3_header.rs b/etherparse/src/transport/igmp/membership_report_v3_header.rs index f22c7c0a..cafd0cee 100644 --- a/etherparse/src/transport/igmp/membership_report_v3_header.rs +++ b/etherparse/src/transport/igmp/membership_report_v3_header.rs @@ -6,7 +6,7 @@ /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Type = 0x22 | Reserved | Checksum | | part of header & /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | this type -/// | Reserved | Number of Group Records (M) | ↓ +/// | Flags | Number of Group Records (M) | ↓ /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | | | /// . . | @@ -33,11 +33,20 @@ /// ``` #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct MembershipReportV3Header { - /// The number of records in the membership report. + /// Additional `Flags`. + /// + /// Documented in the IANA page + /// [https://www.iana.org/assignments/igmp-type-numbers/igmp-type-numbers.xhtml#igmp-mld-report-message-flags]. + pub flags: [u8; 2], + + /// The number of group records in the membership report pub num_of_records: u16, } impl MembershipReportV3Header { /// Number of bytes/octets an [`MembershipReportV3Header`] takes up in serialized form. pub const LEN: usize = 8; + + /// Mask of "extension" flag in `MembershipReportV3Header::flags[0]`. + pub const FLAGS_0_EXTENSION_MASK: u8 = 0b0000_0001; } diff --git a/etherparse/src/transport/igmp_header.rs b/etherparse/src/transport/igmp_header.rs index bd91215d..53597561 100644 --- a/etherparse/src/transport/igmp_header.rs +++ b/etherparse/src/transport/igmp_header.rs @@ -10,7 +10,7 @@ use crate::{ /// The header contains the static part of an IGMP /// packet. /// -/// The following packet types the header contains all data: +/// For the following message types the header contains all the data: /// /// - IGMP v1 & v2 membership query ([`crate::IgmpType::MembershipQuery`]) /// - IGMP v1 membership report ([`crate::IgmpType::MembershipReportV1`]) @@ -19,7 +19,7 @@ use crate::{ /// /// /// and for the followng messages only the static part is contained -/// within the header: +/// within the header (the variable-length part is in the payload): /// /// - IGMPv3 membership query ([`crate::IgmpType::MembershipQuery`]): /// ```text @@ -30,7 +30,7 @@ use crate::{ /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | part of header and /// | Group Address | | this type /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | -/// | Resv |S| QRV | QQIC | Number of Sources (N) | ↓ +/// | Flags |S| QRV | QQIC | Number of Sources (N) | ↓ /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Source Address [1] | | /// +- -+ | @@ -49,7 +49,7 @@ use crate::{ /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | Type = 0x22 | Reserved | Checksum | | part of header & /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | this type -/// | Reserved | Number of Group Records (M) | ↓ +/// | Flags | Number of Group Records (M) | ↓ /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// | | | /// . . | @@ -115,19 +115,34 @@ impl IgmpHeader { /// Reads an IGMP header from a slice and returns a tuple containing the /// resulting header and the unused part of the slice. /// - /// The IGMP message variant is determined by the type byte (and, for - /// `0x11` "Membership Query" messages, the slice length per RFC 9776 - /// §7.1: an exactly 8 byte slice is parsed as an IGMPv1/v2 query while - /// a slice of 12 or more bytes is parsed as an IGMPv3 query). For all - /// other recognized type bytes the fixed 8 byte header is consumed. - /// The returned slice is the part of the input that follows the fixed - /// header (e.g. the source addresses of an IGMPv3 query or the group - /// records of an IGMPv3 membership report). + /// The IGMP message variant is determined by the type byte. For + /// `0x11` "Membership Query" messages, [RFC 9776 §7.1]( + /// https://datatracker.ietf.org/doc/html/rfc9776#section-7.1) defines + /// the version distinction by message length: + /// + /// * IGMPv1 Query: length = 8 octets AND `Max Resp Code` field is zero. + /// * IGMPv2 Query: length = 8 octets AND `Max Resp Code` field is non-zero. + /// * IGMPv3 Query: length >= 12 octets. + /// * Query Messages of any other length (e.g. 9, 10 or 11 octets) + /// MUST be silently ignored. This parser surfaces them as a + /// [`err::igmp::HeaderSliceError::Len`] error so that callers can + /// make that decision explicitly. + /// + /// IGMPv1 and IGMPv2 queries are returned as + /// [`IgmpType::MembershipQuery`] (the `max_response_time` field is + /// `0` for IGMPv1). IGMPv3 queries are returned as + /// [`IgmpType::MembershipQueryWithSources`]. + /// + /// For all other recognized type bytes the fixed 8 byte header is + /// consumed. The returned slice is the part of the input that + /// follows the fixed header (e.g. the source addresses of an IGMPv3 + /// query or the group records of an IGMPv3 membership report). /// /// # Errors /// /// * [`err::igmp::HeaderSliceError::Len`] if the slice is too small - /// to contain a complete header (less than 8 bytes). + /// to contain a complete header (less than 8 bytes for known + /// types, or 9-11 bytes for a Membership Query). /// * [`err::igmp::HeaderSliceError::Content`] with /// [`err::igmp::HeaderError::UnknownType`] if the IGMP type byte /// does not match any of the message types defined in @@ -184,24 +199,18 @@ impl IgmpHeader { } else if slice.len() >= igmp::MembershipQueryWithSourcesHeader::LEN { // IGMPv3 query messages additionally contain source addresses // SAFETY: length checked above to be >= igmp::MembershipQueryWithSourcesHeader::LEN (12). - let resv_s_qrv = unsafe { *slice.get_unchecked(8) }; + let raw_byte_8 = unsafe { *slice.get_unchecked(8) }; let qqic = unsafe { *slice.get_unchecked(9) }; let num_of_sources = u16::from_be_bytes(unsafe { [*slice.get_unchecked(10), *slice.get_unchecked(11)] }); - let s_flag = (resv_s_qrv & 0b1000) != 0; - - // SAFETY: only the lower 3 bits are kept which is always - // within `[0, Qrv::MAX_U8]`. - let qrv = unsafe { igmp::Qrv::new_unchecked(resv_s_qrv & 0b111) }; Ok(( IgmpHeader { igmp_type: IgmpType::MembershipQueryWithSources( igmp::MembershipQueryWithSourcesHeader { max_response_code: igmp::MaxResponseCode(max_resp), group_address: group_address.into(), - s_flag, - qrv, + raw_byte_8, qqic, num_of_sources, }, @@ -278,13 +287,16 @@ impl IgmpHeader { }, )), igmp::IGMPV3_TYPE_MEMBERSHIP_REPORT => { - // bytes 4-5 are reserved, bytes 6-7 are num_of_records. + // SAFETY: Safe as the slice was previously verified to have at least the length + // Self::MIN_LEN (8). + let flags = unsafe { [*slice.get_unchecked(4), *slice.get_unchecked(5)] }; let num_of_records = u16::from_be_bytes(unsafe { [*slice.get_unchecked(6), *slice.get_unchecked(7)] }); Ok(( IgmpHeader { igmp_type: IgmpType::MembershipReportV3(igmp::MembershipReportV3Header { + flags, num_of_records, }), checksum, @@ -325,7 +337,7 @@ impl IgmpHeader { MembershipQueryWithSources(t) => checksum::Sum16BitWords::new() .add_2bytes([igmp::IGMP_TYPE_MEMBERSHIP_QUERY, t.max_response_code.0]) .add_4bytes(t.group_address.octets) - .add_2bytes([((t.s_flag as u8) << 3) | (t.qrv.value() & 0b111), t.qqic]) + .add_2bytes([t.raw_byte_8, t.qqic]) .add_2bytes(t.num_of_sources.to_be_bytes()), MembershipReportV1(t) => checksum::Sum16BitWords::new() .add_2bytes([igmp::IGMPV1_TYPE_MEMBERSHIP_REPORT, 0]) @@ -335,7 +347,7 @@ impl IgmpHeader { .add_4bytes(t.group_address.octets), MembershipReportV3(t) => checksum::Sum16BitWords::new() .add_2bytes([igmp::IGMPV3_TYPE_MEMBERSHIP_REPORT, 0]) - .add_2bytes([0, 0]) + .add_2bytes(t.flags) .add_2bytes(t.num_of_records.to_be_bytes()), LeaveGroup(t) => checksum::Sum16BitWords::new() .add_2bytes([igmp::IGMPV2_TYPE_LEAVE_GROUP, 0]) @@ -395,7 +407,7 @@ impl IgmpHeader { t.group_address.octets[1], t.group_address.octets[2], t.group_address.octets[3], - ((t.s_flag as u8) << 3) | (t.qrv.value() & 0b111), + t.raw_byte_8, t.qqic, num_sources_be[0], num_sources_be[1], @@ -450,8 +462,8 @@ impl IgmpHeader { 0, // reserved c[0], c[1], - 0, - 0, + t.flags[0], + t.flags[1], num_recs_be[0], num_recs_be[1], 0, @@ -510,8 +522,10 @@ mod test { group_address in any::<[u8;4]>(), s_flag in any::(), qrv_raw in 0u8..=igmp::Qrv::MAX_U8, + query_flags in 0u8..=(igmp::MembershipQueryWithSourcesHeader::RAW_BYTE_8_MASK_FLAGS >> igmp::MembershipQueryWithSourcesHeader::RAW_BYTE_8_OFFSET_FLAGS), qqic in any::(), num_of_sources in any::(), + report_flags in any::<[u8;2]>(), num_of_records in any::(), checksum in any::(), // arbitrary trailing bytes that should be returned as `rest` @@ -530,7 +544,9 @@ mod test { ].contains(t), ), ) { - let qrv = igmp::Qrv::try_new(qrv_raw).unwrap(); + let raw_byte_8 = ((query_flags & igmp::MembershipQueryWithSourcesHeader::RAW_BYTE_8_MASK_FLAGS) << igmp::MembershipQueryWithSourcesHeader::RAW_BYTE_8_OFFSET_FLAGS) + | ((s_flag as u8) << 3) + | (qrv_raw & 0b111); let cs_be = checksum.to_be_bytes(); // membership query @@ -558,16 +574,13 @@ mod test { // membership query with sources { - // top 4 reserved bits set arbitrarily so we can also - // verify that the parser ignores them. - let resv_bits = 0b1010_0000u8; let mut head = [0u8; 12]; head[0] = igmp::IGMP_TYPE_MEMBERSHIP_QUERY; head[1] = max_response_code; head[2] = cs_be[0]; head[3] = cs_be[1]; head[4..8].copy_from_slice(&group_address); - head[8] = resv_bits | ((s_flag as u8) << 3) | (qrv_raw & 0b111); + head[8] = raw_byte_8; head[9] = qqic; head[10..12].copy_from_slice(&num_of_sources.to_be_bytes()); @@ -583,8 +596,7 @@ mod test { igmp::MembershipQueryWithSourcesHeader { max_response_code: igmp::MaxResponseCode(max_response_code), group_address: group_address.into(), - s_flag, - qrv, + raw_byte_8, qqic, num_of_sources, }, @@ -680,7 +692,7 @@ mod test { igmp::IGMPV3_TYPE_MEMBERSHIP_REPORT, 0, cs_be[0], cs_be[1], - 0, 0, + report_flags[0], report_flags[1], nr_be[0], nr_be[1], ]; let mut full = Vec::with_capacity(head.len() + suffix.len()); @@ -692,6 +704,7 @@ mod test { header, IgmpHeader { igmp_type: IgmpType::MembershipReportV3(igmp::MembershipReportV3Header { + flags: report_flags, num_of_records, }), checksum, @@ -710,8 +723,7 @@ mod test { IgmpType::MembershipQueryWithSources(igmp::MembershipQueryWithSourcesHeader { max_response_code: igmp::MaxResponseCode(max_response_code), group_address: group_address.into(), - s_flag, - qrv, + raw_byte_8, qqic, num_of_sources, }), @@ -722,6 +734,7 @@ mod test { group_address: group_address.into(), }), IgmpType::MembershipReportV3(igmp::MembershipReportV3Header { + flags: report_flags, num_of_records, }), IgmpType::LeaveGroup(igmp::LeaveGroupType { @@ -808,12 +821,16 @@ mod test { group_address in any::<[u8;4]>(), s_flag in any::(), qrv_raw in 0u8..=igmp::Qrv::MAX_U8, + query_flags in 0u8..=(igmp::MembershipQueryWithSourcesHeader::RAW_BYTE_8_MASK_FLAGS >> igmp::MembershipQueryWithSourcesHeader::RAW_BYTE_8_OFFSET_FLAGS), qqic in any::(), num_of_sources in any::(), + report_flags in any::<[u8;2]>(), num_of_records in any::(), payload in proptest::collection::vec(any::(), 0..1024usize), ) { - let qrv = igmp::Qrv::try_new(qrv_raw).unwrap(); + let raw_byte_8 = ((query_flags & igmp::MembershipQueryWithSourcesHeader::RAW_BYTE_8_MASK_FLAGS) << igmp::MembershipQueryWithSourcesHeader::RAW_BYTE_8_OFFSET_FLAGS) + | ((s_flag as u8) << 3) + | (qrv_raw & 0b111); // membership query { @@ -838,8 +855,7 @@ mod test { let igmp_type = IgmpType::MembershipQueryWithSources(igmp::MembershipQueryWithSourcesHeader { max_response_code: igmp::MaxResponseCode(max_response_code), group_address: group_address.into(), - s_flag, - qrv, + raw_byte_8, qqic, num_of_sources, }); @@ -849,7 +865,7 @@ mod test { .add_2bytes([igmp::IGMP_TYPE_MEMBERSHIP_QUERY, max_response_code]) .add_4bytes(group_address) .add_2bytes([ - ((s_flag as u8) << 3) | (qrv_raw & 0b111), + raw_byte_8, qqic, ]) .add_2bytes(num_of_sources.to_be_bytes()) @@ -897,13 +913,14 @@ mod test { // membership report v3 { let igmp_type = IgmpType::MembershipReportV3(igmp::MembershipReportV3Header { + flags: report_flags, num_of_records, }); let header = IgmpHeader { igmp_type: igmp_type.clone(), checksum: 0 }; let expected = checksum::Sum16BitWords::new() .add_2bytes([igmp::IGMPV3_TYPE_MEMBERSHIP_REPORT, 0]) - .add_2bytes([0, 0]) + .add_2bytes(report_flags) .add_2bytes(num_of_records.to_be_bytes()) .add_slice(&payload) .ones_complement() @@ -955,6 +972,7 @@ mod test { // property when paired with their respective payloads). { let igmp_type = IgmpType::MembershipReportV3(igmp::MembershipReportV3Header { + flags: [0, 0], num_of_records: 1, }); let header_no_payload = IgmpHeader::with_checksum(igmp_type.clone(), &[]); @@ -975,12 +993,16 @@ mod test { group_address in any::<[u8;4]>(), s_flag in any::(), qrv_raw in 0u8..=igmp::Qrv::MAX_U8, + query_flags in 0u8..=(igmp::MembershipQueryWithSourcesHeader::RAW_BYTE_8_MASK_FLAGS >> igmp::MembershipQueryWithSourcesHeader::RAW_BYTE_8_OFFSET_FLAGS), qqic in any::(), num_of_sources in any::(), + report_flags in any::<[u8;2]>(), num_of_records in any::(), payload in proptest::collection::vec(any::(), 0..1024usize), ) { - let qrv = igmp::Qrv::try_new(qrv_raw).unwrap(); + let raw_byte_8 = ((query_flags & igmp::MembershipQueryWithSourcesHeader::RAW_BYTE_8_MASK_FLAGS) << igmp::MembershipQueryWithSourcesHeader::RAW_BYTE_8_OFFSET_FLAGS) + | ((s_flag as u8) << 3) + | (qrv_raw & 0b111); // For every IGMP variant, with_checksum must // 1) preserve the supplied IgmpType verbatim, and @@ -993,8 +1015,7 @@ mod test { IgmpType::MembershipQueryWithSources(igmp::MembershipQueryWithSourcesHeader { max_response_code: igmp::MaxResponseCode(max_response_code), group_address: group_address.into(), - s_flag, - qrv, + raw_byte_8, qqic, num_of_sources, }), @@ -1005,6 +1026,7 @@ mod test { group_address: group_address.into(), }), IgmpType::MembershipReportV3(igmp::MembershipReportV3Header { + flags: report_flags, num_of_records, }), IgmpType::LeaveGroup(igmp::LeaveGroupType { From 3a4f6d6aac3b1d1669d76801444aabeb9bc269c7 Mon Sep 17 00:00:00 2001 From: Julian Schmid Date: Sat, 2 May 2026 10:22:14 +0200 Subject: [PATCH 09/15] IGMP: Introduced "unknown" type and removed error --- etherparse/src/err/igmp/header_error.rs | 76 -------- etherparse/src/err/igmp/header_slice_error.rs | 142 -------------- etherparse/src/err/igmp/mod.rs | 5 - etherparse/src/err/mod.rs | 1 - etherparse/src/transport/igmp/mod.rs | 3 + .../src/transport/igmp/unknown_header.rs | 34 ++++ etherparse/src/transport/igmp_header.rs | 177 ++++++++++++++---- etherparse/src/transport/igmp_type.rs | 3 + 8 files changed, 183 insertions(+), 258 deletions(-) delete mode 100644 etherparse/src/err/igmp/header_error.rs delete mode 100644 etherparse/src/err/igmp/header_slice_error.rs delete mode 100644 etherparse/src/err/igmp/mod.rs create mode 100644 etherparse/src/transport/igmp/unknown_header.rs diff --git a/etherparse/src/err/igmp/header_error.rs b/etherparse/src/err/igmp/header_error.rs deleted file mode 100644 index 666d7dbe..00000000 --- a/etherparse/src/err/igmp/header_error.rs +++ /dev/null @@ -1,76 +0,0 @@ -/// Errors that can be encountered while decoding an IGMP header. -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub enum HeaderError { - /// Error when the IGMP type byte does not match any of the message - /// types defined in RFC 1112, RFC 2236 or RFC 9776 - /// (`0x11`, `0x12`, `0x16`, `0x17`, `0x22`). - UnknownType { type_u8: u8 }, -} - -impl core::fmt::Display for HeaderError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - use HeaderError::*; - match self { - UnknownType { type_u8 } => write!( - f, - "IGMP Header Error: Unknown IGMP message type {type_u8:#04x}. Expected one of 0x11 (Membership Query), 0x12 (IGMPv1 Membership Report), 0x16 (IGMPv2 Membership Report), 0x17 (IGMPv2 Leave Group) or 0x22 (IGMPv3 Membership Report)." - ), - } - } -} - -impl core::error::Error for HeaderError { - fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { - None - } -} - -#[cfg(test)] -mod tests { - use super::HeaderError::*; - use alloc::format; - use std::{ - collections::hash_map::DefaultHasher, - error::Error, - hash::{Hash, Hasher}, - }; - - #[test] - fn debug() { - assert_eq!( - "UnknownType { type_u8: 255 }", - format!("{:?}", UnknownType { type_u8: 0xff }) - ); - } - - #[test] - fn clone_eq_hash() { - let err = UnknownType { type_u8: 0xff }; - assert_eq!(err, err.clone()); - let hash_a = { - let mut hasher = DefaultHasher::new(); - err.hash(&mut hasher); - hasher.finish() - }; - let hash_b = { - let mut hasher = DefaultHasher::new(); - err.clone().hash(&mut hasher); - hasher.finish() - }; - assert_eq!(hash_a, hash_b); - } - - #[test] - fn fmt() { - assert_eq!( - "IGMP Header Error: Unknown IGMP message type 0xff. Expected one of 0x11 (Membership Query), 0x12 (IGMPv1 Membership Report), 0x16 (IGMPv2 Membership Report), 0x17 (IGMPv2 Leave Group) or 0x22 (IGMPv3 Membership Report).", - format!("{}", UnknownType { type_u8: 0xff }) - ); - } - - #[cfg(feature = "std")] - #[test] - fn source() { - assert!(UnknownType { type_u8: 0xff }.source().is_none()); - } -} diff --git a/etherparse/src/err/igmp/header_slice_error.rs b/etherparse/src/err/igmp/header_slice_error.rs deleted file mode 100644 index 897f293b..00000000 --- a/etherparse/src/err/igmp/header_slice_error.rs +++ /dev/null @@ -1,142 +0,0 @@ -use super::HeaderError; -use crate::err::LenError; - -/// Error when decoding an [`crate::IgmpHeader`] from a slice. -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub enum HeaderSliceError { - /// Error when not enough data is present in the slice to decode an - /// IGMP header. - Len(LenError), - - /// Error caused by the contents of the header. - Content(HeaderError), -} - -impl HeaderSliceError { - /// Adds an offset value to all slice length related fields. - #[inline] - pub const fn add_slice_offset(self, offset: usize) -> Self { - use HeaderSliceError::*; - match self { - Len(err) => Len(err.add_offset(offset)), - Content(err) => Content(err), - } - } -} - -impl core::fmt::Display for HeaderSliceError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - use HeaderSliceError::*; - match self { - Len(err) => err.fmt(f), - Content(err) => err.fmt(f), - } - } -} - -impl core::error::Error for HeaderSliceError { - fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { - match self { - HeaderSliceError::Len(err) => Some(err), - HeaderSliceError::Content(err) => Some(err), - } - } -} - -#[cfg(test)] -mod tests { - use super::{HeaderSliceError::*, *}; - use crate::{err::Layer, LenSource}; - use alloc::format; - use std::{ - collections::hash_map::DefaultHasher, - error::Error, - hash::{Hash, Hasher}, - }; - - #[test] - fn add_slice_offset() { - assert_eq!( - Len(LenError { - required_len: 1, - layer: Layer::Igmp, - len: 2, - len_source: LenSource::Slice, - layer_start_offset: 3 - }) - .add_slice_offset(200), - Len(LenError { - required_len: 1, - layer: Layer::Igmp, - len: 2, - len_source: LenSource::Slice, - layer_start_offset: 203 - }) - ); - assert_eq!( - Content(HeaderError::UnknownType { type_u8: 0xff }).add_slice_offset(200), - Content(HeaderError::UnknownType { type_u8: 0xff }) - ); - } - - #[test] - fn debug() { - let err = HeaderError::UnknownType { type_u8: 0xff }; - assert_eq!( - format!("Content({:?})", err.clone()), - format!("{:?}", Content(err)) - ); - } - - #[test] - fn clone_eq_hash() { - let err = Content(HeaderError::UnknownType { type_u8: 0xff }); - assert_eq!(err, err.clone()); - let hash_a = { - let mut hasher = DefaultHasher::new(); - err.hash(&mut hasher); - hasher.finish() - }; - let hash_b = { - let mut hasher = DefaultHasher::new(); - err.clone().hash(&mut hasher); - hasher.finish() - }; - assert_eq!(hash_a, hash_b); - } - - #[test] - fn fmt() { - { - let err = LenError { - required_len: 1, - layer: Layer::Igmp, - len: 2, - len_source: LenSource::Slice, - layer_start_offset: 3, - }; - assert_eq!(format!("{}", &err), format!("{}", Len(err))); - } - { - let err = HeaderError::UnknownType { type_u8: 0xff }; - assert_eq!(format!("{}", &err), format!("{}", Content(err.clone()))); - } - } - - #[cfg(feature = "std")] - #[test] - fn source() { - assert!(Len(LenError { - required_len: 1, - layer: Layer::Igmp, - len: 2, - len_source: LenSource::Slice, - layer_start_offset: 3 - }) - .source() - .is_some()); - assert!(Content(HeaderError::UnknownType { type_u8: 0xff }) - .source() - .is_some()); - } -} diff --git a/etherparse/src/err/igmp/mod.rs b/etherparse/src/err/igmp/mod.rs deleted file mode 100644 index 6c917fd7..00000000 --- a/etherparse/src/err/igmp/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod header_error; -pub use header_error::*; - -mod header_slice_error; -pub use header_slice_error::*; diff --git a/etherparse/src/err/mod.rs b/etherparse/src/err/mod.rs index b0752827..c955625c 100644 --- a/etherparse/src/err/mod.rs +++ b/etherparse/src/err/mod.rs @@ -1,5 +1,4 @@ pub mod arp; -pub mod igmp; #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub mod io; diff --git a/etherparse/src/transport/igmp/mod.rs b/etherparse/src/transport/igmp/mod.rs index 63aa2175..186c1076 100644 --- a/etherparse/src/transport/igmp/mod.rs +++ b/etherparse/src/transport/igmp/mod.rs @@ -25,6 +25,9 @@ pub use membership_report_v3_header::*; mod qrv; pub use qrv::*; +mod unknown_header; +pub use unknown_header::*; + /// "Membership Query" message type (same in IGMPv1, IGMPv2, IGMPv3). pub const IGMP_TYPE_MEMBERSHIP_QUERY: u8 = 0x11; diff --git a/etherparse/src/transport/igmp/unknown_header.rs b/etherparse/src/transport/igmp/unknown_header.rs new file mode 100644 index 00000000..8ae027cd --- /dev/null +++ b/etherparse/src/transport/igmp/unknown_header.rs @@ -0,0 +1,34 @@ +/// Unknown IGMP header with an, to etherparse, unknown type id. +/// +/// ```text +/// 0 1 2 3 +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// | Type = 0x22 | raw_byte_1 | Checksum | | part of header & +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | this type +/// | raw_bytes_4_7 | ↓ +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// | | | +/// . . | +/// . .............. . | part of payload +/// . . | +/// | | ↓ +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct UnknownHeader { + /// Unknown type + pub igmp_type: u8, + + /// Raw byte value after the type value. + pub raw_byte_1: u8, + + /// Raw byte values after the checksum. + pub raw_bytes_4_7: [u8; 4], +} + +impl UnknownHeader { + /// Number of bytes/octets an [`UnknownHeader`] takes up in serialized form. + pub const LEN: usize = 8; +} diff --git a/etherparse/src/transport/igmp_header.rs b/etherparse/src/transport/igmp_header.rs index 53597561..6d6e5a31 100644 --- a/etherparse/src/transport/igmp_header.rs +++ b/etherparse/src/transport/igmp_header.rs @@ -1,9 +1,6 @@ use arrayvec::ArrayVec; -use crate::{ - err::{igmp::HeaderSliceError, LenError}, - *, -}; +use crate::{err::LenError, *}; /// A header of an IGMP packet. /// @@ -125,8 +122,8 @@ impl IgmpHeader { /// * IGMPv3 Query: length >= 12 octets. /// * Query Messages of any other length (e.g. 9, 10 or 11 octets) /// MUST be silently ignored. This parser surfaces them as a - /// [`err::igmp::HeaderSliceError::Len`] error so that callers can - /// make that decision explicitly. + /// [`err::LenError`] so that callers can make that decision + /// explicitly. /// /// IGMPv1 and IGMPv2 queries are returned as /// [`IgmpType::MembershipQuery`] (the `max_response_time` field is @@ -138,26 +135,24 @@ impl IgmpHeader { /// follows the fixed header (e.g. the source addresses of an IGMPv3 /// query or the group records of an IGMPv3 membership report). /// + /// IGMP type bytes that do not match any of the message types + /// defined in RFC 1112, RFC 2236 or RFC 9776 are returned as + /// [`IgmpType::Unknown`] with the raw header bytes preserved. + /// /// # Errors /// - /// * [`err::igmp::HeaderSliceError::Len`] if the slice is too small - /// to contain a complete header (less than 8 bytes for known - /// types, or 9-11 bytes for a Membership Query). - /// * [`err::igmp::HeaderSliceError::Content`] with - /// [`err::igmp::HeaderError::UnknownType`] if the IGMP type byte - /// does not match any of the message types defined in - /// RFC 1112, RFC 2236 or RFC 9776. - pub fn from_slice(slice: &[u8]) -> Result<(IgmpHeader, &[u8]), err::igmp::HeaderSliceError> { - use err::igmp::HeaderSliceError::*; - + /// * [`err::LenError`] if the slice is too small to contain a + /// complete header (less than 8 bytes for any type, or 9-11 bytes + /// for a Membership Query). + pub fn from_slice(slice: &[u8]) -> Result<(IgmpHeader, &[u8]), LenError> { if slice.len() < Self::MIN_LEN { - return Err(Len(err::LenError { + return Err(LenError { required_len: Self::MIN_LEN, len: slice.len(), len_source: LenSource::Slice, layer: err::Layer::Igmp, layer_start_offset: 0, - })); + }); } // SAFETY: length checked above to be >= MIN_LEN (8). @@ -229,13 +224,13 @@ impl IgmpHeader { }, )) } else { - Err(HeaderSliceError::Len(LenError { + Err(LenError { required_len: igmp::MembershipQueryWithSourcesHeader::LEN, len: slice.len(), len_source: LenSource::Slice, layer: err::Layer::Igmp, layer_start_offset: 0, - })) + }) } } igmp::IGMPV1_TYPE_MEMBERSHIP_REPORT => Ok(( @@ -311,7 +306,24 @@ impl IgmpHeader { }, )) } - _ => Err(Content(err::igmp::HeaderError::UnknownType { type_u8 })), + _ => Ok(( + IgmpHeader { + igmp_type: IgmpType::Unknown(igmp::UnknownHeader { + igmp_type: type_u8, + raw_byte_1: max_resp, + raw_bytes_4_7: group_address, + }), + checksum, + }, + // SAFETY: Safe as the slice was previously verified to have at least the length + // Self::MIN_LEN. + unsafe { + core::slice::from_raw_parts( + slice.as_ptr().add(Self::MIN_LEN), + slice.len() - Self::MIN_LEN, + ) + }, + )), } } @@ -352,6 +364,9 @@ impl IgmpHeader { LeaveGroup(t) => checksum::Sum16BitWords::new() .add_2bytes([igmp::IGMPV2_TYPE_LEAVE_GROUP, 0]) .add_4bytes(t.group_address.octets), + Unknown(t) => checksum::Sum16BitWords::new() + .add_2bytes([t.igmp_type, t.raw_byte_1]) + .add_4bytes(t.raw_bytes_4_7), }; sum.add_slice(payload).ones_complement().to_be() } @@ -367,6 +382,7 @@ impl IgmpHeader { MembershipReportV2(_) => igmp::MembershipReportV2Type::LEN, MembershipReportV3(_) => igmp::MembershipReportV3Header::LEN, LeaveGroup(_) => igmp::LeaveGroupType::LEN, + Unknown(_) => igmp::UnknownHeader::LEN, } } @@ -498,6 +514,27 @@ impl IgmpHeader { } bytes } + Unknown(t) => { + let mut bytes = ArrayVec::from([ + t.igmp_type, + t.raw_byte_1, + c[0], + c[1], + t.raw_bytes_4_7[0], + t.raw_bytes_4_7[1], + t.raw_bytes_4_7[2], + t.raw_bytes_4_7[3], + 0, + 0, + 0, + 0, + ]); + // SAFETY: Safe as u8 has no destruction behavior and as 8 is smaller then 12. + unsafe { + bytes.set_len(8); + } + bytes + } } } } @@ -543,6 +580,8 @@ mod test { igmp::IGMPV3_TYPE_MEMBERSHIP_REPORT, ].contains(t), ), + unknown_raw_byte_1 in any::(), + unknown_raw_bytes_4_7 in any::<[u8;4]>(), ) { let raw_byte_8 = ((query_flags & igmp::MembershipQueryWithSourcesHeader::RAW_BYTE_8_MASK_FLAGS) << igmp::MembershipQueryWithSourcesHeader::RAW_BYTE_8_OFFSET_FLAGS) | ((s_flag as u8) << 3) @@ -715,7 +754,7 @@ mod test { // serialize & deserialize all types { - let cases: [IgmpType; 6] = [ + let cases: [IgmpType; 7] = [ IgmpType::MembershipQuery(igmp::MembershipQueryType { max_response_time, group_address: group_address.into(), @@ -740,6 +779,11 @@ mod test { IgmpType::LeaveGroup(igmp::LeaveGroupType { group_address: group_address.into(), }), + IgmpType::Unknown(igmp::UnknownHeader { + igmp_type: unknown_type, + raw_byte_1: unknown_raw_byte_1, + raw_bytes_4_7: unknown_raw_bytes_4_7, + }), ]; for igmp_type in cases { @@ -757,13 +801,13 @@ mod test { for bad_len in 0..IgmpHeader::MIN_LEN { prop_assert_eq!( IgmpHeader::from_slice(&buf[..bad_len]), - Err(err::igmp::HeaderSliceError::Len(err::LenError { + Err(err::LenError { required_len: IgmpHeader::MIN_LEN, len: bad_len, len_source: LenSource::Slice, layer: err::Layer::Igmp, layer_start_offset: 0, - })) + }) ); } } @@ -775,27 +819,44 @@ mod test { buf[0] = igmp::IGMP_TYPE_MEMBERSHIP_QUERY; prop_assert_eq!( IgmpHeader::from_slice(&buf), - Err(err::igmp::HeaderSliceError::Len(err::LenError { + Err(err::LenError { required_len: IgmpHeader::MAX_LEN, len: bad_len, len_source: LenSource::Slice, layer: err::Layer::Igmp, layer_start_offset: 0, - })) + }) ); } } - // unknown type + // unknown type is parsed as IgmpType::Unknown (with the raw + // header bytes preserved) instead of returning an error. { - let mut buf = [0u8; IgmpHeader::MIN_LEN]; - buf[0] = unknown_type; + let bytes = [ + unknown_type, + unknown_raw_byte_1, + cs_be[0], cs_be[1], + unknown_raw_bytes_4_7[0], unknown_raw_bytes_4_7[1], + unknown_raw_bytes_4_7[2], unknown_raw_bytes_4_7[3], + ]; + let mut full = Vec::with_capacity(bytes.len() + suffix.len()); + full.extend_from_slice(&bytes); + full.extend_from_slice(&suffix); + + let (header, rest) = IgmpHeader::from_slice(&full).unwrap(); prop_assert_eq!( - IgmpHeader::from_slice(&buf), - Err(err::igmp::HeaderSliceError::Content( - err::igmp::HeaderError::UnknownType { type_u8: unknown_type } - )) + header, + IgmpHeader { + igmp_type: IgmpType::Unknown(igmp::UnknownHeader { + igmp_type: unknown_type, + raw_byte_1: unknown_raw_byte_1, + raw_bytes_4_7: unknown_raw_bytes_4_7, + }), + checksum, + } ); + prop_assert_eq!(rest, suffix.as_slice()); } } } @@ -827,6 +888,18 @@ mod test { report_flags in any::<[u8;2]>(), num_of_records in any::(), payload in proptest::collection::vec(any::(), 0..1024usize), + unknown_type in any::().prop_filter( + "must not be a known IGMP type", + |t| ![ + igmp::IGMP_TYPE_MEMBERSHIP_QUERY, + igmp::IGMPV1_TYPE_MEMBERSHIP_REPORT, + igmp::IGMPV2_TYPE_MEMBERSHIP_REPORT, + igmp::IGMPV2_TYPE_LEAVE_GROUP, + igmp::IGMPV3_TYPE_MEMBERSHIP_REPORT, + ].contains(t), + ), + unknown_raw_byte_1 in any::(), + unknown_raw_bytes_4_7 in any::<[u8;4]>(), ) { let raw_byte_8 = ((query_flags & igmp::MembershipQueryWithSourcesHeader::RAW_BYTE_8_MASK_FLAGS) << igmp::MembershipQueryWithSourcesHeader::RAW_BYTE_8_OFFSET_FLAGS) | ((s_flag as u8) << 3) @@ -946,6 +1019,25 @@ mod test { assert_rfc_verifies(&IgmpHeader::with_checksum(igmp_type, &payload), &payload); } + // unknown + { + let igmp_type = IgmpType::Unknown(igmp::UnknownHeader { + igmp_type: unknown_type, + raw_byte_1: unknown_raw_byte_1, + raw_bytes_4_7: unknown_raw_bytes_4_7, + }); + let header = IgmpHeader { igmp_type: igmp_type.clone(), checksum: 0 }; + + let expected = checksum::Sum16BitWords::new() + .add_2bytes([unknown_type, unknown_raw_byte_1]) + .add_4bytes(unknown_raw_bytes_4_7) + .add_slice(&payload) + .ones_complement() + .to_be(); + prop_assert_eq!(expected, header.calc_checksum(&payload)); + assert_rfc_verifies(&IgmpHeader::with_checksum(igmp_type, &payload), &payload); + } + // Hand-rolled IGMPv2 Membership Report example with an externally // computed checksum. Verifies that we produce the exact same // checksum as the RFC formula applied byte-for-byte. @@ -999,6 +1091,18 @@ mod test { report_flags in any::<[u8;2]>(), num_of_records in any::(), payload in proptest::collection::vec(any::(), 0..1024usize), + unknown_type in any::().prop_filter( + "must not be a known IGMP type", + |t| ![ + igmp::IGMP_TYPE_MEMBERSHIP_QUERY, + igmp::IGMPV1_TYPE_MEMBERSHIP_REPORT, + igmp::IGMPV2_TYPE_MEMBERSHIP_REPORT, + igmp::IGMPV2_TYPE_LEAVE_GROUP, + igmp::IGMPV3_TYPE_MEMBERSHIP_REPORT, + ].contains(t), + ), + unknown_raw_byte_1 in any::(), + unknown_raw_bytes_4_7 in any::<[u8;4]>(), ) { let raw_byte_8 = ((query_flags & igmp::MembershipQueryWithSourcesHeader::RAW_BYTE_8_MASK_FLAGS) << igmp::MembershipQueryWithSourcesHeader::RAW_BYTE_8_OFFSET_FLAGS) | ((s_flag as u8) << 3) @@ -1007,7 +1111,7 @@ mod test { // For every IGMP variant, with_checksum must // 1) preserve the supplied IgmpType verbatim, and // 2) populate the checksum field with calc_checksum's result. - let cases: [IgmpType; 6] = [ + let cases: [IgmpType; 7] = [ IgmpType::MembershipQuery(igmp::MembershipQueryType { max_response_time, group_address: group_address.into(), @@ -1032,6 +1136,11 @@ mod test { IgmpType::LeaveGroup(igmp::LeaveGroupType { group_address: group_address.into(), }), + IgmpType::Unknown(igmp::UnknownHeader { + igmp_type: unknown_type, + raw_byte_1: unknown_raw_byte_1, + raw_bytes_4_7: unknown_raw_bytes_4_7, + }), ]; for igmp_type in cases { diff --git a/etherparse/src/transport/igmp_type.rs b/etherparse/src/transport/igmp_type.rs index ec2f60fd..c534e225 100644 --- a/etherparse/src/transport/igmp_type.rs +++ b/etherparse/src/transport/igmp_type.rs @@ -20,4 +20,7 @@ pub enum IgmpType { /// Leave Group message type (introduced in IGMPv2, type = 0x17). LeaveGroup(igmp::LeaveGroupType), + + /// Unknown type of IGMP message. + Unknown(igmp::UnknownHeader), } From 84d144dbb13af7f7fc9891e4dc932621c2599a44 Mon Sep 17 00:00:00 2001 From: Julian Schmid Date: Sat, 2 May 2026 10:24:44 +0200 Subject: [PATCH 10/15] Applied fmt --- .../transport/igmp/membership_query_with_sources_header.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/etherparse/src/transport/igmp/membership_query_with_sources_header.rs b/etherparse/src/transport/igmp/membership_query_with_sources_header.rs index f25b7a54..03faf22f 100644 --- a/etherparse/src/transport/igmp/membership_query_with_sources_header.rs +++ b/etherparse/src/transport/igmp/membership_query_with_sources_header.rs @@ -106,8 +106,8 @@ impl MembershipQueryWithSourcesHeader { /// Sets the QRV (Querier's Robustness Variable) in /// the `raw_byte_8` field. pub fn set_qrv(&mut self, value: Qrv) { - self.raw_byte_8 = - (self.raw_byte_8 & (!Self::RAW_BYTE_8_MASK_QRV)) | (value.value() & Self::RAW_BYTE_8_MASK_QRV); + self.raw_byte_8 = (self.raw_byte_8 & (!Self::RAW_BYTE_8_MASK_QRV)) + | (value.value() & Self::RAW_BYTE_8_MASK_QRV); } } @@ -254,4 +254,3 @@ mod test { } } } - From 5eb371a38aa06e413cd2b2edf7701e6e82d4906f Mon Sep 17 00:00:00 2001 From: Julian Schmid Date: Sat, 2 May 2026 10:25:40 +0200 Subject: [PATCH 11/15] Ressolved cargo doc warning --- etherparse/src/transport/igmp/membership_report_v3_header.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etherparse/src/transport/igmp/membership_report_v3_header.rs b/etherparse/src/transport/igmp/membership_report_v3_header.rs index cafd0cee..96f63099 100644 --- a/etherparse/src/transport/igmp/membership_report_v3_header.rs +++ b/etherparse/src/transport/igmp/membership_report_v3_header.rs @@ -36,7 +36,7 @@ pub struct MembershipReportV3Header { /// Additional `Flags`. /// /// Documented in the IANA page - /// [https://www.iana.org/assignments/igmp-type-numbers/igmp-type-numbers.xhtml#igmp-mld-report-message-flags]. + /// . pub flags: [u8; 2], /// The number of group records in the membership report From c5e93d6c1a1051c7f7e421f4f081e270cc7476db Mon Sep 17 00:00:00 2001 From: Julian Schmid Date: Sat, 2 May 2026 10:31:42 +0200 Subject: [PATCH 12/15] Fix type in UnknownHeader diagram Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- etherparse/src/transport/igmp/unknown_header.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etherparse/src/transport/igmp/unknown_header.rs b/etherparse/src/transport/igmp/unknown_header.rs index 8ae027cd..0b8dedad 100644 --- a/etherparse/src/transport/igmp/unknown_header.rs +++ b/etherparse/src/transport/igmp/unknown_header.rs @@ -5,7 +5,7 @@ /// 0 1 2 3 /// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -/// | Type = 0x22 | raw_byte_1 | Checksum | | part of header & +/// | igmp_type | raw_byte_1 | Checksum | | part of header & /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | this type /// | raw_bytes_4_7 | ↓ /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - From 5b0bd63d230b76f9da36a47ba4bb1bbab8340004 Mon Sep 17 00:00:00 2001 From: Julian Schmid Date: Sat, 2 May 2026 14:05:42 +0200 Subject: [PATCH 13/15] Add report group record types --- .../igmp/membership_report_v3_header.rs | 5 +- etherparse/src/transport/igmp/mod.rs | 6 + .../igmp/report_group_record_type.rs | 51 ++++ .../igmp/report_group_record_v3_header.rs | 267 ++++++++++++++++++ 4 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 etherparse/src/transport/igmp/report_group_record_type.rs create mode 100644 etherparse/src/transport/igmp/report_group_record_v3_header.rs diff --git a/etherparse/src/transport/igmp/membership_report_v3_header.rs b/etherparse/src/transport/igmp/membership_report_v3_header.rs index 96f63099..b57b36b0 100644 --- a/etherparse/src/transport/igmp/membership_report_v3_header.rs +++ b/etherparse/src/transport/igmp/membership_report_v3_header.rs @@ -1,4 +1,7 @@ -/// IGMPv3 Membership Report Message header part (without checksum). +/// IGMPv3 Membership Report Message header part. +/// +/// Note that the checksum is not stored in this type and is stored in +/// [`crate::IgmpHeader`]. /// /// ```text /// 0 1 2 3 diff --git a/etherparse/src/transport/igmp/mod.rs b/etherparse/src/transport/igmp/mod.rs index 186c1076..303533fe 100644 --- a/etherparse/src/transport/igmp/mod.rs +++ b/etherparse/src/transport/igmp/mod.rs @@ -25,6 +25,12 @@ pub use membership_report_v3_header::*; mod qrv; pub use qrv::*; +mod report_group_record_type; +pub use report_group_record_type::*; + +mod report_group_record_v3_header; +pub use report_group_record_v3_header::*; + mod unknown_header; pub use unknown_header::*; diff --git a/etherparse/src/transport/igmp/report_group_record_type.rs b/etherparse/src/transport/igmp/report_group_record_type.rs new file mode 100644 index 00000000..9d93551c --- /dev/null +++ b/etherparse/src/transport/igmp/report_group_record_type.rs @@ -0,0 +1,51 @@ +/// Type value within a [`crate::igmp::ReportGroupRecordV3Header`]. +#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct ReportGroupRecordType(pub u8); + +impl ReportGroupRecordType { + /// Indicates that the interface has a filter-mode of INCLUDE + /// for the specified multicast address. The Source Address [i] + /// fields in this Group Record contain the interface's + /// source-list for the specified multicast address, if + /// it is non-empty. + pub const MODE_IS_INCLUDE: ReportGroupRecordType = ReportGroupRecordType(1); + + /// Indicates that the interface has a filter-mode of EXCLUDE for + /// the specified multicast address. The Source Address [i] fields + /// in this Group Record contain the interface's source-list for + /// the specified multicast address, if it is non-empty. An SSM-aware + /// host SHOULD NOT send a MODE_IS_EXCLUDE record type for multicast + /// addresses that fall within the SSM address range as they will be + /// ignored by SSM-aware routers + pub const MODE_IS_EXCLUDE: ReportGroupRecordType = ReportGroupRecordType(2); + + /// Indicates that the interface has changed to INCLUDE filter-mode for + /// the specified multicast address. The Source Address [i] fields in + /// this Group Record contain the interface's new source-list for the + /// specified multicast address, if it is non-empty. + pub const CHANGE_TO_INCLUDE_MODE: ReportGroupRecordType = ReportGroupRecordType(3); + + /// Indicates that the interface has changed to EXCLUDE filter-mode for + /// the specified multicast address. The Source Address [i] fields in + /// this Group Record contain the interface's new source-list for the + /// specified multicast address, if it is non-empty. An SSM-aware host + /// SHOULD NOT send a CHANGE_TO_EXCLUDE_MODE record type for multicast + /// addresses that fall within the SSM address range. + pub const CHANGE_TO_EXCLUDE_MODE: ReportGroupRecordType = ReportGroupRecordType(4); + + /// Indicates that the Source Address [i] fields in this Group Record + /// contain a list of the additional sources that the system wishes to + /// receive, for packets sent to the specified multicast address. If + /// the change was to an INCLUDE source-list, these are the addresses + /// that were added to the list; if the change was to an EXCLUDE + /// source-list, these are the addresses that were deleted from the list. + pub const ALLOW_NEW_SOURCES: ReportGroupRecordType = ReportGroupRecordType(5); + + /// Indicates that the Source Address [i] fields in this Group Record + /// contain a list of the sources that the system no longer wishes to + /// receive, for packets sent to the specified multicast address. If + /// the change was to an INCLUDE source-list, these are the addresses + /// that were deleted from the list; if the change was to an EXCLUDE + /// source-list, these are the addresses that were added to the list. + pub const BLOCK_OLD_SOURCES: ReportGroupRecordType = ReportGroupRecordType(6); +} diff --git a/etherparse/src/transport/igmp/report_group_record_v3_header.rs b/etherparse/src/transport/igmp/report_group_record_v3_header.rs new file mode 100644 index 00000000..427a9ef1 --- /dev/null +++ b/etherparse/src/transport/igmp/report_group_record_v3_header.rs @@ -0,0 +1,267 @@ +use crate::{err::LenError, igmp::ReportGroupRecordType, *}; + +/// Header part of an "IGMPv3 Report Group Record". +/// +/// The header contains the following parts for the group record: +/// +/// ```text +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// | Record Type | Aux Data Len | Number of Sources (N) | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | part of header and this type +/// | Multicast Address | ↓ +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// | Source Address [1] | +/// +- -+ +/// | Source Address [2] | +/// +- -+ +/// . . . +/// . . . +/// . . . +/// +- -+ +/// | Source Address [N] | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// . . +/// . Auxiliary Data . +/// . . +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ReportGroupRecordV3Header { + /// Identifies what type of record + pub record_type: ReportGroupRecordType, + + /// The Aux Data Len field contains the length of the Auxiliary Data + /// field in this Group Record, in units of 32-bit words. It may + /// contain zero, to indicate the absence of any auxiliary data. + pub aux_data_len: u8, + + /// The Number of Sources (N) field specifies how many source + /// addresses are present in this Group Record. + pub num_of_sources: u16, + + /// The Multicast Address field contains the IP multicast address + /// to which this Group Record pertains. + pub multicast_address: [u8; 4], +} + +impl ReportGroupRecordV3Header { + /// Number of bytes/octets an [`ReportGroupRecordV3Header`] takes up in serialized form. + pub const LEN: usize = 8; + + /// Reads an [`ReportGroupRecordV3Header`] from a slice and returns a + /// tuple containing the resulting header and the unused part of the + /// slice. + /// + /// The "unused part" is the slice that follows the fixed 8 byte + /// header (i.e. the source addresses and auxiliary data of the + /// group record). + /// + /// # Errors + /// + /// * [`err::LenError`] if the slice is shorter than + /// [`ReportGroupRecordV3Header::LEN`] (8 bytes). + pub fn from_slice(slice: &[u8]) -> Result<(ReportGroupRecordV3Header, &[u8]), LenError> { + if slice.len() < Self::LEN { + return Err(LenError { + required_len: Self::LEN, + len: slice.len(), + len_source: LenSource::Slice, + layer: err::Layer::Igmp, + layer_start_offset: 0, + }); + } + + // SAFETY: length checked above to be >= Self::LEN (8). + let header = unsafe { + ReportGroupRecordV3Header { + record_type: ReportGroupRecordType(*slice.get_unchecked(0)), + aux_data_len: *slice.get_unchecked(1), + num_of_sources: u16::from_be_bytes([ + *slice.get_unchecked(2), + *slice.get_unchecked(3), + ]), + multicast_address: [ + *slice.get_unchecked(4), + *slice.get_unchecked(5), + *slice.get_unchecked(6), + *slice.get_unchecked(7), + ], + } + }; + + // SAFETY: Safe as the slice was previously verified to have at least the length + // Self::LEN. + let rest = unsafe { + core::slice::from_raw_parts(slice.as_ptr().add(Self::LEN), slice.len() - Self::LEN) + }; + + Ok((header, rest)) + } + + /// Converts the header to its on-the-wire byte representation. + pub fn to_bytes(&self) -> [u8; Self::LEN] { + let n = self.num_of_sources.to_be_bytes(); + [ + self.record_type.0, + self.aux_data_len, + n[0], + n[1], + self.multicast_address[0], + self.multicast_address[1], + self.multicast_address[2], + self.multicast_address[3], + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + use alloc::{format, vec, vec::Vec}; + use proptest::prelude::*; + + #[test] + fn constants() { + assert_eq!(8, ReportGroupRecordV3Header::LEN); + } + + proptest! { + #[test] + fn from_slice( + record_type in any::(), + aux_data_len in any::(), + num_of_sources in any::(), + multicast_address in any::<[u8; 4]>(), + suffix in proptest::collection::vec(any::(), 0..256usize), + ) { + let n_be = num_of_sources.to_be_bytes(); + let head = [ + record_type, + aux_data_len, + n_be[0], n_be[1], + multicast_address[0], multicast_address[1], + multicast_address[2], multicast_address[3], + ]; + + // exact length (no trailing bytes) + { + let (header, rest) = ReportGroupRecordV3Header::from_slice(&head).unwrap(); + prop_assert_eq!( + header, + ReportGroupRecordV3Header { + record_type: ReportGroupRecordType(record_type), + aux_data_len, + num_of_sources, + multicast_address, + } + ); + prop_assert!(rest.is_empty()); + } + + // with trailing bytes (sources + aux data) returned in `rest` + { + let mut full = Vec::with_capacity(head.len() + suffix.len()); + full.extend_from_slice(&head); + full.extend_from_slice(&suffix); + + let (header, rest) = ReportGroupRecordV3Header::from_slice(&full).unwrap(); + prop_assert_eq!( + header, + ReportGroupRecordV3Header { + record_type: ReportGroupRecordType(record_type), + aux_data_len, + num_of_sources, + multicast_address, + } + ); + prop_assert_eq!(rest, suffix.as_slice()); + } + + // length errors for any slice shorter than LEN + { + let buf = [0u8; ReportGroupRecordV3Header::LEN]; + for bad_len in 0..ReportGroupRecordV3Header::LEN { + prop_assert_eq!( + ReportGroupRecordV3Header::from_slice(&buf[..bad_len]), + Err(err::LenError { + required_len: ReportGroupRecordV3Header::LEN, + len: bad_len, + len_source: LenSource::Slice, + layer: err::Layer::Igmp, + layer_start_offset: 0, + }) + ); + } + } + } + } + + proptest! { + #[test] + fn to_bytes( + record_type in any::(), + aux_data_len in any::(), + num_of_sources in any::(), + multicast_address in any::<[u8; 4]>(), + ) { + let header = ReportGroupRecordV3Header { + record_type: ReportGroupRecordType(record_type), + aux_data_len, + num_of_sources, + multicast_address, + }; + + let n_be = num_of_sources.to_be_bytes(); + let expected = [ + record_type, + aux_data_len, + n_be[0], n_be[1], + multicast_address[0], multicast_address[1], + multicast_address[2], multicast_address[3], + ]; + + prop_assert_eq!(header.to_bytes(), expected); + } + } + + proptest! { + #[test] + fn roundtrip( + record_type in any::(), + aux_data_len in any::(), + num_of_sources in any::(), + multicast_address in any::<[u8; 4]>(), + suffix in proptest::collection::vec(any::(), 0..256usize), + ) { + let original = ReportGroupRecordV3Header { + record_type: ReportGroupRecordType(record_type), + aux_data_len, + num_of_sources, + multicast_address, + }; + + // serialize then deserialize: yields the same header and an empty rest. + { + let bytes = original.to_bytes(); + let (parsed, rest) = ReportGroupRecordV3Header::from_slice(&bytes).unwrap(); + prop_assert_eq!(parsed, original.clone()); + prop_assert!(rest.is_empty()); + } + + // serialize, append arbitrary suffix bytes, deserialize: same + // header, suffix returned as `rest`. + { + let bytes = original.to_bytes(); + let mut full = vec![]; + full.extend_from_slice(&bytes); + full.extend_from_slice(&suffix); + + let (parsed, rest) = ReportGroupRecordV3Header::from_slice(&full).unwrap(); + prop_assert_eq!(parsed, original); + prop_assert_eq!(rest, suffix.as_slice()); + } + } + } +} From bbbee4829a7c0cab4ff4a47388a322e9ae5cfc26 Mon Sep 17 00:00:00 2001 From: Julian Schmid Date: Sat, 2 May 2026 14:21:31 +0200 Subject: [PATCH 14/15] IGMP: Flagged type smaller then 64bits with copy --- etherparse/src/transport/igmp/leave_group_type.rs | 2 +- etherparse/src/transport/igmp/membership_query_type.rs | 2 +- etherparse/src/transport/igmp/membership_report_v1_type.rs | 2 +- etherparse/src/transport/igmp/membership_report_v2_type.rs | 2 +- etherparse/src/transport/igmp/membership_report_v3_header.rs | 2 +- etherparse/src/transport/igmp/report_group_record_v3_header.rs | 2 +- etherparse/src/transport/igmp/unknown_header.rs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/etherparse/src/transport/igmp/leave_group_type.rs b/etherparse/src/transport/igmp/leave_group_type.rs index a7c73d4c..cbf20b9f 100644 --- a/etherparse/src/transport/igmp/leave_group_type.rs +++ b/etherparse/src/transport/igmp/leave_group_type.rs @@ -11,7 +11,7 @@ use crate::igmp::GroupAddress; /// | Group Address | /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ /// ``` -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct LeaveGroupType { /// The IP multicast group address of the group being left. pub group_address: GroupAddress, diff --git a/etherparse/src/transport/igmp/membership_query_type.rs b/etherparse/src/transport/igmp/membership_query_type.rs index b66ff73e..bfb6162d 100644 --- a/etherparse/src/transport/igmp/membership_query_type.rs +++ b/etherparse/src/transport/igmp/membership_query_type.rs @@ -16,7 +16,7 @@ use crate::igmp::GroupAddress; /// | Group Address | /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ /// ``` -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct MembershipQueryType { /// The maximum response time for the membership report /// (only for IGMPv2, set to 0 for IGMPv1). diff --git a/etherparse/src/transport/igmp/membership_report_v1_type.rs b/etherparse/src/transport/igmp/membership_report_v1_type.rs index 9c04a970..069f1b91 100644 --- a/etherparse/src/transport/igmp/membership_report_v1_type.rs +++ b/etherparse/src/transport/igmp/membership_report_v1_type.rs @@ -11,7 +11,7 @@ use crate::igmp::GroupAddress; /// | Group Address | /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ /// ``` -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct MembershipReportV1Type { /// IP multicast group address of the group being reported pub group_address: GroupAddress, diff --git a/etherparse/src/transport/igmp/membership_report_v2_type.rs b/etherparse/src/transport/igmp/membership_report_v2_type.rs index d18b2e1a..6ea1fbce 100644 --- a/etherparse/src/transport/igmp/membership_report_v2_type.rs +++ b/etherparse/src/transport/igmp/membership_report_v2_type.rs @@ -10,7 +10,7 @@ use crate::igmp::GroupAddress; /// | Group Address | /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ /// ``` -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct MembershipReportV2Type { /// IP multicast group address of the group being reported pub group_address: GroupAddress, diff --git a/etherparse/src/transport/igmp/membership_report_v3_header.rs b/etherparse/src/transport/igmp/membership_report_v3_header.rs index b57b36b0..c9edb32d 100644 --- a/etherparse/src/transport/igmp/membership_report_v3_header.rs +++ b/etherparse/src/transport/igmp/membership_report_v3_header.rs @@ -34,7 +34,7 @@ /// | | ↓ /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// ``` -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct MembershipReportV3Header { /// Additional `Flags`. /// diff --git a/etherparse/src/transport/igmp/report_group_record_v3_header.rs b/etherparse/src/transport/igmp/report_group_record_v3_header.rs index 427a9ef1..4813ccb6 100644 --- a/etherparse/src/transport/igmp/report_group_record_v3_header.rs +++ b/etherparse/src/transport/igmp/report_group_record_v3_header.rs @@ -27,7 +27,7 @@ use crate::{err::LenError, igmp::ReportGroupRecordType, *}; /// | | /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ /// ``` -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ReportGroupRecordV3Header { /// Identifies what type of record pub record_type: ReportGroupRecordType, diff --git a/etherparse/src/transport/igmp/unknown_header.rs b/etherparse/src/transport/igmp/unknown_header.rs index 0b8dedad..eb8c5d6d 100644 --- a/etherparse/src/transport/igmp/unknown_header.rs +++ b/etherparse/src/transport/igmp/unknown_header.rs @@ -16,7 +16,7 @@ /// | | ↓ /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - /// ``` -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct UnknownHeader { /// Unknown type pub igmp_type: u8, From 3d2dee2a6b686445b4ede2a57de3d86e1fc06be7 Mon Sep 17 00:00:00 2001 From: Julian Schmid Date: Sat, 2 May 2026 19:11:36 +0200 Subject: [PATCH 15/15] Fix docstring warnings --- .../src/transport/igmp/report_group_record_type.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/etherparse/src/transport/igmp/report_group_record_type.rs b/etherparse/src/transport/igmp/report_group_record_type.rs index 9d93551c..164b841a 100644 --- a/etherparse/src/transport/igmp/report_group_record_type.rs +++ b/etherparse/src/transport/igmp/report_group_record_type.rs @@ -4,14 +4,14 @@ pub struct ReportGroupRecordType(pub u8); impl ReportGroupRecordType { /// Indicates that the interface has a filter-mode of INCLUDE - /// for the specified multicast address. The Source Address [i] + /// for the specified multicast address. The Source Address \[i\] /// fields in this Group Record contain the interface's /// source-list for the specified multicast address, if /// it is non-empty. pub const MODE_IS_INCLUDE: ReportGroupRecordType = ReportGroupRecordType(1); /// Indicates that the interface has a filter-mode of EXCLUDE for - /// the specified multicast address. The Source Address [i] fields + /// the specified multicast address. The Source Address \[i\] fields /// in this Group Record contain the interface's source-list for /// the specified multicast address, if it is non-empty. An SSM-aware /// host SHOULD NOT send a MODE_IS_EXCLUDE record type for multicast @@ -20,20 +20,20 @@ impl ReportGroupRecordType { pub const MODE_IS_EXCLUDE: ReportGroupRecordType = ReportGroupRecordType(2); /// Indicates that the interface has changed to INCLUDE filter-mode for - /// the specified multicast address. The Source Address [i] fields in + /// the specified multicast address. The Source Address \[i\] fields in /// this Group Record contain the interface's new source-list for the /// specified multicast address, if it is non-empty. pub const CHANGE_TO_INCLUDE_MODE: ReportGroupRecordType = ReportGroupRecordType(3); /// Indicates that the interface has changed to EXCLUDE filter-mode for - /// the specified multicast address. The Source Address [i] fields in + /// the specified multicast address. The Source Address \[i\] fields in /// this Group Record contain the interface's new source-list for the /// specified multicast address, if it is non-empty. An SSM-aware host /// SHOULD NOT send a CHANGE_TO_EXCLUDE_MODE record type for multicast /// addresses that fall within the SSM address range. pub const CHANGE_TO_EXCLUDE_MODE: ReportGroupRecordType = ReportGroupRecordType(4); - /// Indicates that the Source Address [i] fields in this Group Record + /// Indicates that the Source Address \[i\] fields in this Group Record /// contain a list of the additional sources that the system wishes to /// receive, for packets sent to the specified multicast address. If /// the change was to an INCLUDE source-list, these are the addresses @@ -41,7 +41,7 @@ impl ReportGroupRecordType { /// source-list, these are the addresses that were deleted from the list. pub const ALLOW_NEW_SOURCES: ReportGroupRecordType = ReportGroupRecordType(5); - /// Indicates that the Source Address [i] fields in this Group Record + /// Indicates that the Source Address \[i\] fields in this Group Record /// contain a list of the sources that the system no longer wishes to /// receive, for packets sent to the specified multicast address. If /// the change was to an INCLUDE source-list, these are the addresses