diff --git a/README.md b/README.md index 155dc9b4..013ac3a3 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,9 @@ 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) +* 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/layer.rs b/etherparse/src/err/layer.rs index d977fb29..44767bdf 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 IGMP packet. + Igmp, /// 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", + Igmp => "IGMP 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"), + Igmp => write!(f, "IGMP 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"), + (Igmp, "IGMP 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"), + (Igmp, "IGMP packet"), (Arp, "Address Resolution Protocol packet"), ]; for test in tests { 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/lib.rs b/etherparse/src/lib.rs index e1860b73..67f40d1f 100644 --- a/etherparse/src/lib.rs +++ b/etherparse/src/lib.rs @@ -281,7 +281,10 @@ //! * [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) +//! * 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: // // Clippy triggers triggers errors like the following if the warning stays enabled: 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/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..cbf20b9f --- /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, Copy, 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 [`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 new file mode 100644 index 00000000..33166834 --- /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 IGMPv3). +/// +/// 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..bfb6162d --- /dev/null +++ b/etherparse/src/transport/igmp/membership_query_type.rs @@ -0,0 +1,44 @@ +use crate::igmp::GroupAddress; + +/// 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 +/// 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, Copy, 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 [`MembershipQueryType`] 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..03faf22f --- /dev/null +++ b/etherparse/src/transport/igmp/membership_query_with_sources_header.rs @@ -0,0 +1,256 @@ +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 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +/// | Flags |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 ignored. + pub group_address: GroupAddress, + + /// Raw byte containing "flags", "s" & "QRV" (the getters & setters + /// methods can be used to get & set the different values). + pub raw_byte_8: u8, + + /// QQIC (Querier's Query Interval Code). + pub qqic: u8, + + /// 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_v1_type.rs b/etherparse/src/transport/igmp/membership_report_v1_type.rs new file mode 100644 index 00000000..069f1b91 --- /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, Copy, 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 [`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 new file mode 100644 index 00000000..6ea1fbce --- /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, Copy, 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 [`MembershipReportV2Type`] 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..c9edb32d --- /dev/null +++ b/etherparse/src/transport/igmp/membership_report_v3_header.rs @@ -0,0 +1,55 @@ +/// 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 +/// 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 +/// | Flags | Number of Group Records (M) | ↓ +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// | | | +/// . . | +/// . Group Record [1] . | +/// . . | +/// | | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +/// | | | +/// . . | +/// . Group Record [2] . | part of payload +/// . . | +/// | | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +/// | . | | +/// . . . | +/// | . | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +/// | | | +/// . . | +/// . Group Record [M] . | +/// . . | +/// | | ↓ +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct MembershipReportV3Header { + /// Additional `Flags`. + /// + /// Documented in the IANA page + /// . + 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/mod.rs b/etherparse/src/transport/igmp/mod.rs new file mode 100644 index 00000000..303533fe --- /dev/null +++ b/etherparse/src/transport/igmp/mod.rs @@ -0,0 +1,64 @@ +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::*; + +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::*; + +/// "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..7dfddd9e --- /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::MembershipQueryWithSourcesHeader`]. +/// +/// Established in +/// [RFC-9776](https://datatracker.ietf.org/doc/html/rfc9776). +#[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 the IGMPv3 Membership Query QRV. + pub const MAX_U8: u8 = 0b0000_0111; + + /// Maximum value of the IGMPv3 Membership Query QRV. + 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/report_group_record_type.rs b/etherparse/src/transport/igmp/report_group_record_type.rs new file mode 100644 index 00000000..164b841a --- /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..4813ccb6 --- /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, Copy, 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()); + } + } + } +} diff --git a/etherparse/src/transport/igmp/unknown_header.rs b/etherparse/src/transport/igmp/unknown_header.rs new file mode 100644 index 00000000..eb8c5d6d --- /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 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// | igmp_type | raw_byte_1 | Checksum | | part of header & +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | this type +/// | raw_bytes_4_7 | ↓ +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// | | | +/// . . | +/// . .............. . | part of payload +/// . . | +/// | | ↓ +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// ``` +#[derive(Debug, Clone, Copy, 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 new file mode 100644 index 00000000..6d6e5a31 --- /dev/null +++ b/etherparse/src/transport/igmp_header.rs @@ -0,0 +1,1172 @@ +use arrayvec::ArrayVec; + +use crate::{err::LenError, *}; + +/// A header of an IGMP packet. +/// +/// The header contains the static part of an IGMP +/// packet. +/// +/// 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`]) +/// - 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 (the variable-length part is in the payload): +/// +/// - 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 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +/// | Flags |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 = 0x22 | Reserved | Checksum | | part of header & +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | this type +/// | Flags | Number of Group Records (M) | ↓ +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// | | | +/// . . | +/// . Group Record [1] . | +/// . . | +/// | | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +/// | | | +/// . . | +/// . Group Record [2] . | part of payload +/// . . | +/// | | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +/// | . | | +/// . . . | +/// | . | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +/// | | | +/// . . | +/// . Group Record [M] . | +/// . . | +/// | | ↓ +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - +/// ``` +#[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 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). + #[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. 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::LenError`] 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). + /// + /// 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::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(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 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)] + }); + Ok(( + IgmpHeader { + igmp_type: IgmpType::MembershipQueryWithSources( + igmp::MembershipQueryWithSourcesHeader { + max_response_code: igmp::MaxResponseCode(max_resp), + group_address: group_address.into(), + raw_byte_8, + 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(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 => { + // 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, + }, + // 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, + ) + }, + )) + } + _ => 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, + ) + }, + )), + } + } + + /// 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 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). + 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.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]) + .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(t.flags) + .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), + 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() + } + + /// 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, + Unknown(_) => igmp::UnknownHeader::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.raw_byte_8, + 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], + t.flags[0], + t.flags[1], + 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 + } + 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 + } + } + } +} + +#[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, + 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` + // 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), + ), + 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) + | (qrv_raw & 0b111); + 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 + { + 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] = raw_byte_8; + 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(), + raw_byte_8, + 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], + report_flags[0], report_flags[1], + 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 { + flags: report_flags, + num_of_records, + }), + checksum, + } + ); + prop_assert_eq!(rest, suffix.as_slice()); + } + + // serialize & deserialize all types + { + let cases: [IgmpType; 7] = [ + 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(), + raw_byte_8, + 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 { + flags: report_flags, + num_of_records, + }), + 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 { + 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::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::LenError { + required_len: IgmpHeader::MAX_LEN, + len: bad_len, + len_source: LenSource::Slice, + layer: err::Layer::Igmp, + layer_start_offset: 0, + }) + ); + } + } + + // unknown type is parsed as IgmpType::Unknown (with the raw + // header bytes preserved) instead of returning an error. + { + 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!( + 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()); + } + } + } + + 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, + 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), + 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) + | (qrv_raw & 0b111); + + // 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(), + raw_byte_8, + 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([ + raw_byte_8, + 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 { + 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(report_flags) + .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); + } + + // 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. + // + // 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 { + flags: [0, 0], + 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, + 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), + 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) + | (qrv_raw & 0b111); + + // 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; 7] = [ + 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(), + raw_byte_8, + 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 { + flags: report_flags, + num_of_records, + }), + 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 { + // 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..c534e225 --- /dev/null +++ b/etherparse/src/transport/igmp_type.rs @@ -0,0 +1,26 @@ +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 IGMPv3, type = 0x22 & dynamic size). + MembershipReportV3(igmp::MembershipReportV3Header), + + /// Leave Group message type (introduced in IGMPv2, type = 0x17). + LeaveGroup(igmp::LeaveGroupType), + + /// Unknown type of IGMP message. + Unknown(igmp::UnknownHeader), +} diff --git a/etherparse/src/transport/mod.rs b/etherparse/src/transport/mod.rs index 45e36b1a..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,6 +28,12 @@ pub use icmpv6_slice::*; mod icmpv6_type; pub use icmpv6_type::*; +mod igmp_type; +pub use igmp_type::*; + +mod igmp_header; +pub use igmp_header::*; + mod tcp_header; pub use tcp_header::*;