diff --git a/etherparse/src/transport/icmpv4_header.rs b/etherparse/src/transport/icmpv4_header.rs index 6b24f1c0..9f820dc8 100644 --- a/etherparse/src/transport/icmpv4_header.rs +++ b/etherparse/src/transport/icmpv4_header.rs @@ -385,7 +385,7 @@ mod test { fn read( non_timestamp_type in any::().prop_filter( "type must be a non timestamp type", - |v| (*v != icmpv4::TYPE_TIMESTAMP_REPLY && *v != icmpv4::TYPE_TIMESTAMP) + |v| *v != icmpv4::TYPE_TIMESTAMP_REPLY && *v != icmpv4::TYPE_TIMESTAMP ), non_zero_code in 1u8..=u8::MAX, bytes in any::<[u8;icmpv4::TimestampMessage::LEN]>() diff --git a/etherparse/src/transport/icmpv6/icmpv6_payload/mod.rs b/etherparse/src/transport/icmpv6/icmpv6_payload/mod.rs new file mode 100644 index 00000000..94287468 --- /dev/null +++ b/etherparse/src/transport/icmpv6/icmpv6_payload/mod.rs @@ -0,0 +1,122 @@ +mod neighbor_advertisement_payload; +pub use neighbor_advertisement_payload::*; + +mod neighbor_solicitation_payload; +pub use neighbor_solicitation_payload::*; + +mod redirect_payload; +pub use redirect_payload::*; + +mod router_advertisement_payload; +pub use router_advertisement_payload::*; + +mod router_solicitation_payload; +pub use router_solicitation_payload::*; + +/// Owned, structured payload data that follows the first 8 bytes of an ICMPv6 packet. +#[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum Icmpv6Payload { + /// Payload of a Router Solicitation message. + RouterSolicitation(RouterSolicitationPayload), + /// Payload of a Router Advertisement message. + RouterAdvertisement(RouterAdvertisementPayload), + /// Payload of a Neighbor Solicitation message. + NeighborSolicitation(NeighborSolicitationPayload), + /// Payload of a Neighbor Advertisement message. + NeighborAdvertisement(NeighborAdvertisementPayload), + /// Payload of a Redirect message. + Redirect(RedirectPayload), +} + +impl Icmpv6Payload { + /// Returns the serialized payload length in bytes. + pub fn len(&self) -> usize { + use Icmpv6Payload::*; + match self { + RouterSolicitation(_) => RouterSolicitationPayload::LEN, + RouterAdvertisement(_) => RouterAdvertisementPayload::LEN, + NeighborSolicitation(_) => NeighborSolicitationPayload::LEN, + NeighborAdvertisement(_) => NeighborAdvertisementPayload::LEN, + Redirect(_) => RedirectPayload::LEN, + } + } + + /// Write the fixed payload bytes to the writer. + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] + pub fn write( + &self, + writer: &mut T, + ) -> Result<(), std::io::Error> { + match self { + Icmpv6Payload::RouterSolicitation(value) => writer.write_all(&value.to_bytes()), + Icmpv6Payload::RouterAdvertisement(value) => writer.write_all(&value.to_bytes()), + Icmpv6Payload::NeighborSolicitation(value) => writer.write_all(&value.to_bytes()), + Icmpv6Payload::NeighborAdvertisement(value) => writer.write_all(&value.to_bytes()), + Icmpv6Payload::Redirect(value) => writer.write_all(&value.to_bytes()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::format; + use proptest::prelude::*; + + #[test] + fn router_solicitation_payload_to_bytes() { + assert_eq!([] as [u8; 0], RouterSolicitationPayload.to_bytes()); + } + + proptest! { + #[test] + fn payloads_to_bytes( + reachable_time in any::(), + retrans_timer in any::(), + target_address in any::<[u8;16]>(), + destination_address in any::<[u8;16]>() + ) { + let reachable_time_be = reachable_time.to_be_bytes(); + let retrans_timer_be = retrans_timer.to_be_bytes(); + let mut expected_router_advertisement = [0u8; RouterAdvertisementPayload::LEN]; + expected_router_advertisement[..4].copy_from_slice(&reachable_time_be); + expected_router_advertisement[4..].copy_from_slice(&retrans_timer_be); + + let mut expected_redirect = [0u8; RedirectPayload::LEN]; + expected_redirect[..16].copy_from_slice(&target_address); + expected_redirect[16..].copy_from_slice(&destination_address); + + assert_eq!( + RouterAdvertisementPayload { + reachable_time, + retrans_timer, + }.to_bytes(), + expected_router_advertisement + ); + assert_eq!( + NeighborSolicitationPayload { + target_address: core::net::Ipv6Addr::from(target_address), + } + .to_bytes(), + target_address + ); + assert_eq!( + NeighborAdvertisementPayload { + target_address: core::net::Ipv6Addr::from(target_address), + } + .to_bytes(), + target_address + ); + assert_eq!( + RedirectPayload { + target_address: core::net::Ipv6Addr::from(target_address), + destination_address: core::net::Ipv6Addr::from(destination_address), + } + .to_bytes(), + expected_redirect + ); + } + } +} diff --git a/etherparse/src/transport/icmpv6/icmpv6_payload/neighbor_advertisement_payload.rs b/etherparse/src/transport/icmpv6/icmpv6_payload/neighbor_advertisement_payload.rs new file mode 100644 index 00000000..e13f60c0 --- /dev/null +++ b/etherparse/src/transport/icmpv6/icmpv6_payload/neighbor_advertisement_payload.rs @@ -0,0 +1,44 @@ +use core::net::Ipv6Addr; + +/// Owned payload of a Neighbor Advertisement message (RFC 4861, Section 4.4). +/// +/// The full packet layout is: +/// ```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 | Code | Checksum | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// |R|S|O| Reserved | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// + + +/// | | +/// + Target Address + +/// | | +/// + + +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Options ... +/// +-+-+-+-+-+-+-+-+-+-+-+- +/// ``` +/// +/// In this crate, `R`, `S`, and `O` are represented by +/// [`crate::icmpv6::NeighborAdvertisementHeader`] in +/// [`crate::Icmpv6Type::NeighborAdvertisement`]. This payload struct represents +/// the fixed `Target Address` bytes. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct NeighborAdvertisementPayload { + /// Target IPv6 address. + pub target_address: Ipv6Addr, +} + +impl NeighborAdvertisementPayload { + /// Fixed payload length in bytes after the ICMPv6 header. + pub const LEN: usize = 16; + + /// Convert to on-the-wire bytes. + pub const fn to_bytes(&self) -> [u8; Self::LEN] { + self.target_address.octets() + } +} diff --git a/etherparse/src/transport/icmpv6/icmpv6_payload/neighbor_solicitation_payload.rs b/etherparse/src/transport/icmpv6/icmpv6_payload/neighbor_solicitation_payload.rs new file mode 100644 index 00000000..f02eb0d2 --- /dev/null +++ b/etherparse/src/transport/icmpv6/icmpv6_payload/neighbor_solicitation_payload.rs @@ -0,0 +1,43 @@ +use core::net::Ipv6Addr; + +/// Owned payload of a Neighbor Solicitation message (RFC 4861, Section 4.3). +/// +/// The full packet layout is: +/// ```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 | Code | Checksum | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Reserved | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// + + +/// | | +/// + Target Address + +/// | | +/// + + +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Options ... +/// +-+-+-+-+-+-+-+-+-+-+-+- +/// ``` +/// +/// In this crate, the first 8 bytes (including `Reserved`) are represented by +/// [`crate::Icmpv6Type::NeighborSolicitation`]. This payload struct represents +/// the fixed `Target Address` bytes. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct NeighborSolicitationPayload { + /// Target IPv6 address. + pub target_address: Ipv6Addr, +} + +impl NeighborSolicitationPayload { + /// Fixed payload length in bytes after the ICMPv6 header. + pub const LEN: usize = 16; + + /// Convert to on-the-wire bytes. + pub const fn to_bytes(&self) -> [u8; Self::LEN] { + self.target_address.octets() + } +} diff --git a/etherparse/src/transport/icmpv6/icmpv6_payload/redirect_payload.rs b/etherparse/src/transport/icmpv6/icmpv6_payload/redirect_payload.rs new file mode 100644 index 00000000..1ffd59da --- /dev/null +++ b/etherparse/src/transport/icmpv6/icmpv6_payload/redirect_payload.rs @@ -0,0 +1,59 @@ +use core::net::Ipv6Addr; + +/// Owned payload of a Redirect message (RFC 4861, Section 4.5). +/// +/// The full packet layout is: +/// ```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 | Code | Checksum | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Reserved | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// + + +/// | | +/// + Target Address + +/// | | +/// + + +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// + + +/// | | +/// + Destination Address + +/// | | +/// + + +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Options ... +/// +-+-+-+-+-+-+-+-+-+-+-+- +/// ``` +/// +/// In this crate, the first 8 bytes (including `Reserved`) are represented by +/// [`crate::Icmpv6Type::Redirect`]. This payload struct represents the fixed bytes: +/// - `Target Address` +/// - `Destination Address` +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct RedirectPayload { + /// Better first-hop target address. + pub target_address: Ipv6Addr, + /// Destination address being redirected. + pub destination_address: Ipv6Addr, +} + +impl RedirectPayload { + /// Fixed payload length in bytes after the ICMPv6 header. + pub const LEN: usize = 32; + + /// Convert to on-the-wire bytes. + pub const fn to_bytes(&self) -> [u8; Self::LEN] { + let mut out = [0u8; Self::LEN]; + // Safety: unwraps are safe because Self::LEN == 32, which is larger than the number of + // octets in IPv6 address + *out.first_chunk_mut().unwrap() = self.target_address.octets(); + *out.last_chunk_mut().unwrap() = self.destination_address.octets(); + out + } +} diff --git a/etherparse/src/transport/icmpv6/icmpv6_payload/router_advertisement_payload.rs b/etherparse/src/transport/icmpv6/icmpv6_payload/router_advertisement_payload.rs new file mode 100644 index 00000000..a944cfac --- /dev/null +++ b/etherparse/src/transport/icmpv6/icmpv6_payload/router_advertisement_payload.rs @@ -0,0 +1,48 @@ +/// Owned payload of a Router Advertisement message (RFC 4861, Section 4.2). +/// +/// The full packet layout is: +/// ```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 | Code | Checksum | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Cur Hop Limit |M|O| Reserved | Router Lifetime | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Reachable Time | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Retrans Timer | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Options ... +/// +-+-+-+-+-+-+-+-+-+-+-+- +/// ``` +/// +/// In this crate, `Cur Hop Limit`, `M`, `O`, and `Router Lifetime` are represented by +/// [`crate::icmpv6::RouterAdvertisementHeader`] in [`crate::Icmpv6Type::RouterAdvertisement`]. +/// This payload struct represents the fixed bytes after that: +/// - `Reachable Time` +/// - `Retrans Timer` +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct RouterAdvertisementPayload { + /// Reachable time in milliseconds. + pub reachable_time: u32, + /// Retransmit timer in milliseconds. + pub retrans_timer: u32, +} + +impl RouterAdvertisementPayload { + /// Fixed payload length in bytes after the ICMPv6 header. + pub const LEN: usize = 8; + + /// Convert to on-the-wire bytes. + pub fn to_bytes(&self) -> [u8; Self::LEN] { + let mut out = [0u8; Self::LEN]; + + // Safety: unwraps are safe because Self::LEN == 8, which is larger than the number of + // octets in u32 + *out.first_chunk_mut().unwrap() = self.reachable_time.to_be_bytes(); + *out.last_chunk_mut().unwrap() = self.retrans_timer.to_be_bytes(); + + out + } +} diff --git a/etherparse/src/transport/icmpv6/icmpv6_payload/router_solicitation_payload.rs b/etherparse/src/transport/icmpv6/icmpv6_payload/router_solicitation_payload.rs new file mode 100644 index 00000000..8693f279 --- /dev/null +++ b/etherparse/src/transport/icmpv6/icmpv6_payload/router_solicitation_payload.rs @@ -0,0 +1,29 @@ +/// Owned payload of a Router Solicitation message (RFC 4861, Section 4.1). +/// +/// The full packet layout is: +/// ```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 | Code | Checksum | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Reserved | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Options ... +/// +-+-+-+-+-+-+-+-+-+-+-+- +/// ``` +/// +/// In this crate, the first 8 bytes (including `Reserved`) are represented by +/// [`crate::Icmpv6Type::RouterSolicitation`], so this payload struct has no fixed bytes. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct RouterSolicitationPayload; + +impl RouterSolicitationPayload { + /// Fixed payload length in bytes after the ICMPv6 header. + pub const LEN: usize = 0; + + /// Convert to on-the-wire bytes. + pub const fn to_bytes(&self) -> [u8; Self::LEN] { + [] + } +} diff --git a/etherparse/src/transport/icmpv6/icmpv6_payload_slice/mod.rs b/etherparse/src/transport/icmpv6/icmpv6_payload_slice/mod.rs new file mode 100644 index 00000000..55c14adb --- /dev/null +++ b/etherparse/src/transport/icmpv6/icmpv6_payload_slice/mod.rs @@ -0,0 +1,298 @@ +mod neighbor_advertisement_payload_slice; +pub use neighbor_advertisement_payload_slice::*; + +mod neighbor_solicitation_payload_slice; +pub use neighbor_solicitation_payload_slice::*; + +mod redirect_payload_slice; +pub use redirect_payload_slice::*; + +mod router_advertisement_payload_slice; +pub use router_advertisement_payload_slice::*; + +mod router_solicitation_payload_slice; +pub use router_solicitation_payload_slice::*; + +use crate::{err, icmpv6::Icmpv6Payload}; + +/// Borrowed, structured payload data that follows the first 8 bytes of an ICMPv6 packet. +#[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum Icmpv6PayloadSlice<'a> { + /// Payload of a Router Solicitation message. + RouterSolicitation(RouterSolicitationPayloadSlice<'a>), + /// Payload of a Router Advertisement message. + RouterAdvertisement(RouterAdvertisementPayloadSlice<'a>), + /// Payload of a Neighbor Solicitation message. + NeighborSolicitation(NeighborSolicitationPayloadSlice<'a>), + /// Payload of a Neighbor Advertisement message. + NeighborAdvertisement(NeighborAdvertisementPayloadSlice<'a>), + /// Payload of a Redirect message. + Redirect(RedirectPayloadSlice<'a>), + /// Raw payload of an unsupported or currently unmodeled ICMPv6 message. + Raw(&'a [u8]), +} + +impl<'a> Icmpv6PayloadSlice<'a> { + /// Decode a structured payload based on the ICMPv6 type. + pub fn from_slice( + icmp_type: &crate::Icmpv6Type, + payload: &'a [u8], + ) -> Result, err::LenError> { + use crate::Icmpv6Type::*; + + Ok(match icmp_type { + RouterSolicitation => Icmpv6PayloadSlice::RouterSolicitation( + RouterSolicitationPayloadSlice::from_slice(payload)?, + ), + RouterAdvertisement(_) => Icmpv6PayloadSlice::RouterAdvertisement( + RouterAdvertisementPayloadSlice::from_slice(payload)?, + ), + NeighborSolicitation => Icmpv6PayloadSlice::NeighborSolicitation( + NeighborSolicitationPayloadSlice::from_slice(payload)?, + ), + NeighborAdvertisement(_) => Icmpv6PayloadSlice::NeighborAdvertisement( + NeighborAdvertisementPayloadSlice::from_slice(payload)?, + ), + Redirect => Icmpv6PayloadSlice::Redirect(RedirectPayloadSlice::from_slice(payload)?), + _ => Icmpv6PayloadSlice::Raw(payload), + }) + } + + pub(crate) fn from_type_u8( + type_u8: u8, + code_u8: u8, + payload: &'a [u8], + ) -> Result, err::LenError> { + use crate::icmpv6::*; + + // For the currently modeled ND message payloads (RS/RA/NS/NA/Redirect), + // RFC 4861 validation rules require code 0 (quote: "- ICMP Code is 0."). + // See sections 6.1.1, 6.1.2, 7.1.1, 7.1.2, and 8.1. + if 0 != code_u8 { + return Ok(Icmpv6PayloadSlice::Raw(payload)); + } + + match type_u8 { + TYPE_ROUTER_SOLICITATION => Ok(Icmpv6PayloadSlice::RouterSolicitation( + RouterSolicitationPayloadSlice::from_slice(payload)?, + )), + TYPE_ROUTER_ADVERTISEMENT => Ok(Icmpv6PayloadSlice::RouterAdvertisement( + RouterAdvertisementPayloadSlice::from_slice(payload)?, + )), + TYPE_NEIGHBOR_SOLICITATION => Ok(Icmpv6PayloadSlice::NeighborSolicitation( + NeighborSolicitationPayloadSlice::from_slice(payload)?, + )), + TYPE_NEIGHBOR_ADVERTISEMENT => Ok(Icmpv6PayloadSlice::NeighborAdvertisement( + NeighborAdvertisementPayloadSlice::from_slice(payload)?, + )), + TYPE_REDIRECT_MESSAGE => Ok(Icmpv6PayloadSlice::Redirect( + RedirectPayloadSlice::from_slice(payload)?, + )), + _ => Ok(Icmpv6PayloadSlice::Raw(payload)), + } + } + + /// Returns the full borrowed payload bytes. + pub fn slice(&self) -> &'a [u8] { + match self { + Icmpv6PayloadSlice::RouterSolicitation(value) => value.slice(), + Icmpv6PayloadSlice::RouterAdvertisement(value) => value.slice(), + Icmpv6PayloadSlice::NeighborSolicitation(value) => value.slice(), + Icmpv6PayloadSlice::NeighborAdvertisement(value) => value.slice(), + Icmpv6PayloadSlice::Redirect(value) => value.slice(), + Icmpv6PayloadSlice::Raw(value) => value, + } + } + + /// Convert the borrowed payload to an owned structured payload if supported. + /// + /// For payload types with variable trailing data (for example ND options), + /// the second tuple element contains the remaining unparsed bytes. + pub fn to_payload(&self) -> Option<(Icmpv6Payload, &'a [u8])> { + match self { + Icmpv6PayloadSlice::RouterSolicitation(value) => { + let (payload, options) = value.to_payload(); + Some((Icmpv6Payload::RouterSolicitation(payload), options)) + } + Icmpv6PayloadSlice::RouterAdvertisement(value) => { + let (payload, options) = value.to_payload(); + Some((Icmpv6Payload::RouterAdvertisement(payload), options)) + } + Icmpv6PayloadSlice::NeighborSolicitation(value) => { + let (payload, options) = value.to_payload(); + Some((Icmpv6Payload::NeighborSolicitation(payload), options)) + } + Icmpv6PayloadSlice::NeighborAdvertisement(value) => { + let (payload, options) = value.to_payload(); + Some((Icmpv6Payload::NeighborAdvertisement(payload), options)) + } + Icmpv6PayloadSlice::Redirect(value) => { + let (payload, options) = value.to_payload(); + Some((Icmpv6Payload::Redirect(payload), options)) + } + Icmpv6PayloadSlice::Raw(_) => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{err, err::Layer, LenSource}; + use crate::icmpv6::{ + NeighborAdvertisementPayload, NeighborSolicitationPayload, RedirectPayload, + RouterAdvertisementPayload, RouterSolicitationPayload, + }; + use core::net::Ipv6Addr; + use proptest::prelude::*; + + proptest! { + #[test] + fn router_solicitation(slice in proptest::collection::vec(any::(), 0..64)) { + let actual = RouterSolicitationPayloadSlice::from_slice(&slice).unwrap(); + assert_eq!(actual.slice(), &slice[..]); + assert_eq!(actual.options(), &slice[..]); + assert_eq!(actual.to_payload(), (RouterSolicitationPayload, &slice[..])); + } + + #[test] + fn router_advertisement( + reachable_time in any::(), + retrans_timer in any::(), + options in proptest::collection::vec(any::(), 0..32) + ) { + let mut data = [0u8; 8]; + data[..4].copy_from_slice(&reachable_time.to_be_bytes()); + data[4..].copy_from_slice(&retrans_timer.to_be_bytes()); + let mut payload = alloc::vec::Vec::from(data); + payload.extend_from_slice(&options); + + let actual = RouterAdvertisementPayloadSlice::from_slice(&payload).unwrap(); + assert_eq!(actual.reachable_time(), reachable_time); + assert_eq!(actual.retrans_timer(), retrans_timer); + assert_eq!(actual.options(), &options[..]); + assert_eq!( + actual.to_payload(), + ( + RouterAdvertisementPayload { + reachable_time, + retrans_timer, + }, + &options[..] + ) + ); + } + + #[test] + fn neighbor_solicitation( + target_address in any::<[u8;16]>(), + options in proptest::collection::vec(any::(), 0..32) + ) { + let mut payload = alloc::vec::Vec::from(target_address); + payload.extend_from_slice(&options); + let actual = NeighborSolicitationPayloadSlice::from_slice(&payload).unwrap(); + assert_eq!(actual.target_address(), Ipv6Addr::from(target_address)); + assert_eq!(actual.options(), &options[..]); + assert_eq!( + actual.to_payload(), + ( + NeighborSolicitationPayload { + target_address: Ipv6Addr::from(target_address) + }, + &options[..] + ) + ); + } + + #[test] + fn neighbor_advertisement( + target_address in any::<[u8;16]>(), + options in proptest::collection::vec(any::(), 0..32) + ) { + let mut payload = alloc::vec::Vec::from(target_address); + payload.extend_from_slice(&options); + let actual = NeighborAdvertisementPayloadSlice::from_slice(&payload).unwrap(); + assert_eq!(actual.target_address(), Ipv6Addr::from(target_address)); + assert_eq!(actual.options(), &options[..]); + assert_eq!( + actual.to_payload(), + ( + NeighborAdvertisementPayload { + target_address: Ipv6Addr::from(target_address), + }, + &options[..] + ) + ); + } + + #[test] + fn redirect( + target_address in any::<[u8;16]>(), + destination_address in any::<[u8;16]>(), + options in proptest::collection::vec(any::(), 0..32) + ) { + let mut payload = alloc::vec::Vec::from(target_address); + payload.extend_from_slice(&destination_address); + payload.extend_from_slice(&options); + let actual = RedirectPayloadSlice::from_slice(&payload).unwrap(); + assert_eq!(actual.target_address(), Ipv6Addr::from(target_address)); + assert_eq!(actual.destination_address(), Ipv6Addr::from(destination_address)); + assert_eq!(actual.options(), &options[..]); + assert_eq!( + actual.to_payload(), + ( + RedirectPayload { + target_address: Ipv6Addr::from(target_address), + destination_address: Ipv6Addr::from(destination_address), + }, + &options[..] + ) + ); + } + } + + #[test] + fn len_errors() { + assert_eq!( + Err(err::LenError { + required_len: RouterAdvertisementPayloadSlice::FIXED_PART_LEN, + len: 7, + len_source: LenSource::Slice, + layer: Layer::Icmpv6, + layer_start_offset: 0, + }), + RouterAdvertisementPayloadSlice::from_slice(&[0; 7]) + ); + assert_eq!( + Err(err::LenError { + required_len: NeighborSolicitationPayloadSlice::FIXED_PART_LEN, + len: 15, + len_source: LenSource::Slice, + layer: Layer::Icmpv6, + layer_start_offset: 0, + }), + NeighborSolicitationPayloadSlice::from_slice(&[0; 15]) + ); + assert_eq!( + Err(err::LenError { + required_len: NeighborAdvertisementPayloadSlice::FIXED_PART_LEN, + len: 15, + len_source: LenSource::Slice, + layer: Layer::Icmpv6, + layer_start_offset: 0, + }), + NeighborAdvertisementPayloadSlice::from_slice(&[0; 15]) + ); + assert_eq!( + Err(err::LenError { + required_len: RedirectPayloadSlice::FIXED_PART_LEN, + len: 31, + len_source: LenSource::Slice, + layer: Layer::Icmpv6, + layer_start_offset: 0, + }), + RedirectPayloadSlice::from_slice(&[0; 31]) + ); + } +} diff --git a/etherparse/src/transport/icmpv6/icmpv6_payload_slice/neighbor_advertisement_payload_slice.rs b/etherparse/src/transport/icmpv6/icmpv6_payload_slice/neighbor_advertisement_payload_slice.rs new file mode 100644 index 00000000..3477456b --- /dev/null +++ b/etherparse/src/transport/icmpv6/icmpv6_payload_slice/neighbor_advertisement_payload_slice.rs @@ -0,0 +1,92 @@ +use crate::{ + err, + err::Layer, + icmpv6::{NdpOptionsIterator, NeighborAdvertisementPayload}, + LenSource, +}; +use core::net::Ipv6Addr; + +/// Borrowed payload of a Neighbor Advertisement message (RFC 4861, Section 4.4). +/// +/// The full packet layout is: +/// ```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 | Code | Checksum | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// |R|S|O| Reserved | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// + + +/// | | +/// + Target Address + +/// | | +/// + + +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Options ... +/// +-+-+-+-+-+-+-+-+-+-+-+- +/// ``` +/// +/// In this crate, `R`, `S`, and `O` are represented by +/// [`crate::icmpv6::NeighborAdvertisementHeader`] in +/// [`crate::Icmpv6Type::NeighborAdvertisement`]. This slice starts after that fixed part +/// and contains the fixed `Target Address` followed by options. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct NeighborAdvertisementPayloadSlice<'a> { + slice: &'a [u8], +} + +impl<'a> NeighborAdvertisementPayloadSlice<'a> { + /// Length of the fixed payload part (the `Target Address`) in bytes. + pub const FIXED_PART_LEN: usize = NeighborAdvertisementPayload::LEN; + + /// Creates a payload slice from the bytes after the ICMPv6 header. + pub fn from_slice(slice: &'a [u8]) -> Result { + if slice.len() < Self::FIXED_PART_LEN { + Err(err::LenError { + required_len: Self::FIXED_PART_LEN, + len: slice.len(), + len_source: LenSource::Slice, + layer: Layer::Icmpv6, + layer_start_offset: 0, + }) + } else { + Ok(Self { slice }) + } + } + + /// Returns the full payload slice. + pub fn slice(&self) -> &'a [u8] { + self.slice + } + + /// Target address carried in the payload. + pub fn target_address(&self) -> Ipv6Addr { + // Safe to unwrap because `from_slice` guarantees + // `self.slice.len() >= FIXED_PART_LEN`, and `FIXED_PART_LEN` includes + // the full 16-byte target address field. + Ipv6Addr::from(*self.slice.first_chunk().unwrap()) + } + + /// Returns the Neighbor Discovery options. + pub fn options(&self) -> &'a [u8] { + &self.slice[Self::FIXED_PART_LEN..] + } + + /// Returns an iterator over Neighbor Discovery options. + pub fn options_iterator(&self) -> NdpOptionsIterator<'a> { + NdpOptionsIterator::from_slice(self.options()) + } + + /// Convert to an owned structured payload and return trailing options. + pub fn to_payload(&self) -> (NeighborAdvertisementPayload, &'a [u8]) { + ( + NeighborAdvertisementPayload { + target_address: self.target_address(), + }, + self.options(), + ) + } +} diff --git a/etherparse/src/transport/icmpv6/icmpv6_payload_slice/neighbor_solicitation_payload_slice.rs b/etherparse/src/transport/icmpv6/icmpv6_payload_slice/neighbor_solicitation_payload_slice.rs new file mode 100644 index 00000000..a4aa04bd --- /dev/null +++ b/etherparse/src/transport/icmpv6/icmpv6_payload_slice/neighbor_solicitation_payload_slice.rs @@ -0,0 +1,91 @@ +use crate::{ + err, + err::Layer, + icmpv6::{NdpOptionsIterator, NeighborSolicitationPayload}, + LenSource, +}; +use core::net::Ipv6Addr; + +/// Borrowed payload of a Neighbor Solicitation message (RFC 4861, Section 4.3). +/// +/// The full packet layout is: +/// ```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 | Code | Checksum | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Reserved | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// + + +/// | | +/// + Target Address + +/// | | +/// + + +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Options ... +/// +-+-+-+-+-+-+-+-+-+-+-+- +/// ``` +/// +/// In this crate, the first 8 bytes (including `Reserved`) are represented by +/// [`crate::Icmpv6Type::NeighborSolicitation`]. This slice starts after that fixed part +/// and contains the fixed `Target Address` followed by options. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct NeighborSolicitationPayloadSlice<'a> { + slice: &'a [u8], +} + +impl<'a> NeighborSolicitationPayloadSlice<'a> { + /// Length of the fixed payload part (the `Target Address`) in bytes. + pub const FIXED_PART_LEN: usize = NeighborSolicitationPayload::LEN; + + /// Creates a payload slice from the bytes after the ICMPv6 header. + pub fn from_slice(slice: &'a [u8]) -> Result { + if slice.len() < Self::FIXED_PART_LEN { + Err(err::LenError { + required_len: Self::FIXED_PART_LEN, + len: slice.len(), + len_source: LenSource::Slice, + layer: Layer::Icmpv6, + layer_start_offset: 0, + }) + } else { + Ok(Self { slice }) + } + } + + /// Returns the full payload slice. + pub fn slice(&self) -> &'a [u8] { + self.slice + } + + /// Target address carried in the payload. + pub fn target_address(&self) -> Ipv6Addr { + // Safe to unwrap because `from_slice` guarantees + // `self.slice.len() >= FIXED_PART_LEN`, and `FIXED_PART_LEN` includes + // the full 16-byte target address field. + Ipv6Addr::from(*self.slice.first_chunk().unwrap()) + } + + /// Returns the Neighbor Discovery options. + pub fn options(&self) -> &'a [u8] { + &self.slice[Self::FIXED_PART_LEN..] + } + + /// Returns an iterator over Neighbor Discovery options. + pub fn options_iterator(&self) -> NdpOptionsIterator<'a> { + NdpOptionsIterator::from_slice(self.options()) + } + + /// Convert to an owned structured payload and return trailing options. + pub fn to_payload(&self) -> (NeighborSolicitationPayload, &'a [u8]) { + ( + NeighborSolicitationPayload { + target_address: self.target_address(), + }, + self.options(), + ) + } +} diff --git a/etherparse/src/transport/icmpv6/icmpv6_payload_slice/redirect_payload_slice.rs b/etherparse/src/transport/icmpv6/icmpv6_payload_slice/redirect_payload_slice.rs new file mode 100644 index 00000000..93859f99 --- /dev/null +++ b/etherparse/src/transport/icmpv6/icmpv6_payload_slice/redirect_payload_slice.rs @@ -0,0 +1,113 @@ +use crate::{ + err, + err::Layer, + icmpv6::{NdpOptionsIterator, RedirectPayload}, + LenSource, +}; +use core::net::Ipv6Addr; + +/// Borrowed payload of a Redirect message (RFC 4861, Section 4.5). +/// +/// The full packet layout is: +/// ```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 | Code | Checksum | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Reserved | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// + + +/// | | +/// + Target Address + +/// | | +/// + + +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// + + +/// | | +/// + Destination Address + +/// | | +/// + + +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Options ... +/// +-+-+-+-+-+-+-+-+-+-+-+- +/// ``` +/// +/// In this crate, the first 8 bytes (including `Reserved`) are represented by +/// [`crate::Icmpv6Type::Redirect`]. This slice starts after that fixed part and +/// contains: +/// - `Target Address` +/// - `Destination Address` +/// - options +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RedirectPayloadSlice<'a> { + slice: &'a [u8], +} + +impl<'a> RedirectPayloadSlice<'a> { + const IPV6_ADDRESS_LEN: usize = (Ipv6Addr::BITS / 8) as usize; + + /// Length of the fixed payload part (`Target Address` + `Destination Address`) in bytes. + pub const FIXED_PART_LEN: usize = RedirectPayload::LEN; + + /// Creates a payload slice from the bytes after the ICMPv6 header. + pub fn from_slice(slice: &'a [u8]) -> Result { + if slice.len() < Self::FIXED_PART_LEN { + Err(err::LenError { + required_len: Self::FIXED_PART_LEN, + len: slice.len(), + len_source: LenSource::Slice, + layer: Layer::Icmpv6, + layer_start_offset: 0, + }) + } else { + Ok(Self { slice }) + } + } + + /// Returns the full payload slice. + pub fn slice(&self) -> &'a [u8] { + self.slice + } + + /// Better first-hop target address. + pub fn target_address(&self) -> Ipv6Addr { + // Safe to unwrap because `from_slice` guarantees + // `self.slice.len() >= FIXED_PART_LEN`, and `FIXED_PART_LEN` includes + // the first 16-byte IPv6 address field. + Ipv6Addr::from(*self.slice.first_chunk().unwrap()) + } + + /// Destination address being redirected. + pub fn destination_address(&self) -> Ipv6Addr { + // Safe to unwrap because `from_slice` guarantees + // `self.slice.len() >= FIXED_PART_LEN`, and `FIXED_PART_LEN` includes + // both 16-byte IPv6 address fields. + Ipv6Addr::from(*self.slice[Self::IPV6_ADDRESS_LEN..].first_chunk().unwrap()) + } + + /// Returns the Neighbor Discovery options. + pub fn options(&self) -> &'a [u8] { + &self.slice[Self::FIXED_PART_LEN..] + } + + /// Returns an iterator over Neighbor Discovery options. + pub fn options_iterator(&self) -> NdpOptionsIterator<'a> { + NdpOptionsIterator::from_slice(self.options()) + } + + /// Convert to an owned structured payload and return trailing options. + pub fn to_payload(&self) -> (RedirectPayload, &'a [u8]) { + ( + RedirectPayload { + target_address: self.target_address(), + destination_address: self.destination_address(), + }, + self.options(), + ) + } +} diff --git a/etherparse/src/transport/icmpv6/icmpv6_payload_slice/router_advertisement_payload_slice.rs b/etherparse/src/transport/icmpv6/icmpv6_payload_slice/router_advertisement_payload_slice.rs new file mode 100644 index 00000000..81b8e4c8 --- /dev/null +++ b/etherparse/src/transport/icmpv6/icmpv6_payload_slice/router_advertisement_payload_slice.rs @@ -0,0 +1,103 @@ +use crate::{ + err, + err::Layer, + icmpv6::{NdpOptionsIterator, RouterAdvertisementPayload}, + LenSource, +}; + +/// Borrowed payload of a Router Advertisement message (RFC 4861, Section 4.2). +/// +/// The full packet layout is: +/// ```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 | Code | Checksum | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Cur Hop Limit |M|O| Reserved | Router Lifetime | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Reachable Time | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Retrans Timer | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Options ... +/// +-+-+-+-+-+-+-+-+-+-+-+- +/// ``` +/// +/// In this crate, `Cur Hop Limit`, `M`, `O`, and `Router Lifetime` are represented by +/// [`crate::icmpv6::RouterAdvertisementHeader`] in [`crate::Icmpv6Type::RouterAdvertisement`]. +/// This slice starts after that fixed part and contains: +/// - `Reachable Time` +/// - `Retrans Timer` +/// - options +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RouterAdvertisementPayloadSlice<'a> { + slice: &'a [u8], +} + +impl<'a> RouterAdvertisementPayloadSlice<'a> { + const U32_FIELD_LEN: usize = (u32::BITS / 8) as usize; + const RETRANS_TIMER_OFFSET: usize = Self::U32_FIELD_LEN; + + /// Length of the fixed payload part (`Reachable Time` and `Retrans Timer`) in bytes. + pub const FIXED_PART_LEN: usize = RouterAdvertisementPayload::LEN; + + /// Creates a payload slice from the bytes after the ICMPv6 header. + pub fn from_slice(slice: &'a [u8]) -> Result { + if slice.len() < Self::FIXED_PART_LEN { + Err(err::LenError { + required_len: Self::FIXED_PART_LEN, + len: slice.len(), + len_source: LenSource::Slice, + layer: Layer::Icmpv6, + layer_start_offset: 0, + }) + } else { + Ok(Self { slice }) + } + } + + /// Returns the full payload slice. + pub fn slice(&self) -> &'a [u8] { + self.slice + } + + /// Reachable time in milliseconds. + pub fn reachable_time(&self) -> u32 { + // Safe to unwrap because `from_slice` guarantees `self.slice.len() >= FIXED_PART_LEN`, + // and `FIXED_PART_LEN` includes this full 4-byte field. + u32::from_be_bytes(*self.slice.first_chunk().unwrap()) + } + + /// Retransmit timer in milliseconds. + pub fn retrans_timer(&self) -> u32 { + // Safe to unwrap because `from_slice` guarantees `self.slice.len() >= FIXED_PART_LEN`, + // and `FIXED_PART_LEN` includes this full 4-byte field at the given offset. + u32::from_be_bytes( + *self.slice[Self::RETRANS_TIMER_OFFSET..] + .first_chunk() + .unwrap(), + ) + } + + /// Returns the Neighbor Discovery options. + pub fn options(&self) -> &'a [u8] { + &self.slice[Self::FIXED_PART_LEN..] + } + + /// Returns an iterator over Neighbor Discovery options. + pub fn options_iterator(&self) -> NdpOptionsIterator<'a> { + NdpOptionsIterator::from_slice(self.options()) + } + + /// Convert to an owned structured payload and return trailing options. + pub fn to_payload(&self) -> (RouterAdvertisementPayload, &'a [u8]) { + ( + RouterAdvertisementPayload { + reachable_time: self.reachable_time(), + retrans_timer: self.retrans_timer(), + }, + self.options(), + ) + } +} diff --git a/etherparse/src/transport/icmpv6/icmpv6_payload_slice/router_solicitation_payload_slice.rs b/etherparse/src/transport/icmpv6/icmpv6_payload_slice/router_solicitation_payload_slice.rs new file mode 100644 index 00000000..7a2b9f9a --- /dev/null +++ b/etherparse/src/transport/icmpv6/icmpv6_payload_slice/router_solicitation_payload_slice.rs @@ -0,0 +1,54 @@ +use crate::{ + err, + icmpv6::{NdpOptionsIterator, RouterSolicitationPayload}, +}; + +/// Borrowed payload of a Router Solicitation message (RFC 4861, Section 4.1). +/// +/// The full packet layout is: +/// ```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 | Code | Checksum | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Reserved | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Options ... +/// +-+-+-+-+-+-+-+-+-+-+-+- +/// ``` +/// +/// In this crate, the first 8 bytes (including `Reserved`) are represented by +/// [`crate::Icmpv6Type::RouterSolicitation`]. This slice represents the bytes +/// after that fixed part (i.e. the options area). +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RouterSolicitationPayloadSlice<'a> { + slice: &'a [u8], +} + +impl<'a> RouterSolicitationPayloadSlice<'a> { + /// Creates a payload slice from the bytes after the ICMPv6 header. + pub fn from_slice(slice: &'a [u8]) -> Result { + Ok(Self { slice }) + } + + /// Returns the full payload slice. + pub fn slice(&self) -> &'a [u8] { + self.slice + } + + /// Returns the Neighbor Discovery options. + pub fn options(&self) -> &'a [u8] { + self.slice + } + + /// Returns an iterator over Neighbor Discovery options. + pub fn options_iterator(&self) -> NdpOptionsIterator<'a> { + NdpOptionsIterator::from_slice(self.options()) + } + + /// Convert to an owned structured payload and return trailing options. + pub fn to_payload(&self) -> (RouterSolicitationPayload, &'a [u8]) { + (RouterSolicitationPayload, self.options()) + } +} diff --git a/etherparse/src/transport/icmpv6/mod.rs b/etherparse/src/transport/icmpv6/mod.rs index f8143dc3..58f761a9 100644 --- a/etherparse/src/transport/icmpv6/mod.rs +++ b/etherparse/src/transport/icmpv6/mod.rs @@ -13,6 +13,26 @@ pub use time_exceeded_code::*; mod neighbor_advertisement_header; pub use neighbor_advertisement_header::*; +pub use ndp_option::prefix_information::*; + +mod ndp_option_type_impl; +pub use ndp_option_type_impl::*; + +mod ndp_option; +pub use ndp_option::*; + +mod ndp_option_read_error; +pub use ndp_option_read_error::*; + +mod ndp_options_iterator; +pub use ndp_options_iterator::*; + +mod icmpv6_payload; +pub use icmpv6_payload::*; + +mod icmpv6_payload_slice; +pub use icmpv6_payload_slice::*; + mod router_advertisement_header; pub use router_advertisement_header::*; diff --git a/etherparse/src/transport/icmpv6/ndp_option/mod.rs b/etherparse/src/transport/icmpv6/ndp_option/mod.rs new file mode 100644 index 00000000..2e019f13 --- /dev/null +++ b/etherparse/src/transport/icmpv6/ndp_option/mod.rs @@ -0,0 +1,321 @@ +use crate::icmpv6::NdpOptionType; + +mod mtu_option_slice; +pub use mtu_option_slice::*; + +mod ndp_option_header; +pub use ndp_option_header::*; + +mod prefix_information_option_slice; +pub use prefix_information_option_slice::*; + +mod redirected_header_option_slice; +pub use redirected_header_option_slice::*; + +mod source_link_layer_address_option_slice; +pub use source_link_layer_address_option_slice::*; + +mod target_link_layer_address_option_slice; +pub use target_link_layer_address_option_slice::*; + +mod unknown_ndp_option_slice; +pub use unknown_ndp_option_slice::*; + +pub mod prefix_information; + +/// Length in bytes of the common Neighbor Discovery option header +/// (`Type` + `Length`). +pub(super) const NDP_OPTION_HEADER_LEN: usize = 2; + +/// Neighbor Discovery option decoded from an ICMPv6 payload. +#[derive(Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum NdpOptionSlice<'a> { + /// Source link-layer address option (type 1). + SourceLinkLayerAddress(SourceLinkLayerAddressOptionSlice<'a>), + /// Target link-layer address option (type 2). + TargetLinkLayerAddress(TargetLinkLayerAddressOptionSlice<'a>), + /// Prefix information option (type 3). + PrefixInformation(PrefixInformationOptionSlice<'a>), + /// Redirected header option (type 4). + RedirectedHeader(RedirectedHeaderOptionSlice<'a>), + /// MTU option (type 5). + Mtu(MtuOptionSlice<'a>), + /// Unknown option type. + Unknown(UnknownNdpOptionSlice<'a>), +} + +impl<'a> NdpOptionSlice<'a> { + /// Returns the serialized bytes of the option. + pub fn as_bytes(&self) -> &'a [u8] { + match self { + NdpOptionSlice::SourceLinkLayerAddress(value) => value.as_bytes(), + NdpOptionSlice::TargetLinkLayerAddress(value) => value.as_bytes(), + NdpOptionSlice::PrefixInformation(value) => value.as_bytes(), + NdpOptionSlice::RedirectedHeader(value) => value.as_bytes(), + NdpOptionSlice::Mtu(value) => value.as_bytes(), + NdpOptionSlice::Unknown(value) => value.as_bytes(), + } + } + + /// Returns the option type value. + pub fn option_type(&self) -> NdpOptionType { + match self { + NdpOptionSlice::SourceLinkLayerAddress(value) => value.option_type(), + NdpOptionSlice::TargetLinkLayerAddress(value) => value.option_type(), + NdpOptionSlice::PrefixInformation(value) => value.option_type(), + NdpOptionSlice::RedirectedHeader(value) => value.option_type(), + NdpOptionSlice::Mtu(value) => value.option_type(), + NdpOptionSlice::Unknown(value) => value.option_type(), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::icmpv6::{NdpOptionReadError, NdpOptionType, PrefixInformation}; + use alloc::format; + + #[test] + fn debug() { + assert_eq!( + "Mtu(MtuOptionSlice { slice: [5, 1, 0, 0, 0, 0, 5, 220] })", + format!( + "{:?}", + NdpOptionSlice::Mtu( + MtuOptionSlice::from_slice(&[5, 1, 0, 0, 0, 0, 5, 220]).unwrap() + ) + ) + ); + } + + #[test] + fn option_type_and_as_bytes() { + let source = + SourceLinkLayerAddressOptionSlice::from_slice(&[1, 1, 1, 2, 3, 4, 5, 6]).unwrap(); + assert_eq!( + NdpOptionType::SOURCE_LINK_LAYER_ADDRESS, + source.option_type() + ); + assert_eq!(&[1, 1, 1, 2, 3, 4, 5, 6], source.as_bytes()); + assert_eq!(&[1, 2, 3, 4, 5, 6], source.link_layer_address()); + + let target = + TargetLinkLayerAddressOptionSlice::from_slice(&[2, 1, 6, 5, 4, 3, 2, 1]).unwrap(); + assert_eq!( + NdpOptionType::TARGET_LINK_LAYER_ADDRESS, + target.option_type() + ); + assert_eq!(&[2, 1, 6, 5, 4, 3, 2, 1], target.as_bytes()); + assert_eq!(&[6, 5, 4, 3, 2, 1], target.link_layer_address()); + + let prefix = PrefixInformation { + prefix_length: 64, + on_link: true, + autonomous_address_configuration: true, + valid_lifetime: 1, + preferred_lifetime: 2, + prefix: [3; 16], + }; + let prefix_bytes = prefix.to_bytes(); + let prefix_slice = PrefixInformationOptionSlice::from_slice(&prefix_bytes).unwrap(); + assert_eq!( + NdpOptionType::PREFIX_INFORMATION, + prefix_slice.option_type() + ); + assert_eq!(&prefix_bytes, prefix_slice.as_bytes()); + assert_eq!(prefix.prefix_length, prefix_slice.prefix_length()); + assert_eq!(prefix.on_link, prefix_slice.on_link()); + assert_eq!( + prefix.autonomous_address_configuration, + prefix_slice.autonomous_address_configuration() + ); + assert_eq!(prefix.valid_lifetime, prefix_slice.valid_lifetime()); + assert_eq!(prefix.preferred_lifetime, prefix_slice.preferred_lifetime()); + assert_eq!(prefix.prefix, prefix_slice.prefix()); + assert_eq!(prefix, prefix_slice.prefix_information()); + + let redirected = RedirectedHeaderOptionSlice::from_slice(&[ + 4, 2, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, + ]) + .unwrap(); + assert_eq!(NdpOptionType::REDIRECTED_HEADER, redirected.option_type()); + assert_eq!( + &[4, 2, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8], + redirected.as_bytes() + ); + assert_eq!(&[1, 2, 3, 4, 5, 6, 7, 8], redirected.redirected_packet()); + + let mtu = MtuOptionSlice::from_slice(&[5, 1, 0, 0, 0, 0, 5, 220]).unwrap(); + assert_eq!(NdpOptionType::MTU, mtu.option_type()); + assert_eq!(&[5, 1, 0, 0, 0, 0, 5, 220], mtu.as_bytes()); + assert_eq!(1500, mtu.mtu()); + + let unknown = UnknownNdpOptionSlice::from_slice(&[250, 1, 1, 2, 3, 4, 5, 6]).unwrap(); + assert_eq!(NdpOptionType(250), unknown.option_type()); + assert_eq!(&[250, 1, 1, 2, 3, 4, 5, 6], unknown.as_bytes()); + assert_eq!(&[1, 2, 3, 4, 5, 6], unknown.data()); + } + + #[test] + fn from_slice_errors() { + assert_eq!( + Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType::SOURCE_LINK_LAYER_ADDRESS, + expected_size: NDP_OPTION_HEADER_LEN, + actual_size: 1 + }), + SourceLinkLayerAddressOptionSlice::from_slice(&[1]) + ); + assert_eq!( + Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType::TARGET_LINK_LAYER_ADDRESS, + expected_size: NDP_OPTION_HEADER_LEN, + actual_size: 1 + }), + TargetLinkLayerAddressOptionSlice::from_slice(&[2]) + ); + assert_eq!( + Err(NdpOptionReadError::UnexpectedHeader { + expected_option_id: NdpOptionType::SOURCE_LINK_LAYER_ADDRESS, + actual_option_id: NdpOptionType::TARGET_LINK_LAYER_ADDRESS, + expected_length_units: 1, + actual_length_units: 1, + }), + SourceLinkLayerAddressOptionSlice::from_slice(&[2, 1, 1, 2, 3, 4, 5, 6]) + ); + assert_eq!( + Err(NdpOptionReadError::ZeroLength { + option_id: NdpOptionType::SOURCE_LINK_LAYER_ADDRESS + }), + SourceLinkLayerAddressOptionSlice::from_slice(&[1, 0]) + ); + assert_eq!( + Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType::SOURCE_LINK_LAYER_ADDRESS, + expected_size: 16, + actual_size: 8 + }), + SourceLinkLayerAddressOptionSlice::from_slice(&[1, 2, 1, 2, 3, 4, 5, 6]) + ); + assert_eq!( + Err(NdpOptionReadError::UnexpectedHeader { + expected_option_id: NdpOptionType::TARGET_LINK_LAYER_ADDRESS, + actual_option_id: NdpOptionType::SOURCE_LINK_LAYER_ADDRESS, + expected_length_units: 1, + actual_length_units: 1, + }), + TargetLinkLayerAddressOptionSlice::from_slice(&[1, 1, 1, 2, 3, 4, 5, 6]) + ); + assert_eq!( + Err(NdpOptionReadError::ZeroLength { + option_id: NdpOptionType::TARGET_LINK_LAYER_ADDRESS + }), + TargetLinkLayerAddressOptionSlice::from_slice(&[2, 0]) + ); + assert_eq!( + Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType::TARGET_LINK_LAYER_ADDRESS, + expected_size: 16, + actual_size: 8 + }), + TargetLinkLayerAddressOptionSlice::from_slice(&[2, 2, 1, 2, 3, 4, 5, 6]) + ); + assert_eq!( + Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType::PREFIX_INFORMATION, + expected_size: PrefixInformation::LEN, + actual_size: PrefixInformation::LEN - 1 + }), + PrefixInformationOptionSlice::from_slice(&[0u8; PrefixInformation::LEN - 1]) + ); + assert_eq!( + Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType::REDIRECTED_HEADER, + expected_size: 8, + actual_size: 7 + }), + RedirectedHeaderOptionSlice::from_slice(&[0u8; 7]) + ); + assert_eq!( + Err(NdpOptionReadError::UnexpectedHeader { + expected_option_id: NdpOptionType::REDIRECTED_HEADER, + actual_option_id: NdpOptionType::MTU, + expected_length_units: 1, + actual_length_units: 1, + }), + RedirectedHeaderOptionSlice::from_slice(&[5, 1, 0, 0, 0, 0, 0, 0]) + ); + assert_eq!( + Err(NdpOptionReadError::ZeroLength { + option_id: NdpOptionType::REDIRECTED_HEADER + }), + RedirectedHeaderOptionSlice::from_slice(&[4, 0, 0, 0, 0, 0, 0, 0]) + ); + assert_eq!( + Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType::REDIRECTED_HEADER, + expected_size: 16, + actual_size: 8 + }), + RedirectedHeaderOptionSlice::from_slice(&[4, 2, 0, 0, 0, 0, 0, 0]) + ); + assert_eq!( + Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType::MTU, + expected_size: 8, + actual_size: 7 + }), + MtuOptionSlice::from_slice(&[0u8; 7]) + ); + assert_eq!( + Err(NdpOptionReadError::UnexpectedHeader { + expected_option_id: NdpOptionType::MTU, + actual_option_id: NdpOptionType::REDIRECTED_HEADER, + expected_length_units: 1, + actual_length_units: 1, + }), + MtuOptionSlice::from_slice(&[4, 1, 0, 0, 0, 0, 5, 220]) + ); + assert_eq!( + Err(NdpOptionReadError::UnexpectedHeader { + expected_option_id: NdpOptionType::MTU, + actual_option_id: NdpOptionType::MTU, + expected_length_units: 1, + actual_length_units: 2, + }), + MtuOptionSlice::from_slice(&[5, 2, 0, 0, 0, 0, 5, 220]) + ); + assert_eq!( + Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType(0), + expected_size: NDP_OPTION_HEADER_LEN, + actual_size: 0 + }), + UnknownNdpOptionSlice::from_slice(&[]) + ); + assert_eq!( + Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType(250), + expected_size: NDP_OPTION_HEADER_LEN, + actual_size: 1 + }), + UnknownNdpOptionSlice::from_slice(&[250]) + ); + assert_eq!( + Err(NdpOptionReadError::ZeroLength { + option_id: NdpOptionType(250) + }), + UnknownNdpOptionSlice::from_slice(&[250, 0]) + ); + assert_eq!( + Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType(250), + expected_size: 16, + actual_size: 8 + }), + UnknownNdpOptionSlice::from_slice(&[250, 2, 1, 2, 3, 4, 5, 6]) + ); + } +} diff --git a/etherparse/src/transport/icmpv6/ndp_option/mtu_option_slice.rs b/etherparse/src/transport/icmpv6/ndp_option/mtu_option_slice.rs new file mode 100644 index 00000000..633f6046 --- /dev/null +++ b/etherparse/src/transport/icmpv6/ndp_option/mtu_option_slice.rs @@ -0,0 +1,64 @@ +use crate::icmpv6::{NdpOptionHeader, NdpOptionReadError, NdpOptionType}; + +/// MTU option slice (RFC 4861, Section 4.6.4, type 5). +/// +/// The option layout is: +/// ```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 | Length | Reserved | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | MTU | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +/// +/// This slice stores the full serialized option, including the +/// `Type` and `Length` bytes. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MtuOptionSlice<'a> { + slice: &'a [u8; MtuOptionSlice::LEN], +} + +impl<'a> MtuOptionSlice<'a> { + /// Serialized MTU option length in bytes. + pub const LEN: usize = 8; + const LENGTH_UNITS: u8 = 1; + + pub fn from_slice(slice: &'a [u8]) -> Result { + let slice = <&[u8; MtuOptionSlice::LEN]>::try_from(slice).map_err(|_| { + NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType::MTU, + expected_size: MtuOptionSlice::LEN, + actual_size: slice.len(), + } + })?; + + let header = NdpOptionHeader::from_bytes([slice[0], slice[1]]); + if header.option_type != NdpOptionType::MTU || header.length_units != Self::LENGTH_UNITS { + return Err(NdpOptionReadError::UnexpectedHeader { + expected_option_id: NdpOptionType::MTU, + actual_option_id: header.option_type, + expected_length_units: Self::LENGTH_UNITS, + actual_length_units: header.length_units, + }); + } + + Ok(Self { slice }) + } + + /// Returns the option type value (5). + pub const fn option_type(&self) -> NdpOptionType { + NdpOptionType::MTU + } + + /// Returns the serialized option bytes. + pub fn as_bytes(&self) -> &'a [u8] { + self.slice + } + + /// Returns the MTU value carried by the option. + pub fn mtu(&self) -> u32 { + u32::from_be_bytes([self.slice[4], self.slice[5], self.slice[6], self.slice[7]]) + } +} diff --git a/etherparse/src/transport/icmpv6/ndp_option/ndp_option_header.rs b/etherparse/src/transport/icmpv6/ndp_option/ndp_option_header.rs new file mode 100644 index 00000000..fbfd930f --- /dev/null +++ b/etherparse/src/transport/icmpv6/ndp_option/ndp_option_header.rs @@ -0,0 +1,112 @@ +use super::NDP_OPTION_HEADER_LEN; +use crate::icmpv6::{NdpOptionReadError, NdpOptionType}; + +/// Header present at the beginning of every Neighbor Discovery option. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct NdpOptionHeader { + /// Option `Type` field. + pub option_type: NdpOptionType, + /// Option `Length` field in units of 8 octets. + pub length_units: u8, +} + +impl NdpOptionHeader { + /// Serialized size of the option header in bytes. + pub const LEN: usize = NDP_OPTION_HEADER_LEN; + + /// Decodes a header from its serialized bytes. + pub fn from_bytes(bytes: [u8; NDP_OPTION_HEADER_LEN]) -> Self { + Self { + option_type: NdpOptionType(bytes[0]), + length_units: bytes[1], + } + } + + /// Encodes the header to serialized bytes. + pub const fn to_bytes(&self) -> [u8; NDP_OPTION_HEADER_LEN] { + [self.option_type.0, self.length_units] + } + + /// Decodes the header from the start of a serialized options slice. + pub fn from_slice(slice: &[u8]) -> Result<(Self, &[u8]), NdpOptionReadError> { + let (bytes, rest) = slice + .split_first_chunk::() + .ok_or_else(|| NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType(slice.first().copied().unwrap_or(0)), + expected_size: NDP_OPTION_HEADER_LEN, + actual_size: slice.len(), + })?; + Ok((Self::from_bytes(*bytes), rest)) + } + + /// Returns the total serialized option size in bytes. + pub const fn byte_len(&self) -> usize { + (self.length_units as usize) * 8 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_to_bytes() { + let header = NdpOptionHeader { + option_type: NdpOptionType::MTU, + length_units: 1, + }; + assert_eq!(header, NdpOptionHeader::from_bytes(header.to_bytes())); + } + + #[test] + fn from_slice() { + assert_eq!( + Ok(( + NdpOptionHeader { + option_type: NdpOptionType::PREFIX_INFORMATION, + length_units: 4, + }, + &[][..] + )), + NdpOptionHeader::from_slice(&[3, 4]), + ); + assert_eq!( + Ok(( + NdpOptionHeader { + option_type: NdpOptionType::PREFIX_INFORMATION, + length_units: 4, + }, + &[1, 2, 3][..] + )), + NdpOptionHeader::from_slice(&[3, 4, 1, 2, 3]), + ); + assert_eq!( + Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType(250), + expected_size: NdpOptionHeader::LEN, + actual_size: 1, + }), + NdpOptionHeader::from_slice(&[250]), + ); + assert_eq!( + Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType(0), + expected_size: NdpOptionHeader::LEN, + actual_size: 0, + }), + NdpOptionHeader::from_slice(&[]), + ); + } + + #[test] + fn byte_len() { + assert_eq!( + 32, + NdpOptionHeader { + option_type: NdpOptionType::PREFIX_INFORMATION, + length_units: 4, + } + .byte_len() + ); + } +} diff --git a/etherparse/src/transport/icmpv6/ndp_option/prefix_information.rs b/etherparse/src/transport/icmpv6/ndp_option/prefix_information.rs new file mode 100644 index 00000000..0d116889 --- /dev/null +++ b/etherparse/src/transport/icmpv6/ndp_option/prefix_information.rs @@ -0,0 +1,204 @@ +use crate::icmpv6::{NdpOptionReadError, NdpOptionType}; + +/// Prefix Information option payload defined in RFC 4861. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct PrefixInformation { + /// Number of leading bits in the prefix that are valid. + pub prefix_length: u8, + /// "On-link" flag. + pub on_link: bool, + /// "Autonomous address-configuration" flag. + pub autonomous_address_configuration: bool, + /// Lifetime in seconds for which the prefix is valid. + pub valid_lifetime: u32, + /// Lifetime in seconds for which addresses generated from the prefix remain preferred. + pub preferred_lifetime: u32, + /// The advertised prefix. + pub prefix: [u8; 16], +} + +impl PrefixInformation { + /// Length of a prefix information option in units of 8 octets. + const LENGTH_UNITS: u8 = 4; + + /// Serialized length in bytes. + pub const LEN: usize = 32; + + /// Mask to read the "on-link" flag. + pub const ON_LINK_MASK: u8 = 0b1000_0000; + + /// Mask to read the "autonomous address-configuration" flag. + pub const AUTONOMOUS_ADDRESS_CONFIGURATION_MASK: u8 = 0b0100_0000; + + /// Decode the option from the on-the-wire bytes. + pub fn from_bytes(bytes: [u8; Self::LEN]) -> Result { + let bytes = bytes.as_slice(); + // Safe to unwrap because `bytes` originates from `[u8; Self::LEN]` and + // the chunk sizes below exactly cover `Self::LEN` (32 bytes). + let (type_and_len, rest) = bytes.split_first_chunk::<2>().unwrap(); + if *type_and_len + != [ + NdpOptionType::PREFIX_INFORMATION.0, + Self::LENGTH_UNITS, + ] + { + return Err(NdpOptionReadError::UnexpectedHeader { + expected_option_id: NdpOptionType::PREFIX_INFORMATION, + actual_option_id: NdpOptionType(type_and_len[0]), + expected_length_units: Self::LENGTH_UNITS, + actual_length_units: type_and_len[1], + }); + } + let (prefix_length_and_flags, rest) = rest.split_first_chunk::<2>().unwrap(); + let (valid_lifetime, rest) = rest.split_first_chunk::<4>().unwrap(); + let (preferred_lifetime, rest) = rest.split_first_chunk::<4>().unwrap(); + let (_reserved2, prefix) = rest.split_first_chunk::<4>().unwrap(); + let prefix = *prefix.first_chunk::<16>().unwrap(); + + Ok(PrefixInformation { + prefix_length: prefix_length_and_flags[0], + on_link: 0 != prefix_length_and_flags[1] & Self::ON_LINK_MASK, + autonomous_address_configuration: 0 + != prefix_length_and_flags[1] & Self::AUTONOMOUS_ADDRESS_CONFIGURATION_MASK, + valid_lifetime: u32::from_be_bytes(*valid_lifetime), + preferred_lifetime: u32::from_be_bytes(*preferred_lifetime), + prefix, + }) + } + + /// Decode the option from a byte slice. + /// + /// The slice must contain exactly [`PrefixInformation::LEN`] bytes. + pub fn from_slice(bytes: &[u8]) -> Result { + let bytes: &[u8; Self::LEN] = + bytes + .try_into() + .map_err(|_| NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType::PREFIX_INFORMATION, + expected_size: Self::LEN, + actual_size: bytes.len(), + })?; + PrefixInformation::from_bytes(*bytes) + } + + /// Convert the prefix information to the on-the-wire bytes. + pub fn to_bytes(&self) -> [u8; Self::LEN] { + let mut bytes = [0u8; Self::LEN]; + // Safe to unwrap because chunk sizes below exactly cover `Self::LEN` (32 bytes). + let (type_and_len, rest) = bytes.as_mut_slice().split_first_chunk_mut::<2>().unwrap(); + let (prefix_length_and_flags, rest) = rest.split_first_chunk_mut::<2>().unwrap(); + let (valid_lifetime, rest) = rest.split_first_chunk_mut::<4>().unwrap(); + let (preferred_lifetime, rest) = rest.split_first_chunk_mut::<4>().unwrap(); + let (_reserved2, prefix) = rest.split_first_chunk_mut::<4>().unwrap(); + let prefix = prefix.first_chunk_mut::<16>().unwrap(); + + *type_and_len = [NdpOptionType::PREFIX_INFORMATION.into(), Self::LENGTH_UNITS]; + *prefix_length_and_flags = [ + self.prefix_length, + (if self.on_link { Self::ON_LINK_MASK } else { 0 }) + | if self.autonomous_address_configuration { + Self::AUTONOMOUS_ADDRESS_CONFIGURATION_MASK + } else { + 0 + }, + ]; + *valid_lifetime = self.valid_lifetime.to_be_bytes(); + *preferred_lifetime = self.preferred_lifetime.to_be_bytes(); + *prefix = self.prefix; + + bytes + } +} + +#[cfg(test)] +mod tests { + use super::*; + use proptest::prelude::*; + + proptest! { + #[test] + fn to_and_from_bytes( + prefix_length in any::(), + on_link in any::(), + autonomous_address_configuration in any::(), + valid_lifetime in any::(), + preferred_lifetime in any::(), + prefix in any::<[u8;16]>() + ) { + let value = PrefixInformation { + prefix_length, + on_link, + autonomous_address_configuration, + valid_lifetime, + preferred_lifetime, + prefix, + }; + assert_eq!(PrefixInformation::from_bytes(value.to_bytes()), Ok(value)); + assert_eq!(PrefixInformation::from_slice(&value.to_bytes()), Ok(value)); + } + } + + #[test] + fn from_slice_len_error() { + assert_eq!( + PrefixInformation::from_slice(&[0u8; PrefixInformation::LEN - 1]), + Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType::PREFIX_INFORMATION, + expected_size: PrefixInformation::LEN, + actual_size: PrefixInformation::LEN - 1, + }) + ); + assert_eq!( + PrefixInformation::from_slice(&[0u8; PrefixInformation::LEN + 1]), + Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType::PREFIX_INFORMATION, + expected_size: PrefixInformation::LEN, + actual_size: PrefixInformation::LEN + 1, + }) + ); + } + + #[test] + fn from_bytes_invalid_header() { + let mut bytes = PrefixInformation { + prefix_length: 64, + on_link: true, + autonomous_address_configuration: true, + valid_lifetime: 1, + preferred_lifetime: 2, + prefix: [3; 16], + } + .to_bytes(); + + bytes[0] = NdpOptionType::MTU.0; + assert_eq!( + PrefixInformation::from_bytes(bytes), + Err(NdpOptionReadError::UnexpectedHeader { + expected_option_id: NdpOptionType::PREFIX_INFORMATION, + actual_option_id: NdpOptionType::MTU, + expected_length_units: 4, + actual_length_units: 4, + }) + ); + + let mut bytes = PrefixInformation { + prefix_length: 64, + on_link: true, + autonomous_address_configuration: true, + valid_lifetime: 1, + preferred_lifetime: 2, + prefix: [3; 16], + } + .to_bytes(); + bytes[1] = 1; + assert_eq!( + PrefixInformation::from_bytes(bytes), + Err(NdpOptionReadError::UnexpectedHeader { + expected_option_id: NdpOptionType::PREFIX_INFORMATION, + actual_option_id: NdpOptionType::PREFIX_INFORMATION, + expected_length_units: 4, + actual_length_units: 1, + }) + ); + } +} diff --git a/etherparse/src/transport/icmpv6/ndp_option/prefix_information_option_slice.rs b/etherparse/src/transport/icmpv6/ndp_option/prefix_information_option_slice.rs new file mode 100644 index 00000000..081bbb44 --- /dev/null +++ b/etherparse/src/transport/icmpv6/ndp_option/prefix_information_option_slice.rs @@ -0,0 +1,117 @@ +use crate::icmpv6::{NdpOptionReadError, NdpOptionType, PrefixInformation}; + +/// Prefix Information option slice (RFC 4861, Section 4.6.2, type 3). +/// +/// The option layout is: +/// ```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 | Length | Prefix Length |L|A| Reserved1 | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Valid Lifetime | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Preferred Lifetime | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Reserved2 | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// + + +/// | | +/// + Prefix + +/// | | +/// + + +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +/// +/// This slice stores the full serialized option, including the +/// `Type` and `Length` bytes. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PrefixInformationOptionSlice<'a> { + slice: &'a [u8; PrefixInformation::LEN], +} + +impl<'a> PrefixInformationOptionSlice<'a> { + const PREFIX_LENGTH_OFFSET: usize = 2; + const FLAGS_OFFSET: usize = 3; + const VALID_LIFETIME_OFFSET: usize = 4; + const PREFERRED_LIFETIME_OFFSET: usize = 8; + const PREFIX_OFFSET: usize = 16; + + pub fn from_slice(slice: &'a [u8]) -> Result { + let slice: &'a [u8; PrefixInformation::LEN] = + slice.try_into().map_err(|_| NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType::PREFIX_INFORMATION, + expected_size: PrefixInformation::LEN, + actual_size: slice.len(), + })?; + // Validate the encoded option header (`Type` and `Length`) as well. + PrefixInformation::from_bytes(*slice)?; + Ok(Self { slice }) + } + + /// Returns the option type value (3). + pub const fn option_type(&self) -> NdpOptionType { + NdpOptionType::PREFIX_INFORMATION + } + + /// Returns the serialized option bytes. + pub fn as_bytes(&self) -> &'a [u8; PrefixInformation::LEN] { + self.slice + } + + /// Returns the prefix length (in bits). + pub fn prefix_length(&self) -> u8 { + self.slice[Self::PREFIX_LENGTH_OFFSET] + } + + /// Returns the `L` (on-link) flag. + pub fn on_link(&self) -> bool { + 0 != self.slice[Self::FLAGS_OFFSET] & PrefixInformation::ON_LINK_MASK + } + + /// Returns the `A` (autonomous address-configuration) flag. + pub fn autonomous_address_configuration(&self) -> bool { + 0 != self.slice[Self::FLAGS_OFFSET] + & PrefixInformation::AUTONOMOUS_ADDRESS_CONFIGURATION_MASK + } + + /// Returns the valid lifetime in seconds. + pub fn valid_lifetime(&self) -> u32 { + // Safe to unwrap because `self.slice` is always exactly `PrefixInformation::LEN` bytes. + u32::from_be_bytes( + *self.slice[Self::VALID_LIFETIME_OFFSET..] + .first_chunk() + .unwrap(), + ) + } + + /// Returns the preferred lifetime in seconds. + pub fn preferred_lifetime(&self) -> u32 { + // Safe to unwrap because `self.slice` is always exactly `PrefixInformation::LEN` bytes. + u32::from_be_bytes( + *self.slice[Self::PREFERRED_LIFETIME_OFFSET..] + .first_chunk() + .unwrap(), + ) + } + + /// Returns the 128-bit prefix field. + pub fn prefix(&self) -> [u8; 16] { + // Safe to unwrap because `self.slice` is always exactly `PrefixInformation::LEN` bytes. + *self.slice[Self::PREFIX_OFFSET..].first_chunk().unwrap() + } + + /// Decodes the option as [`PrefixInformation`]. + pub fn prefix_information(&self) -> PrefixInformation { + PrefixInformation { + prefix_length: self.prefix_length(), + on_link: self.on_link(), + autonomous_address_configuration: self.autonomous_address_configuration(), + valid_lifetime: self.valid_lifetime(), + preferred_lifetime: self.preferred_lifetime(), + prefix: self.prefix(), + } + } +} diff --git a/etherparse/src/transport/icmpv6/ndp_option/redirected_header_option_slice.rs b/etherparse/src/transport/icmpv6/ndp_option/redirected_header_option_slice.rs new file mode 100644 index 00000000..04bb1ad5 --- /dev/null +++ b/etherparse/src/transport/icmpv6/ndp_option/redirected_header_option_slice.rs @@ -0,0 +1,86 @@ +use crate::icmpv6::{NdpOptionHeader, NdpOptionReadError, NdpOptionType}; + +/// Redirected Header option slice (RFC 4861, Section 4.6.3, type 4). +/// +/// The option layout is: +/// ```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 | Length | Reserved | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Reserved | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// ~ IP header + data ~ +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +/// +/// This slice stores the full serialized option, including the +/// `Type` and `Length` bytes. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RedirectedHeaderOptionSlice<'a> { + slice: &'a [u8], +} + +impl<'a> RedirectedHeaderOptionSlice<'a> { + /// Length in bytes of the fixed part (`Type`, `Length`, and reserved fields). + const FIXED_PART_LEN: usize = 8; + + pub fn from_slice(slice: &'a [u8]) -> Result { + if slice.len() < Self::FIXED_PART_LEN { + return Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType::REDIRECTED_HEADER, + expected_size: Self::FIXED_PART_LEN, + actual_size: slice.len(), + }); + } + + let (header, _) = NdpOptionHeader::from_slice(slice)?; + if NdpOptionType::REDIRECTED_HEADER != header.option_type { + return Err(NdpOptionReadError::UnexpectedHeader { + expected_option_id: NdpOptionType::REDIRECTED_HEADER, + actual_option_id: header.option_type, + expected_length_units: header.length_units, + actual_length_units: header.length_units, + }); + } + if 0 == header.length_units { + return Err(NdpOptionReadError::ZeroLength { + option_id: header.option_type, + }); + } + let expected_size = header.byte_len(); + if expected_size != slice.len() { + return Err(NdpOptionReadError::UnexpectedSize { + option_id: header.option_type, + expected_size, + actual_size: slice.len(), + }); + } + + Ok(Self { slice }) + } + + /// Returns the option type value (4). + pub const fn option_type(&self) -> NdpOptionType { + NdpOptionType::REDIRECTED_HEADER + } + + /// Returns the serialized option bytes. + pub fn as_bytes(&self) -> &'a [u8] { + self.slice + } + + /// Returns the redirected packet bytes carried by the option. + /// + /// Note that this slice may include trailing zero-padding, since the option + /// length is encoded in 8-octet units. Callers must not assume all returned + /// bytes are meaningful packet data. Trim or parse the embedded packet using + /// its own length fields (or a helper that strips padding) before + /// interpreting payload bytes. + pub fn redirected_packet(&self) -> &'a [u8] { + &self.slice[Self::FIXED_PART_LEN..] + } +} diff --git a/etherparse/src/transport/icmpv6/ndp_option/source_link_layer_address_option_slice.rs b/etherparse/src/transport/icmpv6/ndp_option/source_link_layer_address_option_slice.rs new file mode 100644 index 00000000..f151f58c --- /dev/null +++ b/etherparse/src/transport/icmpv6/ndp_option/source_link_layer_address_option_slice.rs @@ -0,0 +1,67 @@ +use super::NDP_OPTION_HEADER_LEN; +use crate::icmpv6::{NdpOptionHeader, NdpOptionReadError, NdpOptionType}; + +/// Source Link-Layer Address option slice (RFC 4861, Section 4.6.1, type 1). +/// +/// The option layout is: +/// ```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 | Length | Link-Layer Address ... +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +/// +/// This slice stores the full serialized option, including the +/// `Type` and `Length` bytes. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SourceLinkLayerAddressOptionSlice<'a> { + slice: &'a [u8], +} + +impl<'a> SourceLinkLayerAddressOptionSlice<'a> { + pub fn from_slice(slice: &'a [u8]) -> Result { + let (header, _) = NdpOptionHeader::from_slice(slice)?; + + if NdpOptionType::SOURCE_LINK_LAYER_ADDRESS != header.option_type { + return Err(NdpOptionReadError::UnexpectedHeader { + expected_option_id: NdpOptionType::SOURCE_LINK_LAYER_ADDRESS, + actual_option_id: header.option_type, + expected_length_units: header.length_units, + actual_length_units: header.length_units, + }); + } + + if 0 == header.length_units { + return Err(NdpOptionReadError::ZeroLength { + option_id: header.option_type, + }); + } + + let expected_size = header.byte_len(); + if expected_size != slice.len() { + return Err(NdpOptionReadError::UnexpectedSize { + option_id: header.option_type, + expected_size, + actual_size: slice.len(), + }); + } + + Ok(Self { slice }) + } + + /// Returns the option type value (1). + pub const fn option_type(&self) -> NdpOptionType { + NdpOptionType::SOURCE_LINK_LAYER_ADDRESS + } + + /// Returns the serialized option bytes. + pub fn as_bytes(&self) -> &'a [u8] { + self.slice + } + + /// Returns the link-layer address bytes carried by the option. + pub fn link_layer_address(&self) -> &'a [u8] { + &self.slice[NDP_OPTION_HEADER_LEN..] + } +} diff --git a/etherparse/src/transport/icmpv6/ndp_option/target_link_layer_address_option_slice.rs b/etherparse/src/transport/icmpv6/ndp_option/target_link_layer_address_option_slice.rs new file mode 100644 index 00000000..275789f1 --- /dev/null +++ b/etherparse/src/transport/icmpv6/ndp_option/target_link_layer_address_option_slice.rs @@ -0,0 +1,67 @@ +use super::NDP_OPTION_HEADER_LEN; +use crate::icmpv6::{NdpOptionHeader, NdpOptionReadError, NdpOptionType}; + +/// Target Link-Layer Address option slice (RFC 4861, Section 4.6.1, type 2). +/// +/// The option layout is: +/// ```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 | Length | Link-Layer Address ... +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +/// +/// This slice stores the full serialized option, including the +/// `Type` and `Length` bytes. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TargetLinkLayerAddressOptionSlice<'a> { + slice: &'a [u8], +} + +impl<'a> TargetLinkLayerAddressOptionSlice<'a> { + pub fn from_slice(slice: &'a [u8]) -> Result { + let (header, _) = NdpOptionHeader::from_slice(slice)?; + + if NdpOptionType::TARGET_LINK_LAYER_ADDRESS != header.option_type { + return Err(NdpOptionReadError::UnexpectedHeader { + expected_option_id: NdpOptionType::TARGET_LINK_LAYER_ADDRESS, + actual_option_id: header.option_type, + expected_length_units: header.length_units, + actual_length_units: header.length_units, + }); + } + + if 0 == header.length_units { + return Err(NdpOptionReadError::ZeroLength { + option_id: header.option_type, + }); + } + + let expected_size = header.byte_len(); + if expected_size != slice.len() { + return Err(NdpOptionReadError::UnexpectedSize { + option_id: header.option_type, + expected_size, + actual_size: slice.len(), + }); + } + + Ok(Self { slice }) + } + + /// Returns the option type value (2). + pub const fn option_type(&self) -> NdpOptionType { + NdpOptionType::TARGET_LINK_LAYER_ADDRESS + } + + /// Returns the serialized option bytes. + pub fn as_bytes(&self) -> &'a [u8] { + self.slice + } + + /// Returns the link-layer address bytes carried by the option. + pub fn link_layer_address(&self) -> &'a [u8] { + &self.slice[NDP_OPTION_HEADER_LEN..] + } +} diff --git a/etherparse/src/transport/icmpv6/ndp_option/unknown_ndp_option_slice.rs b/etherparse/src/transport/icmpv6/ndp_option/unknown_ndp_option_slice.rs new file mode 100644 index 00000000..6a100c02 --- /dev/null +++ b/etherparse/src/transport/icmpv6/ndp_option/unknown_ndp_option_slice.rs @@ -0,0 +1,57 @@ +use super::NDP_OPTION_HEADER_LEN; +use crate::icmpv6::{NdpOptionHeader, NdpOptionReadError, NdpOptionType}; + +/// Unknown Neighbor Discovery option slice. +/// +/// All ND options begin with this common prefix: +/// ```text +/// 0 1 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Type | Length | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +/// +/// This slice stores the full serialized option, including the +/// `Type` and `Length` bytes. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UnknownNdpOptionSlice<'a> { + slice: &'a [u8], +} + +impl<'a> UnknownNdpOptionSlice<'a> { + pub fn from_slice(slice: &'a [u8]) -> Result { + let (header, _) = NdpOptionHeader::from_slice(slice)?; + if 0 == header.length_units { + return Err(NdpOptionReadError::ZeroLength { + option_id: header.option_type, + }); + } + + let expected_size = header.byte_len(); + if expected_size != slice.len() { + return Err(NdpOptionReadError::UnexpectedSize { + option_id: header.option_type, + expected_size, + actual_size: slice.len(), + }); + } + + Ok(Self { slice }) + } + + /// Returns the option type value. + pub fn option_type(&self) -> NdpOptionType { + NdpOptionType(self.slice[0]) + } + + /// Returns the serialized option bytes. + pub fn as_bytes(&self) -> &'a [u8] { + self.slice + } + + /// Returns the option data bytes after the type/length fields. + pub fn data(&self) -> &'a [u8] { + &self.slice[NDP_OPTION_HEADER_LEN..] + } +} diff --git a/etherparse/src/transport/icmpv6/ndp_option_read_error.rs b/etherparse/src/transport/icmpv6/ndp_option_read_error.rs new file mode 100644 index 00000000..72d49ded --- /dev/null +++ b/etherparse/src/transport/icmpv6/ndp_option_read_error.rs @@ -0,0 +1,73 @@ +use crate::icmpv6::NdpOptionType; + +/// Error while decoding Neighbor Discovery options. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[non_exhaustive] +pub enum NdpOptionReadError { + /// Not enough bytes are left to decode the option or its body. + UnexpectedEndOfSlice { + option_id: NdpOptionType, + expected_size: usize, + actual_size: usize, + }, + /// An ND option with a length value of zero was encountered. + ZeroLength { option_id: NdpOptionType }, + /// The option has a fixed encoded size and the received size differs. + UnexpectedSize { option_id: NdpOptionType, + expected_size: usize, + actual_size: usize, + }, + /// The option header `Type` and/or `Length` fields do not match expectations. + UnexpectedHeader { + expected_option_id: NdpOptionType, + actual_option_id: NdpOptionType, + expected_length_units: u8, + actual_length_units: u8, + }, +} + +impl core::fmt::Display for NdpOptionReadError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + use NdpOptionReadError::*; + match self { + UnexpectedEndOfSlice { + option_id, + expected_size: expected_len, + actual_size: actual_len, + } => write!( + f, + "NDP option error: Not enough bytes left to read option of type {} (expected at least {expected_len} bytes, only {actual_len} bytes available).", + option_id.0, + ), + ZeroLength { option_id } => write!( + f, + "NDP option error: Encountered option of type {} with an invalid length of zero.", + option_id.0, + ), + UnexpectedSize { option_id, expected_size, actual_size, } => write!( + f, + "NDP option error: Option of type {} had unexpected size value {actual_size} (expected {expected_size}).", + option_id.0, + ), + UnexpectedHeader { + expected_option_id, + actual_option_id, + expected_length_units, + actual_length_units, + } => write!( + f, + "NDP option error: Option header mismatch (expected type {} length {}, got type {} length {}).", + expected_option_id.0, + expected_length_units, + actual_option_id.0, + actual_length_units, + ), + } + } +} + +impl core::error::Error for NdpOptionReadError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + None + } +} diff --git a/etherparse/src/transport/icmpv6/ndp_option_type_impl.rs b/etherparse/src/transport/icmpv6/ndp_option_type_impl.rs new file mode 100644 index 00000000..6c012294 --- /dev/null +++ b/etherparse/src/transport/icmpv6/ndp_option_type_impl.rs @@ -0,0 +1,68 @@ +/// Identifiers for Neighbor Discovery option `Type` values. +/// +/// You can access the underlying `u8` value via `.0`, and any `u8` can +/// be converted into an `NdpOptionType`: +/// +/// ``` +/// use etherparse::icmpv6::NdpOptionType; +/// +/// assert_eq!(NdpOptionType::SOURCE_LINK_LAYER_ADDRESS.0, 1); +/// assert_eq!(NdpOptionType::SOURCE_LINK_LAYER_ADDRESS, NdpOptionType(1)); +/// +/// let option_type: NdpOptionType = 3u8.into(); +/// assert_eq!(NdpOptionType::PREFIX_INFORMATION, option_type); +/// +/// let raw: u8 = NdpOptionType::MTU.into(); +/// assert_eq!(5, raw); +/// ``` +#[derive(PartialEq, Eq, Clone, Copy, Hash, Ord, PartialOrd)] +pub struct NdpOptionType(pub u8); + +impl NdpOptionType { + /// Source Link-Layer Address option \[[RFC4861](https://datatracker.ietf.org/doc/html/rfc4861#section-4.6.1)\]. + pub const SOURCE_LINK_LAYER_ADDRESS: Self = Self(1); + /// Target Link-Layer Address option \[[RFC4861](https://datatracker.ietf.org/doc/html/rfc4861#section-4.6.1)\]. + pub const TARGET_LINK_LAYER_ADDRESS: Self = Self(2); + /// Prefix Information option \[[RFC4861](https://datatracker.ietf.org/doc/html/rfc4861#section-4.6.2)\]. + pub const PREFIX_INFORMATION: Self = Self(3); + /// Redirected Header option \[[RFC4861](https://datatracker.ietf.org/doc/html/rfc4861#section-4.6.3)\]. + pub const REDIRECTED_HEADER: Self = Self(4); + /// MTU option \[[RFC4861](https://datatracker.ietf.org/doc/html/rfc4861#section-4.6.4)\]. + pub const MTU: Self = Self(5); + + /// Human-readable name for known option types. + pub const fn keyword_str(self) -> Option<&'static str> { + match self.0 { + 1 => Some("Source Link-Layer Address"), + 2 => Some("Target Link-Layer Address"), + 3 => Some("Prefix Information"), + 4 => Some("Redirected Header"), + 5 => Some("MTU"), + _ => None, + } + } +} + +impl From for NdpOptionType { + #[inline] + fn from(val: u8) -> Self { + Self(val) + } +} + +impl From for u8 { + #[inline] + fn from(val: NdpOptionType) -> Self { + val.0 + } +} + +impl core::fmt::Debug for NdpOptionType { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + if let Some(keyword) = self.keyword_str() { + write!(f, "{} ({})", self.0, keyword) + } else { + write!(f, "{}", self.0) + } + } +} diff --git a/etherparse/src/transport/icmpv6/ndp_options_iterator.rs b/etherparse/src/transport/icmpv6/ndp_options_iterator.rs new file mode 100644 index 00000000..b6a3fb03 --- /dev/null +++ b/etherparse/src/transport/icmpv6/ndp_options_iterator.rs @@ -0,0 +1,246 @@ +use crate::icmpv6::{ + MtuOptionSlice, NdpOptionHeader, NdpOptionReadError, NdpOptionSlice, NdpOptionType, + PrefixInformationOptionSlice, RedirectedHeaderOptionSlice, SourceLinkLayerAddressOptionSlice, + TargetLinkLayerAddressOptionSlice, UnknownNdpOptionSlice, +}; + +/// Allows iterating over Neighbor Discovery options in an ICMPv6 payload. +#[derive(Clone, Eq, PartialEq)] +pub struct NdpOptionsIterator<'a> { + pub(crate) options: &'a [u8], +} + +impl<'a> NdpOptionsIterator<'a> { + /// Creates an iterator over Neighbor Discovery options in serialized form. + pub fn from_slice(options: &'a [u8]) -> NdpOptionsIterator<'a> { + NdpOptionsIterator { options } + } + + /// Returns the bytes not yet processed by the iterator. + pub fn rest(&self) -> &'a [u8] { + self.options + } + + fn parse_next_option( + &mut self, + ) -> Result, NdpOptionReadError> { + use NdpOptionReadError::*; + + let (header, _) = NdpOptionHeader::from_slice(self.options)?; + let option_id = header.option_type; + let length_units = header.length_units; + if 0 == length_units { + return Err(ZeroLength { option_id }); + } + + let option_len = header.byte_len(); + let (option, rest) = self.options.split_at_checked(option_len) + .ok_or_else(|| UnexpectedEndOfSlice { + option_id, + expected_size: option_len, + actual_size: self.options.len(), + })?; + + let parsed = match option_id { + NdpOptionType::SOURCE_LINK_LAYER_ADDRESS => { + SourceLinkLayerAddressOptionSlice::from_slice(option) + .map(NdpOptionSlice::SourceLinkLayerAddress) + } + NdpOptionType::TARGET_LINK_LAYER_ADDRESS => { + TargetLinkLayerAddressOptionSlice::from_slice(option) + .map(NdpOptionSlice::TargetLinkLayerAddress) + } + NdpOptionType::PREFIX_INFORMATION => { + PrefixInformationOptionSlice::from_slice(option) + .map(NdpOptionSlice::PrefixInformation) + } + NdpOptionType::REDIRECTED_HEADER => RedirectedHeaderOptionSlice::from_slice(option) + .map(NdpOptionSlice::RedirectedHeader), + NdpOptionType::MTU => { + MtuOptionSlice::from_slice(option).map(NdpOptionSlice::Mtu) + } + _ => UnknownNdpOptionSlice::from_slice(option).map(NdpOptionSlice::Unknown), + }?; + + self.options = rest; + + Ok(parsed) + } +} + +impl<'a> Iterator for NdpOptionsIterator<'a> { + type Item = Result, NdpOptionReadError>; + + fn next(&mut self) -> Option { + if self.options.is_empty() { + return None; + } + + let parse_result = self.parse_next_option(); + + if parse_result.is_err() { + // We don't try to parse any more options after encountering an invalid option. + self.options = &[]; + } + Some(parse_result) + } +} + +impl core::fmt::Debug for NdpOptionsIterator<'_> { + fn fmt(&self, fmt: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { + let mut list = fmt.debug_list(); + for item in self.clone() { + match item { + Ok(value) => { + list.entry(&value); + } + Err(err) => { + list.entry(&Result::<(), NdpOptionReadError>::Err(err.clone())); + } + } + } + list.finish() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::icmpv6::PrefixInformation; + + + #[test] + fn from_slice_and_rest() { + let buffer = [1, 1, 1, 2, 3, 4, 5, 6]; + let iterator = NdpOptionsIterator::from_slice(&buffer); + assert_eq!(iterator.rest(), &buffer[..]); + } + + #[test] + fn next() { + let source_link_layer_address = [1, 1, 1, 2, 3, 4, 5, 6]; + let target_link_layer_address = [2, 1, 1, 2, 3, 4, 5, 6]; + let redirected_header = [4, 1, 0, 0, 0, 0, 0, 0]; + let mtu = [5, 1, 0, 0, 0, 0, 5, 220]; + let unknown = [250, 1, 1, 2, 3, 4, 5, 6]; + + let prefix = PrefixInformation { + prefix_length: 64, + on_link: true, + autonomous_address_configuration: true, + valid_lifetime: 1, + preferred_lifetime: 2, + prefix: [3; 16], + }; + let prefix_bytes = prefix.to_bytes(); + + let mut options = alloc::vec::Vec::new(); + options.extend(source_link_layer_address); + options.extend(target_link_layer_address); + options.extend(redirected_header); + options.extend(mtu); + options.extend(unknown); + options.extend(prefix_bytes); + + let mut iter = NdpOptionsIterator::from_slice(&options); + assert_eq!( + Some(Ok(NdpOptionSlice::SourceLinkLayerAddress( + SourceLinkLayerAddressOptionSlice::from_slice(&source_link_layer_address).unwrap() + ))), + iter.next() + ); + assert_eq!( + Some(Ok(NdpOptionSlice::TargetLinkLayerAddress( + TargetLinkLayerAddressOptionSlice::from_slice(&target_link_layer_address).unwrap() + ))), + iter.next() + ); + assert_eq!( + Some(Ok(NdpOptionSlice::RedirectedHeader( + RedirectedHeaderOptionSlice::from_slice(&redirected_header).unwrap() + ))), + iter.next() + ); + assert_eq!( + Some(Ok(NdpOptionSlice::Mtu( + MtuOptionSlice::from_slice(&mtu).unwrap() + ))), + iter.next() + ); + assert_eq!( + Some(Ok(NdpOptionSlice::Unknown( + UnknownNdpOptionSlice::from_slice(&unknown).unwrap() + ))), + iter.next() + ); + assert_eq!( + Some(Ok(NdpOptionSlice::PrefixInformation( + PrefixInformationOptionSlice::from_slice(&prefix_bytes).unwrap() + ))), + iter.next() + ); + assert_eq!(None, iter.next()); + assert_eq!(0, iter.rest().len()); + } + + #[test] + fn next_errors() { + assert_eq!( + Some(Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType::SOURCE_LINK_LAYER_ADDRESS, + expected_size: NdpOptionHeader::LEN, + actual_size: 1, + })), + NdpOptionsIterator::from_slice(&[1]).next() + ); + assert_eq!( + Some(Err(NdpOptionReadError::ZeroLength { + option_id: NdpOptionType::SOURCE_LINK_LAYER_ADDRESS + })), + NdpOptionsIterator::from_slice(&[1, 0]).next() + ); + assert_eq!( + Some(Err(NdpOptionReadError::UnexpectedEndOfSlice { + option_id: NdpOptionType::SOURCE_LINK_LAYER_ADDRESS, + expected_size: 8, + actual_size: 6, + })), + NdpOptionsIterator::from_slice(&[1, 1, 1, 2, 3, 4]).next() + ); + assert_eq!( + Some(Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType::PREFIX_INFORMATION, + expected_size: 32, + actual_size: 8, + })), + NdpOptionsIterator::from_slice(&[3, 1, 0, 0, 0, 0, 0, 0]).next() + ); + assert_eq!( + Some(Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType::MTU, + expected_size: 8, + actual_size: 16, + })), + NdpOptionsIterator::from_slice(&[5, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + .next() + ); + } + + #[test] + fn next_stops_after_error() { + let mut iter = NdpOptionsIterator::from_slice(&[ + 3, 1, 0, 0, 0, 0, 0, 0, // Prefix Information with invalid length units + 5, 1, 0, 0, 0, 0, 5, 220, // Valid MTU option that must not be reached + ]); + assert_eq!( + Some(Err(NdpOptionReadError::UnexpectedSize { + option_id: NdpOptionType::PREFIX_INFORMATION, + expected_size: 32, + actual_size: 8, + })), + iter.next() + ); + assert_eq!(None, iter.next()); + assert_eq!(0, iter.rest().len()); + } +} diff --git a/etherparse/src/transport/icmpv6_slice.rs b/etherparse/src/transport/icmpv6_slice.rs index 7be4a164..2564f719 100644 --- a/etherparse/src/transport/icmpv6_slice.rs +++ b/etherparse/src/transport/icmpv6_slice.rs @@ -99,6 +99,18 @@ impl<'a> Icmpv6Slice<'a> { return EchoReply(IcmpEchoHeader::from_bytes(self.bytes5to8())); } } + TYPE_ROUTER_SOLICITATION => { + if 0 == self.code_u8() { + return RouterSolicitation; + } + } + TYPE_ROUTER_ADVERTISEMENT => { + if 0 == self.code_u8() { + return RouterAdvertisement(RouterAdvertisementHeader::from_bytes( + self.bytes5to8(), + )); + } + } TYPE_NEIGHBOR_SOLICITATION => { if 0 == self.code_u8() { return NeighborSolicitation; @@ -111,6 +123,11 @@ impl<'a> Icmpv6Slice<'a> { )); } } + TYPE_REDIRECT_MESSAGE => { + if 0 == self.code_u8() { + return Redirect; + } + } _ => {} } Unknown { @@ -200,6 +217,12 @@ impl<'a> Icmpv6Slice<'a> { // at least the length of Icmpv6Header::MIN_LEN(8). unsafe { core::slice::from_raw_parts(self.slice.as_ptr().add(8), self.slice.len() - 8) } } + + /// Returns the structured payload of the ICMPv6 message if this type is supported. + #[inline] + pub fn payload_slice(&self) -> Result, err::LenError> { + icmpv6::Icmpv6PayloadSlice::from_type_u8(self.type_u8(), self.code_u8(), self.payload()) + } } #[cfg(test)] @@ -643,6 +666,135 @@ mod test { } } + #[test] + fn payload_slice() { + use crate::icmpv6::*; + + { + let packet = [ + TYPE_ROUTER_SOLICITATION, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 2, + 3, + 4, + 5, + 6, + ]; + let slice = Icmpv6Slice::from_slice(&packet).unwrap(); + assert_eq!(slice.icmp_type(), RouterSolicitation); + assert_eq!( + slice.payload_slice().unwrap(), + Icmpv6PayloadSlice::RouterSolicitation( + RouterSolicitationPayloadSlice::from_slice(&packet[8..]).unwrap() + ) + ); + } + + { + let packet = [ + TYPE_ROUTER_ADVERTISEMENT, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 2, + 5, + 1, + 0, + 0, + 0, + 0, + 5, + 220, + ]; + let slice = Icmpv6Slice::from_slice(&packet).unwrap(); + assert_eq!( + slice.icmp_type(), + RouterAdvertisement(RouterAdvertisementHeader { + cur_hop_limit: 0, + managed_address_config: false, + other_config: false, + router_lifetime: 0, + }) + ); + assert_eq!( + slice.payload_slice().unwrap(), + Icmpv6PayloadSlice::RouterAdvertisement( + RouterAdvertisementPayloadSlice::from_slice(&packet[8..]).unwrap() + ) + ); + } + + { + let mut packet = [0u8; 24]; + packet[0] = TYPE_NEIGHBOR_SOLICITATION; + packet[8..24].copy_from_slice(&[1; 16]); + let slice = Icmpv6Slice::from_slice(&packet).unwrap(); + assert_eq!( + slice.payload_slice().unwrap(), + Icmpv6PayloadSlice::NeighborSolicitation( + NeighborSolicitationPayloadSlice::from_slice(&packet[8..]).unwrap() + ) + ); + } + + { + let mut packet = [0u8; 24]; + packet[0] = TYPE_NEIGHBOR_ADVERTISEMENT; + packet[8..24].copy_from_slice(&[2; 16]); + let slice = Icmpv6Slice::from_slice(&packet).unwrap(); + assert_eq!( + slice.payload_slice().unwrap(), + Icmpv6PayloadSlice::NeighborAdvertisement( + NeighborAdvertisementPayloadSlice::from_slice(&packet[8..]).unwrap() + ) + ); + } + + { + let mut packet = [0u8; 40]; + packet[0] = TYPE_REDIRECT_MESSAGE; + packet[8..24].copy_from_slice(&[3; 16]); + packet[24..40].copy_from_slice(&[4; 16]); + let slice = Icmpv6Slice::from_slice(&packet).unwrap(); + assert_eq!(slice.icmp_type(), Redirect); + assert_eq!( + slice.payload_slice().unwrap(), + Icmpv6PayloadSlice::Redirect( + RedirectPayloadSlice::from_slice(&packet[8..]).unwrap() + ) + ); + } + + { + let packet = [TYPE_ECHO_REQUEST, 0, 0, 0, 0, 0, 0, 0, 9, 9]; + let slice = Icmpv6Slice::from_slice(&packet).unwrap(); + assert_eq!( + slice.payload_slice().unwrap(), + Icmpv6PayloadSlice::Raw(&packet[8..]) + ); + } + } + #[test] fn debug() { let data = [0u8; 8]; diff --git a/etherparse/src/transport/icmpv6_type.rs b/etherparse/src/transport/icmpv6_type.rs index e4490437..4317ae81 100644 --- a/etherparse/src/transport/icmpv6_type.rs +++ b/etherparse/src/transport/icmpv6_type.rs @@ -687,6 +687,31 @@ impl Icmpv6Type { }) } + /// Decode a structured payload based on this ICMPv6 type. + #[inline] + pub fn payload_slice<'a>( + &self, + payload: &'a [u8], + ) -> Result, err::LenError> { + icmpv6::Icmpv6PayloadSlice::from_slice(self, payload) + } + + /// Convert a structured payload slice into an owned payload if supported. + /// + /// The returned [`icmpv6::Icmpv6Payload`] variants contain only fixed-part + /// message fields (for example `target_address`) and intentionally exclude + /// NDP options. For payload types with variable trailing data, the second + /// tuple element contains the remaining bytes that must be parsed + /// separately (for example via [`icmpv6::Icmpv6PayloadSlice`] option + /// accessors/iterators). + #[inline] + pub fn payload_from_slice<'a>( + &self, + payload: &'a [u8], + ) -> Result, err::LenError> { + self.payload_slice(payload).map(|value| value.to_payload()) + } + /// Serialized length of the header in bytes/octets. /// /// Note that this size is not the size of the entire @@ -1105,6 +1130,65 @@ mod test { } } + #[test] + fn payload_slice() { + use crate::icmpv6::*; + + assert_eq!( + Icmpv6Type::RouterSolicitation + .payload_slice(&[1, 1, 1, 2, 3, 4, 5, 6]) + .unwrap(), + Icmpv6PayloadSlice::RouterSolicitation( + RouterSolicitationPayloadSlice::from_slice(&[1, 1, 1, 2, 3, 4, 5, 6]).unwrap() + ) + ); + + assert_eq!( + Icmpv6Type::RouterAdvertisement(RouterAdvertisementHeader { + cur_hop_limit: 0, + managed_address_config: false, + other_config: false, + router_lifetime: 0, + }) + .payload_slice(&[0, 0, 0, 1, 0, 0, 0, 2]) + .unwrap(), + Icmpv6PayloadSlice::RouterAdvertisement( + RouterAdvertisementPayloadSlice::from_slice(&[0, 0, 0, 1, 0, 0, 0, 2]).unwrap() + ) + ); + + assert_eq!( + Icmpv6Type::NeighborSolicitation + .payload_from_slice(&[1; 16]) + .unwrap(), + Some(( + Icmpv6Payload::NeighborSolicitation(NeighborSolicitationPayload { + target_address: [1; 16].into() + }), + &[][..] + )) + ); + + assert_eq!( + Icmpv6Type::NeighborSolicitation + .payload_from_slice(&[1; 16 + 8]) + .unwrap(), + Some(( + Icmpv6Payload::NeighborSolicitation(NeighborSolicitationPayload { + target_address: [1; 16].into() + }), + &[1; 8][..] + )) + ); + + assert_eq!( + Icmpv6Type::EchoRequest(IcmpEchoHeader { id: 1, seq: 2 }) + .payload_from_slice(&[1, 2, 3, 4]) + .unwrap(), + None + ); + } + #[test] fn debug() { assert_eq!(