| use byteorder::{ByteOrder, NetworkEndian}; |
| use core::fmt; |
| |
| use super::{Error, Result}; |
| use crate::time::Duration; |
| use crate::wire::ip::checksum; |
| |
| use crate::wire::Ipv4Address; |
| |
| enum_with_unknown! { |
| /// Internet Group Management Protocol v1/v2 message version/type. |
| pub enum Message(u8) { |
| /// Membership Query |
| MembershipQuery = 0x11, |
| /// Version 2 Membership Report |
| MembershipReportV2 = 0x16, |
| /// Leave Group |
| LeaveGroup = 0x17, |
| /// Version 1 Membership Report |
| MembershipReportV1 = 0x12 |
| } |
| } |
| |
| /// A read/write wrapper around an Internet Group Management Protocol v1/v2 packet buffer. |
| #[derive(Debug)] |
| #[cfg_attr(feature = "defmt", derive(defmt::Format))] |
| pub struct Packet<T: AsRef<[u8]>> { |
| buffer: T, |
| } |
| |
| mod field { |
| use crate::wire::field::*; |
| |
| pub const TYPE: usize = 0; |
| pub const MAX_RESP_CODE: usize = 1; |
| pub const CHECKSUM: Field = 2..4; |
| pub const GROUP_ADDRESS: Field = 4..8; |
| } |
| |
| impl fmt::Display for Message { |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| match *self { |
| Message::MembershipQuery => write!(f, "membership query"), |
| Message::MembershipReportV2 => write!(f, "version 2 membership report"), |
| Message::LeaveGroup => write!(f, "leave group"), |
| Message::MembershipReportV1 => write!(f, "version 1 membership report"), |
| Message::Unknown(id) => write!(f, "{id}"), |
| } |
| } |
| } |
| |
| /// Internet Group Management Protocol v1/v2 defined in [RFC 2236]. |
| /// |
| /// [RFC 2236]: https://tools.ietf.org/html/rfc2236 |
| impl<T: AsRef<[u8]>> Packet<T> { |
| /// Imbue a raw octet buffer with IGMPv2 packet structure. |
| pub const fn new_unchecked(buffer: T) -> Packet<T> { |
| Packet { buffer } |
| } |
| |
| /// Shorthand for a combination of [new_unchecked] and [check_len]. |
| /// |
| /// [new_unchecked]: #method.new_unchecked |
| /// [check_len]: #method.check_len |
| pub fn new_checked(buffer: T) -> Result<Packet<T>> { |
| let packet = Self::new_unchecked(buffer); |
| packet.check_len()?; |
| Ok(packet) |
| } |
| |
| /// Ensure that no accessor method will panic if called. |
| /// Returns `Err(Error)` if the buffer is too short. |
| pub fn check_len(&self) -> Result<()> { |
| let len = self.buffer.as_ref().len(); |
| if len < field::GROUP_ADDRESS.end { |
| Err(Error) |
| } else { |
| Ok(()) |
| } |
| } |
| |
| /// Consume the packet, returning the underlying buffer. |
| pub fn into_inner(self) -> T { |
| self.buffer |
| } |
| |
| /// Return the message type field. |
| #[inline] |
| pub fn msg_type(&self) -> Message { |
| let data = self.buffer.as_ref(); |
| Message::from(data[field::TYPE]) |
| } |
| |
| /// Return the maximum response time, using the encoding specified in |
| /// [RFC 3376]: 4.1.1. Max Resp Code. |
| /// |
| /// [RFC 3376]: https://tools.ietf.org/html/rfc3376 |
| #[inline] |
| pub fn max_resp_code(&self) -> u8 { |
| let data = self.buffer.as_ref(); |
| data[field::MAX_RESP_CODE] |
| } |
| |
| /// Return the checksum field. |
| #[inline] |
| pub fn checksum(&self) -> u16 { |
| let data = self.buffer.as_ref(); |
| NetworkEndian::read_u16(&data[field::CHECKSUM]) |
| } |
| |
| /// Return the source address field. |
| #[inline] |
| pub fn group_addr(&self) -> Ipv4Address { |
| let data = self.buffer.as_ref(); |
| Ipv4Address::from_bytes(&data[field::GROUP_ADDRESS]) |
| } |
| |
| /// Validate the header checksum. |
| /// |
| /// # Fuzzing |
| /// This function always returns `true` when fuzzing. |
| pub fn verify_checksum(&self) -> bool { |
| if cfg!(fuzzing) { |
| return true; |
| } |
| |
| let data = self.buffer.as_ref(); |
| checksum::data(data) == !0 |
| } |
| } |
| |
| impl<T: AsRef<[u8]> + AsMut<[u8]>> Packet<T> { |
| /// Set the message type field. |
| #[inline] |
| pub fn set_msg_type(&mut self, value: Message) { |
| let data = self.buffer.as_mut(); |
| data[field::TYPE] = value.into() |
| } |
| |
| /// Set the maximum response time, using the encoding specified in |
| /// [RFC 3376]: 4.1.1. Max Resp Code. |
| #[inline] |
| pub fn set_max_resp_code(&mut self, value: u8) { |
| let data = self.buffer.as_mut(); |
| data[field::MAX_RESP_CODE] = value; |
| } |
| |
| /// Set the checksum field. |
| #[inline] |
| pub fn set_checksum(&mut self, value: u16) { |
| let data = self.buffer.as_mut(); |
| NetworkEndian::write_u16(&mut data[field::CHECKSUM], value) |
| } |
| |
| /// Set the group address field |
| #[inline] |
| pub fn set_group_address(&mut self, addr: Ipv4Address) { |
| let data = self.buffer.as_mut(); |
| data[field::GROUP_ADDRESS].copy_from_slice(addr.as_bytes()); |
| } |
| |
| /// Compute and fill in the header checksum. |
| pub fn fill_checksum(&mut self) { |
| self.set_checksum(0); |
| let checksum = { |
| let data = self.buffer.as_ref(); |
| !checksum::data(data) |
| }; |
| self.set_checksum(checksum) |
| } |
| } |
| |
| /// A high-level representation of an Internet Group Management Protocol v1/v2 header. |
| #[derive(Debug, PartialEq, Eq, Clone)] |
| #[cfg_attr(feature = "defmt", derive(defmt::Format))] |
| pub enum Repr { |
| MembershipQuery { |
| max_resp_time: Duration, |
| group_addr: Ipv4Address, |
| version: IgmpVersion, |
| }, |
| MembershipReport { |
| group_addr: Ipv4Address, |
| version: IgmpVersion, |
| }, |
| LeaveGroup { |
| group_addr: Ipv4Address, |
| }, |
| } |
| |
| /// Type of IGMP membership report version |
| #[derive(Debug, PartialEq, Eq, Clone, Copy)] |
| #[cfg_attr(feature = "defmt", derive(defmt::Format))] |
| pub enum IgmpVersion { |
| /// IGMPv1 |
| Version1, |
| /// IGMPv2 |
| Version2, |
| } |
| |
| impl Repr { |
| /// Parse an Internet Group Management Protocol v1/v2 packet and return |
| /// a high-level representation. |
| pub fn parse<T>(packet: &Packet<&T>) -> Result<Repr> |
| where |
| T: AsRef<[u8]> + ?Sized, |
| { |
| // Check if the address is 0.0.0.0 or multicast |
| let addr = packet.group_addr(); |
| if !addr.is_unspecified() && !addr.is_multicast() { |
| return Err(Error); |
| } |
| |
| // construct a packet based on the Type field |
| match packet.msg_type() { |
| Message::MembershipQuery => { |
| let max_resp_time = max_resp_code_to_duration(packet.max_resp_code()); |
| // See RFC 3376: 7.1. Query Version Distinctions |
| let version = if packet.max_resp_code() == 0 { |
| IgmpVersion::Version1 |
| } else { |
| IgmpVersion::Version2 |
| }; |
| Ok(Repr::MembershipQuery { |
| max_resp_time, |
| group_addr: addr, |
| version, |
| }) |
| } |
| Message::MembershipReportV2 => Ok(Repr::MembershipReport { |
| group_addr: packet.group_addr(), |
| version: IgmpVersion::Version2, |
| }), |
| Message::LeaveGroup => Ok(Repr::LeaveGroup { |
| group_addr: packet.group_addr(), |
| }), |
| Message::MembershipReportV1 => { |
| // for backwards compatibility with IGMPv1 |
| Ok(Repr::MembershipReport { |
| group_addr: packet.group_addr(), |
| version: IgmpVersion::Version1, |
| }) |
| } |
| _ => Err(Error), |
| } |
| } |
| |
| /// Return the length of a packet that will be emitted from this high-level representation. |
| pub const fn buffer_len(&self) -> usize { |
| // always 8 bytes |
| field::GROUP_ADDRESS.end |
| } |
| |
| /// Emit a high-level representation into an Internet Group Management Protocol v2 packet. |
| pub fn emit<T>(&self, packet: &mut Packet<&mut T>) |
| where |
| T: AsRef<[u8]> + AsMut<[u8]> + ?Sized, |
| { |
| match *self { |
| Repr::MembershipQuery { |
| max_resp_time, |
| group_addr, |
| version, |
| } => { |
| packet.set_msg_type(Message::MembershipQuery); |
| match version { |
| IgmpVersion::Version1 => packet.set_max_resp_code(0), |
| IgmpVersion::Version2 => { |
| packet.set_max_resp_code(duration_to_max_resp_code(max_resp_time)) |
| } |
| } |
| packet.set_group_address(group_addr); |
| } |
| Repr::MembershipReport { |
| group_addr, |
| version, |
| } => { |
| match version { |
| IgmpVersion::Version1 => packet.set_msg_type(Message::MembershipReportV1), |
| IgmpVersion::Version2 => packet.set_msg_type(Message::MembershipReportV2), |
| }; |
| packet.set_max_resp_code(0); |
| packet.set_group_address(group_addr); |
| } |
| Repr::LeaveGroup { group_addr } => { |
| packet.set_msg_type(Message::LeaveGroup); |
| packet.set_group_address(group_addr); |
| } |
| } |
| |
| packet.fill_checksum() |
| } |
| } |
| |
| fn max_resp_code_to_duration(value: u8) -> Duration { |
| let value: u64 = value.into(); |
| let decisecs = if value < 128 { |
| value |
| } else { |
| let mant = value & 0xF; |
| let exp = (value >> 4) & 0x7; |
| (mant | 0x10) << (exp + 3) |
| }; |
| Duration::from_millis(decisecs * 100) |
| } |
| |
| const fn duration_to_max_resp_code(duration: Duration) -> u8 { |
| let decisecs = duration.total_millis() / 100; |
| if decisecs < 128 { |
| decisecs as u8 |
| } else if decisecs < 31744 { |
| let mut mant = decisecs >> 3; |
| let mut exp = 0u8; |
| while mant > 0x1F && exp < 0x8 { |
| mant >>= 1; |
| exp += 1; |
| } |
| 0x80 | (exp << 4) | (mant as u8 & 0xF) |
| } else { |
| 0xFF |
| } |
| } |
| |
| impl<'a, T: AsRef<[u8]> + ?Sized> fmt::Display for Packet<&'a T> { |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| match Repr::parse(self) { |
| Ok(repr) => write!(f, "{repr}"), |
| Err(err) => write!(f, "IGMP ({err})"), |
| } |
| } |
| } |
| |
| impl fmt::Display for Repr { |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| match *self { |
| Repr::MembershipQuery { |
| max_resp_time, |
| group_addr, |
| version, |
| } => write!( |
| f, |
| "IGMP membership query max_resp_time={max_resp_time} group_addr={group_addr} version={version:?}" |
| ), |
| Repr::MembershipReport { |
| group_addr, |
| version, |
| } => write!( |
| f, |
| "IGMP membership report group_addr={group_addr} version={version:?}" |
| ), |
| Repr::LeaveGroup { group_addr } => { |
| write!(f, "IGMP leave group group_addr={group_addr})") |
| } |
| } |
| } |
| } |
| |
| use crate::wire::pretty_print::{PrettyIndent, PrettyPrint}; |
| |
| impl<T: AsRef<[u8]>> PrettyPrint for Packet<T> { |
| fn pretty_print( |
| buffer: &dyn AsRef<[u8]>, |
| f: &mut fmt::Formatter, |
| indent: &mut PrettyIndent, |
| ) -> fmt::Result { |
| match Packet::new_checked(buffer) { |
| Err(err) => writeln!(f, "{indent}({err})"), |
| Ok(packet) => writeln!(f, "{indent}{packet}"), |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| |
| static LEAVE_PACKET_BYTES: [u8; 8] = [0x17, 0x00, 0x02, 0x69, 0xe0, 0x00, 0x06, 0x96]; |
| static REPORT_PACKET_BYTES: [u8; 8] = [0x16, 0x00, 0x08, 0xda, 0xe1, 0x00, 0x00, 0x25]; |
| |
| #[test] |
| fn test_leave_group_deconstruct() { |
| let packet = Packet::new_unchecked(&LEAVE_PACKET_BYTES[..]); |
| assert_eq!(packet.msg_type(), Message::LeaveGroup); |
| assert_eq!(packet.max_resp_code(), 0); |
| assert_eq!(packet.checksum(), 0x269); |
| assert_eq!( |
| packet.group_addr(), |
| Ipv4Address::from_bytes(&[224, 0, 6, 150]) |
| ); |
| assert!(packet.verify_checksum()); |
| } |
| |
| #[test] |
| fn test_report_deconstruct() { |
| let packet = Packet::new_unchecked(&REPORT_PACKET_BYTES[..]); |
| assert_eq!(packet.msg_type(), Message::MembershipReportV2); |
| assert_eq!(packet.max_resp_code(), 0); |
| assert_eq!(packet.checksum(), 0x08da); |
| assert_eq!( |
| packet.group_addr(), |
| Ipv4Address::from_bytes(&[225, 0, 0, 37]) |
| ); |
| assert!(packet.verify_checksum()); |
| } |
| |
| #[test] |
| fn test_leave_construct() { |
| let mut bytes = vec![0xa5; 8]; |
| let mut packet = Packet::new_unchecked(&mut bytes); |
| packet.set_msg_type(Message::LeaveGroup); |
| packet.set_max_resp_code(0); |
| packet.set_group_address(Ipv4Address::from_bytes(&[224, 0, 6, 150])); |
| packet.fill_checksum(); |
| assert_eq!(&*packet.into_inner(), &LEAVE_PACKET_BYTES[..]); |
| } |
| |
| #[test] |
| fn test_report_construct() { |
| let mut bytes = vec![0xa5; 8]; |
| let mut packet = Packet::new_unchecked(&mut bytes); |
| packet.set_msg_type(Message::MembershipReportV2); |
| packet.set_max_resp_code(0); |
| packet.set_group_address(Ipv4Address::from_bytes(&[225, 0, 0, 37])); |
| packet.fill_checksum(); |
| assert_eq!(&*packet.into_inner(), &REPORT_PACKET_BYTES[..]); |
| } |
| |
| #[test] |
| fn max_resp_time_to_duration_and_back() { |
| for i in 0..256usize { |
| let time1 = i as u8; |
| let duration = max_resp_code_to_duration(time1); |
| let time2 = duration_to_max_resp_code(duration); |
| assert!(time1 == time2); |
| } |
| } |
| |
| #[test] |
| fn duration_to_max_resp_time_max() { |
| for duration in 31744..65536 { |
| let time = duration_to_max_resp_code(Duration::from_millis(duration * 100)); |
| assert_eq!(time, 0xFF); |
| } |
| } |
| } |