use core::fmt;
use core::net::Ipv6Addr;
use crate::error::{CrafterError, Result};
use crate::mac::MacAddr;
use crate::protocols::dns::{decode_dns_name_typed, DnsName};
pub const NDP_OPT_SOURCE_LINK_LAYER_ADDR: u8 = 1;
pub const NDP_OPT_TARGET_LINK_LAYER_ADDR: u8 = 2;
pub const NDP_OPT_PREFIX_INFORMATION: u8 = 3;
pub const NDP_OPT_REDIRECTED_HEADER: u8 = 4;
pub const NDP_OPT_MTU: u8 = 5;
pub const NDP_OPT_NONCE: u8 = 14;
pub const NDP_OPT_ROUTE_INFORMATION: u8 = 24;
pub const NDP_OPT_RDNSS: u8 = 25;
pub const NDP_OPT_RA_FLAGS_EXTENSION: u8 = 26;
pub const NDP_OPT_DNSSL: u8 = 31;
pub const NDP_OPT_CAPTIVE_PORTAL: u8 = 37;
pub const NDP_OPT_PREF64: u8 = 38;
pub const NDP_OPTION_LENGTH_UNIT: usize = 8;
pub const NDP_OPTION_HEADER_LEN: usize = 2;
pub const NDP_LINK_LAYER_ADDR_ETHERNET_LEN: usize = 6;
pub const NDP_PREFIX_FLAG_ON_LINK: u8 = 0x80;
pub const NDP_PREFIX_FLAG_AUTONOMOUS: u8 = 0x40;
pub const NDP_PREFIX_FLAGS_RESERVED: u8 = 0x3f;
pub const NDP_PREFIX_LIFETIME_INFINITY: u32 = 0xffff_ffff;
pub const NDP_PREFIX_INFORMATION_LEN: usize = 32;
pub const NDP_PREFIX_INFORMATION_UNITS: u8 = 4;
pub const NDP_MTU_OPTION_LEN: usize = 8;
pub const NDP_MTU_OPTION_UNITS: u8 = 1;
pub const NDP_REDIRECTED_HEADER_RESERVED_LEN: usize = 6;
pub const NDP_PRF_MASK: u8 = 0x18;
pub const NDP_PRF_SHIFT: u8 = 3;
pub const NDP_ROUTE_INFORMATION_LEN_NO_PREFIX: usize = 8;
pub const NDP_ROUTE_INFORMATION_LEN_HALF_PREFIX: usize = 16;
pub const NDP_ROUTE_INFORMATION_LEN_FULL_PREFIX: usize = 24;
pub const NDP_ROUTE_LIFETIME_INFINITY: u32 = 0xffff_ffff;
pub const NDP_DNS_RESERVED_LEN: usize = 2;
pub const NDP_RDNSS_ADDRESS_LEN: usize = 16;
pub const NDP_DNS_LIFETIME_INFINITY: u32 = 0xffff_ffff;
pub const fn ndp_rdnss_length_units(addresses: usize) -> usize {
1 + 2 * addresses
}
pub const NDP_RA_FLAGS_EXTENSION_BITS_LEN: usize = 6;
pub const NDP_RA_FLAGS_EXTENSION_LEN: usize = 8;
pub const NDP_RA_FLAGS_EXTENSION_UNITS: u8 = 1;
pub const NDP_NONCE_MIN_LEN: usize = 6;
pub const NDP_PREF64_LEN: usize = 16;
pub const NDP_PREF64_UNITS: u8 = 2;
pub const NDP_PREF64_PREFIX_LEN: usize = 12;
pub const NDP_PREF64_SCALED_LIFETIME_SHIFT: u8 = 3;
pub const NDP_PREF64_PLC_MASK: u16 = 0x0007;
pub const NDP_PREF64_SCALED_LIFETIME_MAX: u16 = 0x1fff;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Prf {
High,
#[default]
Medium,
Low,
Reserved,
}
impl Prf {
pub const fn to_bits(self) -> u8 {
match self {
Prf::High => 0b01,
Prf::Medium => 0b00,
Prf::Low => 0b11,
Prf::Reserved => 0b10,
}
}
pub const fn from_bits(bits: u8) -> Self {
match bits & 0b11 {
0b01 => Prf::High,
0b00 => Prf::Medium,
0b11 => Prf::Low,
_ => Prf::Reserved,
}
}
pub const fn to_flag_bits(self) -> u8 {
(self.to_bits() << NDP_PRF_SHIFT) & NDP_PRF_MASK
}
pub const fn from_flag_byte(flags: u8) -> Self {
Self::from_bits((flags & NDP_PRF_MASK) >> NDP_PRF_SHIFT)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Pref64Plc {
PrefixLength(u8),
Reserved(u8),
}
impl Pref64Plc {
pub const fn prefix_length_bits(self) -> Option<u8> {
match self {
Pref64Plc::PrefixLength(bits) => Some(bits),
Pref64Plc::Reserved(_) => None,
}
}
pub const fn to_plc(self) -> Option<u8> {
match self {
Pref64Plc::PrefixLength(96) => Some(0),
Pref64Plc::PrefixLength(64) => Some(1),
Pref64Plc::PrefixLength(56) => Some(2),
Pref64Plc::PrefixLength(48) => Some(3),
Pref64Plc::PrefixLength(40) => Some(4),
Pref64Plc::PrefixLength(32) => Some(5),
Pref64Plc::PrefixLength(_) => None,
Pref64Plc::Reserved(code) => Some(code & 0x07),
}
}
pub const fn from_plc(code: u8) -> Self {
match code & 0x07 {
0 => Pref64Plc::PrefixLength(96),
1 => Pref64Plc::PrefixLength(64),
2 => Pref64Plc::PrefixLength(56),
3 => Pref64Plc::PrefixLength(48),
4 => Pref64Plc::PrefixLength(40),
5 => Pref64Plc::PrefixLength(32),
other => Pref64Plc::Reserved(other),
}
}
pub const fn from_prefix_length_bits(bits: u8) -> Self {
Pref64Plc::PrefixLength(bits)
}
}
pub const fn ndp_option_type_name(ty: u8) -> Option<&'static str> {
match ty {
NDP_OPT_SOURCE_LINK_LAYER_ADDR => Some("Source Link-Layer Address"),
NDP_OPT_TARGET_LINK_LAYER_ADDR => Some("Target Link-Layer Address"),
NDP_OPT_PREFIX_INFORMATION => Some("Prefix Information"),
NDP_OPT_REDIRECTED_HEADER => Some("Redirected Header"),
NDP_OPT_MTU => Some("MTU"),
NDP_OPT_NONCE => Some("Nonce"),
NDP_OPT_ROUTE_INFORMATION => Some("Route Information"),
NDP_OPT_RDNSS => Some("Recursive DNS Server"),
NDP_OPT_RA_FLAGS_EXTENSION => Some("RA Flags Extension"),
NDP_OPT_DNSSL => Some("DNS Search List"),
NDP_OPT_CAPTIVE_PORTAL => Some("Captive Portal"),
NDP_OPT_PREF64 => Some("PREF64"),
_ => None,
}
}
pub const fn ndp_option_type_is_known(ty: u8) -> bool {
ndp_option_type_name(ty).is_some()
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum NdpOption {
Generic {
ty: u8,
value: Vec<u8>,
length: Option<u8>,
},
Unknown {
ty: u8,
bytes: Vec<u8>,
length: Option<u8>,
},
}
impl NdpOption {
pub fn generic(ty: u8, value: impl Into<Vec<u8>>) -> Self {
Self::Generic {
ty,
value: value.into(),
length: None,
}
}
pub fn unknown(ty: u8, bytes: impl Into<Vec<u8>>) -> Self {
Self::Unknown {
ty,
bytes: bytes.into(),
length: None,
}
}
pub fn source_link_layer_address(mac: MacAddr) -> Self {
Self::generic(NDP_OPT_SOURCE_LINK_LAYER_ADDR, mac.octets().to_vec())
}
pub fn target_link_layer_address(mac: MacAddr) -> Self {
Self::generic(NDP_OPT_TARGET_LINK_LAYER_ADDR, mac.octets().to_vec())
}
pub fn link_layer_address(&self) -> Option<MacAddr> {
match self.option_type() {
NDP_OPT_SOURCE_LINK_LAYER_ADDR | NDP_OPT_TARGET_LINK_LAYER_ADDR => {
let value = self.value();
if value.len() < NDP_LINK_LAYER_ADDR_ETHERNET_LEN {
return None;
}
let mut octets = [0u8; NDP_LINK_LAYER_ADDR_ETHERNET_LEN];
octets.copy_from_slice(&value[..NDP_LINK_LAYER_ADDR_ETHERNET_LEN]);
Some(MacAddr::new(octets))
}
_ => None,
}
}
pub fn prefix_information(
prefix: Ipv6Addr,
prefix_len: u8,
on_link: bool,
autonomous: bool,
valid_lifetime: u32,
preferred_lifetime: u32,
) -> Self {
let mut flags = 0u8;
if on_link {
flags |= NDP_PREFIX_FLAG_ON_LINK;
}
if autonomous {
flags |= NDP_PREFIX_FLAG_AUTONOMOUS;
}
Self::prefix_information_raw(
prefix,
prefix_len,
flags,
valid_lifetime,
preferred_lifetime,
0,
)
}
pub fn prefix_information_raw(
prefix: Ipv6Addr,
prefix_len: u8,
flags: u8,
valid_lifetime: u32,
preferred_lifetime: u32,
reserved2: u32,
) -> Self {
let mut value = Vec::with_capacity(NDP_PREFIX_INFORMATION_LEN - NDP_OPTION_HEADER_LEN);
value.push(prefix_len);
value.push(flags);
value.extend_from_slice(&valid_lifetime.to_be_bytes());
value.extend_from_slice(&preferred_lifetime.to_be_bytes());
value.extend_from_slice(&reserved2.to_be_bytes());
value.extend_from_slice(&prefix.octets());
Self::generic(NDP_OPT_PREFIX_INFORMATION, value)
}
fn prefix_flags(&self) -> Option<u8> {
if self.option_type() != NDP_OPT_PREFIX_INFORMATION {
return None;
}
self.value().get(1).copied()
}
pub fn prefix_length(&self) -> Option<u8> {
if self.option_type() != NDP_OPT_PREFIX_INFORMATION {
return None;
}
self.value().first().copied()
}
pub fn prefix_on_link(&self) -> Option<bool> {
self.prefix_flags()
.map(|flags| flags & NDP_PREFIX_FLAG_ON_LINK != 0)
}
pub fn prefix_autonomous(&self) -> Option<bool> {
self.prefix_flags()
.map(|flags| flags & NDP_PREFIX_FLAG_AUTONOMOUS != 0)
}
pub fn prefix_reserved1(&self) -> Option<u8> {
self.prefix_flags()
.map(|flags| flags & NDP_PREFIX_FLAGS_RESERVED)
}
pub fn prefix_valid_lifetime(&self) -> Option<u32> {
self.prefix_u32_at(2)
}
pub fn prefix_preferred_lifetime(&self) -> Option<u32> {
self.prefix_u32_at(6)
}
pub fn prefix_reserved2(&self) -> Option<u32> {
self.prefix_u32_at(10)
}
pub fn prefix(&self) -> Option<Ipv6Addr> {
if self.option_type() != NDP_OPT_PREFIX_INFORMATION {
return None;
}
let value = self.value();
let prefix = value.get(14..30)?;
let mut octets = [0u8; 16];
octets.copy_from_slice(prefix);
Some(Ipv6Addr::from(octets))
}
fn prefix_u32_at(&self, at: usize) -> Option<u32> {
if self.option_type() != NDP_OPT_PREFIX_INFORMATION {
return None;
}
let value = self.value();
let word = value.get(at..at + 4)?;
Some(u32::from_be_bytes([word[0], word[1], word[2], word[3]]))
}
pub fn mtu(mtu: u32) -> Self {
let mut value = Vec::with_capacity(NDP_MTU_OPTION_LEN - NDP_OPTION_HEADER_LEN);
value.extend_from_slice(&[0u8, 0u8]);
value.extend_from_slice(&mtu.to_be_bytes());
Self::generic(NDP_OPT_MTU, value)
}
pub fn mtu_value(&self) -> Option<u32> {
if self.option_type() != NDP_OPT_MTU {
return None;
}
let value = self.value();
let word = value.get(2..6)?;
Some(u32::from_be_bytes([word[0], word[1], word[2], word[3]]))
}
pub fn redirected_header(original_packet: &[u8]) -> Self {
let mut value =
Vec::with_capacity(NDP_REDIRECTED_HEADER_RESERVED_LEN + original_packet.len());
value.extend_from_slice(&[0u8; NDP_REDIRECTED_HEADER_RESERVED_LEN]);
value.extend_from_slice(original_packet);
Self::generic(NDP_OPT_REDIRECTED_HEADER, value)
}
pub fn redirected_header_data(&self) -> Option<&[u8]> {
if self.option_type() != NDP_OPT_REDIRECTED_HEADER {
return None;
}
self.value().get(NDP_REDIRECTED_HEADER_RESERVED_LEN..)
}
pub fn route_information(
prefix: Ipv6Addr,
prefix_len: u8,
preference: Prf,
route_lifetime: u32,
) -> Self {
let prefix_octets = route_prefix_octets_for_len(prefix_len);
Self::route_information_raw(
prefix,
prefix_len,
preference.to_flag_bits(),
route_lifetime,
prefix_octets,
)
}
pub fn route_information_raw(
prefix: Ipv6Addr,
prefix_len: u8,
flags: u8,
route_lifetime: u32,
prefix_octets: usize,
) -> Self {
let carried = prefix_octets.min(16);
let mut value = Vec::with_capacity(6 + carried);
value.push(prefix_len);
value.push(flags);
value.extend_from_slice(&route_lifetime.to_be_bytes());
value.extend_from_slice(&prefix.octets()[..carried]);
Self::generic(NDP_OPT_ROUTE_INFORMATION, value)
}
fn route_flags(&self) -> Option<u8> {
if self.option_type() != NDP_OPT_ROUTE_INFORMATION {
return None;
}
self.value().get(1).copied()
}
pub fn route_prefix_length(&self) -> Option<u8> {
if self.option_type() != NDP_OPT_ROUTE_INFORMATION {
return None;
}
self.value().first().copied()
}
pub fn route_preference(&self) -> Option<Prf> {
self.route_flags().map(Prf::from_flag_byte)
}
pub fn route_lifetime(&self) -> Option<u32> {
if self.option_type() != NDP_OPT_ROUTE_INFORMATION {
return None;
}
let value = self.value();
let word = value.get(2..6)?;
Some(u32::from_be_bytes([word[0], word[1], word[2], word[3]]))
}
pub fn route_prefix(&self) -> Option<Ipv6Addr> {
if self.option_type() != NDP_OPT_ROUTE_INFORMATION {
return None;
}
let value = self.value();
let carried = value.get(6..)?;
let take = carried.len().min(16);
let mut octets = [0u8; 16];
octets[..take].copy_from_slice(&carried[..take]);
Some(Ipv6Addr::from(octets))
}
pub fn rdnss(lifetime: u32, servers: &[Ipv6Addr]) -> Self {
let mut value =
Vec::with_capacity(NDP_DNS_RESERVED_LEN + 4 + servers.len() * NDP_RDNSS_ADDRESS_LEN);
value.extend_from_slice(&[0u8; NDP_DNS_RESERVED_LEN]);
value.extend_from_slice(&lifetime.to_be_bytes());
for server in servers {
value.extend_from_slice(&server.octets());
}
Self::generic(NDP_OPT_RDNSS, value)
}
pub fn rdnss_lifetime(&self) -> Option<u32> {
if self.option_type() != NDP_OPT_RDNSS {
return None;
}
let word = self
.value()
.get(NDP_DNS_RESERVED_LEN..NDP_DNS_RESERVED_LEN + 4)?;
Some(u32::from_be_bytes([word[0], word[1], word[2], word[3]]))
}
pub fn rdnss_servers(&self) -> Option<Vec<Ipv6Addr>> {
if self.option_type() != NDP_OPT_RDNSS {
return None;
}
let value = self.value();
let addresses = value.get(NDP_DNS_RESERVED_LEN + 4..)?;
let mut servers = Vec::with_capacity(addresses.len() / NDP_RDNSS_ADDRESS_LEN);
for chunk in addresses.chunks_exact(NDP_RDNSS_ADDRESS_LEN) {
let mut octets = [0u8; NDP_RDNSS_ADDRESS_LEN];
octets.copy_from_slice(chunk);
servers.push(Ipv6Addr::from(octets));
}
Some(servers)
}
pub fn dnssl<S: AsRef<str>>(lifetime: u32, domains: &[S]) -> Self {
Self::dnssl_checked(lifetime, domains).unwrap_or_else(|_| {
let mut value = Vec::with_capacity(NDP_DNS_RESERVED_LEN + 4);
value.extend_from_slice(&[0u8; NDP_DNS_RESERVED_LEN]);
value.extend_from_slice(&lifetime.to_be_bytes());
Self::generic(NDP_OPT_DNSSL, value)
})
}
pub fn dnssl_checked<S: AsRef<str>>(lifetime: u32, domains: &[S]) -> Result<Self> {
let mut value = Vec::with_capacity(NDP_DNS_RESERVED_LEN + 4 + domains.len() * 8);
value.extend_from_slice(&[0u8; NDP_DNS_RESERVED_LEN]);
value.extend_from_slice(&lifetime.to_be_bytes());
for domain in domains {
let name = DnsName::parse(domain.as_ref())?;
value.extend_from_slice(&name.encode_uncompressed()?);
}
Ok(Self::generic(NDP_OPT_DNSSL, value))
}
pub fn dnssl_lifetime(&self) -> Option<u32> {
if self.option_type() != NDP_OPT_DNSSL {
return None;
}
let word = self
.value()
.get(NDP_DNS_RESERVED_LEN..NDP_DNS_RESERVED_LEN + 4)?;
Some(u32::from_be_bytes([word[0], word[1], word[2], word[3]]))
}
pub fn dnssl_domains(&self) -> Option<Vec<String>> {
if self.option_type() != NDP_OPT_DNSSL {
return None;
}
let value = self.value();
let names_area = match value.get(NDP_DNS_RESERVED_LEN + 4..) {
Some(area) => area,
None => return Some(Vec::new()),
};
let mut domains = Vec::new();
let mut offset = 0usize;
while offset < names_area.len() {
if names_area[offset] == 0 {
break;
}
match decode_dns_name_typed(names_area, offset) {
Ok((name, used)) if used > 0 => {
domains.push(name.presentation().to_string());
offset += used;
}
_ => break,
}
}
Some(domains)
}
pub fn ra_flags_extension(bits: [u8; NDP_RA_FLAGS_EXTENSION_BITS_LEN]) -> Self {
Self::generic(NDP_OPT_RA_FLAGS_EXTENSION, bits.to_vec())
}
pub fn ra_flags_extension_bits(&self) -> Option<[u8; NDP_RA_FLAGS_EXTENSION_BITS_LEN]> {
if self.option_type() != NDP_OPT_RA_FLAGS_EXTENSION {
return None;
}
let value = self.value();
let bits = value.get(..NDP_RA_FLAGS_EXTENSION_BITS_LEN)?;
let mut octets = [0u8; NDP_RA_FLAGS_EXTENSION_BITS_LEN];
octets.copy_from_slice(bits);
Some(octets)
}
pub fn nonce(nonce: &[u8]) -> Self {
Self::generic(NDP_OPT_NONCE, nonce.to_vec())
}
pub fn nonce_value(&self) -> Option<&[u8]> {
if self.option_type() != NDP_OPT_NONCE {
return None;
}
Some(self.value())
}
pub fn pref64(scaled_lifetime: u16, prefix_length: u8, prefix: Ipv6Addr) -> Result<Self> {
let plc = Pref64Plc::from_prefix_length_bits(prefix_length)
.to_plc()
.ok_or_else(|| {
CrafterError::invalid_field_value(
"ndp.pref64.prefix_length",
"PREF64 prefix length must be one of 96, 64, 56, 48, 40, or 32 bits (RFC 8781 sec 4)",
)
})?;
Ok(Self::pref64_raw(scaled_lifetime, plc, prefix))
}
pub fn pref64_raw(scaled_lifetime: u16, plc: u8, prefix: Ipv6Addr) -> Self {
let word = ((scaled_lifetime & NDP_PREF64_SCALED_LIFETIME_MAX)
<< NDP_PREF64_SCALED_LIFETIME_SHIFT)
| (u16::from(plc) & NDP_PREF64_PLC_MASK);
let mut value = Vec::with_capacity(NDP_PREF64_LEN - NDP_OPTION_HEADER_LEN);
value.extend_from_slice(&word.to_be_bytes());
value.extend_from_slice(&prefix.octets()[..NDP_PREF64_PREFIX_LEN]);
Self::generic(NDP_OPT_PREF64, value)
}
fn pref64_word(&self) -> Option<u16> {
if self.option_type() != NDP_OPT_PREF64 {
return None;
}
let word = self.value().get(0..2)?;
Some(u16::from_be_bytes([word[0], word[1]]))
}
pub fn pref64_scaled_lifetime(&self) -> Option<u16> {
self.pref64_word()
.map(|word| word >> NDP_PREF64_SCALED_LIFETIME_SHIFT)
}
pub fn pref64_plc(&self) -> Option<Pref64Plc> {
self.pref64_word()
.map(|word| Pref64Plc::from_plc((word & NDP_PREF64_PLC_MASK) as u8))
}
pub fn pref64_prefix_length(&self) -> Option<u8> {
self.pref64_plc().and_then(Pref64Plc::prefix_length_bits)
}
pub fn pref64_prefix(&self) -> Option<Ipv6Addr> {
if self.option_type() != NDP_OPT_PREF64 {
return None;
}
let value = self.value();
let prefix = value.get(2..2 + NDP_PREF64_PREFIX_LEN)?;
let mut octets = [0u8; 16];
octets[..NDP_PREF64_PREFIX_LEN].copy_from_slice(prefix);
Some(Ipv6Addr::from(octets))
}
pub fn captive_portal(uri: &str) -> Self {
Self::generic(NDP_OPT_CAPTIVE_PORTAL, uri.as_bytes().to_vec())
}
pub fn captive_portal_uri(&self) -> Option<String> {
if self.option_type() != NDP_OPT_CAPTIVE_PORTAL {
return None;
}
let value = self.value();
let end = value
.iter()
.rposition(|&b| b != 0)
.map_or(0, |last| last + 1);
core::str::from_utf8(&value[..end])
.ok()
.map(|uri| uri.to_string())
}
pub fn length(mut self, length: u8) -> Self {
match &mut self {
Self::Generic { length: l, .. } | Self::Unknown { length: l, .. } => {
*l = Some(length);
}
}
self
}
pub fn clear_length(mut self) -> Self {
match &mut self {
Self::Generic { length: l, .. } | Self::Unknown { length: l, .. } => {
*l = None;
}
}
self
}
pub const fn option_type(&self) -> u8 {
match self {
Self::Generic { ty, .. } | Self::Unknown { ty, .. } => *ty,
}
}
pub fn value(&self) -> &[u8] {
match self {
Self::Generic { value, .. } => value,
Self::Unknown { bytes, .. } => bytes,
}
}
pub const fn explicit_length(&self) -> Option<u8> {
match self {
Self::Generic { length, .. } | Self::Unknown { length, .. } => *length,
}
}
pub const fn is_known(&self) -> bool {
ndp_option_type_is_known(self.option_type())
}
pub fn effective_length(&self) -> Result<u8> {
if let Some(length) = self.explicit_length() {
return Ok(length);
}
auto_fill_length(self.value().len())
}
pub fn encoded_len(&self) -> Result<usize> {
Ok(self.effective_length()? as usize * NDP_OPTION_LENGTH_UNIT)
}
pub fn encode(&self) -> Result<Vec<u8>> {
let mut bytes = Vec::with_capacity(self.encoded_len()?.max(NDP_OPTION_HEADER_LEN));
self.encode_into(&mut bytes)?;
Ok(bytes)
}
pub fn encode_into(&self, out: &mut Vec<u8>) -> Result<()> {
let length = self.effective_length()?;
let ty = self.option_type();
let value = self.value();
let total = length as usize * NDP_OPTION_LENGTH_UNIT;
let start = out.len();
out.push(ty);
out.push(length);
let value_capacity = total.saturating_sub(NDP_OPTION_HEADER_LEN);
if value.len() >= value_capacity {
out.extend_from_slice(&value[..value_capacity]);
} else {
out.extend_from_slice(value);
let target = start + total;
if out.len() < target {
out.resize(target, 0);
}
}
Ok(())
}
pub fn decode_one(bytes: &[u8]) -> Result<(Self, usize)> {
if bytes.len() < NDP_OPTION_HEADER_LEN {
return Err(CrafterError::buffer_too_short(
"ndp.option.header",
NDP_OPTION_HEADER_LEN,
bytes.len(),
));
}
let ty = bytes[0];
let length = bytes[1];
if length == 0 {
return Err(CrafterError::invalid_field_value(
"ndp.option.length",
"NDP option length field must not be zero",
));
}
let total = length as usize * NDP_OPTION_LENGTH_UNIT;
if total > bytes.len() {
return Err(CrafterError::buffer_too_short(
"ndp.option.value",
total,
bytes.len(),
));
}
let value = bytes[NDP_OPTION_HEADER_LEN..total].to_vec();
let option = if ndp_option_type_is_known(ty) {
Self::Generic {
ty,
value,
length: Some(length),
}
} else {
Self::Unknown {
ty,
bytes: value,
length: Some(length),
}
};
Ok((option, total))
}
}
impl fmt::Display for NdpOption {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let ty = self.option_type();
let name = ndp_option_type_name(ty).unwrap_or("Unknown");
let length = match self.explicit_length() {
Some(length) => length.to_string(),
None => "auto".to_string(),
};
write!(
f,
"{name}(type={ty}, len={length}, value_len={})",
self.value().len()
)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct NdpOptions {
options: Vec<NdpOption>,
}
impl NdpOptions {
pub const fn new() -> Self {
Self {
options: Vec::new(),
}
}
pub fn push(mut self, option: NdpOption) -> Self {
self.options.push(option);
self
}
pub fn add(&mut self, option: NdpOption) {
self.options.push(option);
}
pub fn options(&self) -> &[NdpOption] {
&self.options
}
pub fn len(&self) -> usize {
self.options.len()
}
pub fn is_empty(&self) -> bool {
self.options.is_empty()
}
pub fn iter(&self) -> core::slice::Iter<'_, NdpOption> {
self.options.iter()
}
pub fn encoded_len(&self) -> Result<usize> {
let mut total = 0usize;
for option in &self.options {
total += option.encoded_len()?;
}
Ok(total)
}
pub fn encode(&self) -> Result<Vec<u8>> {
let mut bytes = Vec::with_capacity(self.encoded_len()?);
self.encode_into(&mut bytes)?;
Ok(bytes)
}
pub fn encode_into(&self, out: &mut Vec<u8>) -> Result<()> {
for option in &self.options {
option.encode_into(out)?;
}
Ok(())
}
pub fn decode(bytes: &[u8]) -> Result<Self> {
let mut options = Vec::new();
let mut offset = 0usize;
while offset < bytes.len() {
let (option, consumed) = NdpOption::decode_one(&bytes[offset..])?;
offset += consumed;
options.push(option);
}
Ok(Self { options })
}
}
impl FromIterator<NdpOption> for NdpOptions {
fn from_iter<I: IntoIterator<Item = NdpOption>>(iter: I) -> Self {
Self {
options: iter.into_iter().collect(),
}
}
}
impl<'a> IntoIterator for &'a NdpOptions {
type Item = &'a NdpOption;
type IntoIter = core::slice::Iter<'a, NdpOption>;
fn into_iter(self) -> Self::IntoIter {
self.options.iter()
}
}
fn route_prefix_octets_for_len(prefix_len: u8) -> usize {
match prefix_len {
0 => 0,
1..=64 => 8,
_ => 16,
}
}
fn auto_fill_length(value_len: usize) -> Result<u8> {
let total = NDP_OPTION_HEADER_LEN + value_len;
let units = total.div_ceil(NDP_OPTION_LENGTH_UNIT);
let units = units.max(1);
u8::try_from(units).map_err(|_| {
CrafterError::invalid_field_value(
"ndp.option.length",
"NDP option length in 8-octet units does not fit in one byte",
)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ndp_option_codepoints_match_iana_registry() {
assert_eq!(NDP_OPT_SOURCE_LINK_LAYER_ADDR, 1);
assert_eq!(NDP_OPT_TARGET_LINK_LAYER_ADDR, 2);
assert_eq!(NDP_OPT_PREFIX_INFORMATION, 3);
assert_eq!(NDP_OPT_REDIRECTED_HEADER, 4);
assert_eq!(NDP_OPT_MTU, 5);
assert_eq!(NDP_OPT_NONCE, 14);
assert_eq!(NDP_OPT_ROUTE_INFORMATION, 24);
assert_eq!(NDP_OPT_RDNSS, 25);
assert_eq!(NDP_OPT_RA_FLAGS_EXTENSION, 26);
assert_eq!(NDP_OPT_DNSSL, 31);
assert_eq!(NDP_OPT_CAPTIVE_PORTAL, 37);
assert_eq!(NDP_OPT_PREF64, 38);
}
#[test]
fn source_link_layer_address_round_trips() {
let mac = MacAddr::new([0x00, 0x00, 0x5e, 0x00, 0x53, 0x01]);
let opt = NdpOption::source_link_layer_address(mac);
assert_eq!(opt.option_type(), NDP_OPT_SOURCE_LINK_LAYER_ADDR);
assert!(opt.is_known());
assert_eq!(opt.link_layer_address(), Some(mac));
let bytes = opt.encode().unwrap();
assert_eq!(bytes.len(), 8);
assert_eq!(&bytes[0..2], &[NDP_OPT_SOURCE_LINK_LAYER_ADDR, 1]);
assert_eq!(&bytes[2..8], &mac.octets());
let (decoded, consumed) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(consumed, 8);
assert_eq!(decoded.option_type(), NDP_OPT_SOURCE_LINK_LAYER_ADDR);
assert_eq!(decoded.link_layer_address(), Some(mac));
let tlla = NdpOption::target_link_layer_address(mac);
assert_eq!(tlla.option_type(), NDP_OPT_TARGET_LINK_LAYER_ADDR);
assert_eq!(tlla.link_layer_address(), Some(mac));
}
#[test]
fn link_layer_address_accessor_rejects_other_options() {
assert_eq!(
NdpOption::generic(NDP_OPT_MTU, [0, 0, 0, 0, 5, 0xdc]).link_layer_address(),
None
);
assert_eq!(
NdpOption::generic(NDP_OPT_SOURCE_LINK_LAYER_ADDR, [1, 2, 3]).link_layer_address(),
None
);
}
#[test]
fn known_vs_unknown_type_classification() {
assert!(ndp_option_type_is_known(NDP_OPT_MTU));
assert_eq!(ndp_option_type_name(NDP_OPT_MTU), Some("MTU"));
assert!(!ndp_option_type_is_known(99));
assert_eq!(ndp_option_type_name(99), None);
}
#[test]
fn ordered_known_and_unknown_round_trip() {
let mtu = NdpOption::generic(NDP_OPT_MTU, [0x00, 0x00, 0x00, 0x00, 0x05, 0xdc]);
let unknown = NdpOption::unknown(0x99, [0xde, 0xad, 0xbe, 0xef, 0x01, 0x02]);
let options = NdpOptions::new().push(mtu.clone()).push(unknown.clone());
let encoded = options.encode().unwrap();
assert_eq!(encoded.len(), 16);
assert_eq!(&encoded[0..2], &[NDP_OPT_MTU, 1]);
assert_eq!(&encoded[8..10], &[0x99, 1]);
let decoded = NdpOptions::decode(&encoded).unwrap();
assert_eq!(decoded.len(), 2);
assert_eq!(decoded.options()[0].option_type(), NDP_OPT_MTU);
assert!(decoded.options()[0].is_known());
assert!(matches!(decoded.options()[0], NdpOption::Generic { .. }));
assert_eq!(decoded.options()[1].option_type(), 0x99);
assert!(!decoded.options()[1].is_known());
match &decoded.options()[1] {
NdpOption::Unknown { bytes, .. } => {
assert_eq!(bytes, &[0xde, 0xad, 0xbe, 0xef, 0x01, 0x02]);
}
other => panic!("expected Unknown, got {other:?}"),
}
assert_eq!(decoded.encode().unwrap(), encoded);
}
#[test]
fn length_auto_fills_to_eight_octet_boundary() {
let exact = NdpOption::generic(NDP_OPT_SOURCE_LINK_LAYER_ADDR, [1, 2, 3, 4, 5, 6]);
assert_eq!(exact.effective_length().unwrap(), 1);
let exact_bytes = exact.encode().unwrap();
assert_eq!(exact_bytes.len(), 8);
assert_eq!(exact_bytes[1], 1);
assert_eq!(&exact_bytes[2..8], &[1, 2, 3, 4, 5, 6]);
let padded = NdpOption::generic(NDP_OPT_PREFIX_INFORMATION, [9; 7]);
assert_eq!(padded.effective_length().unwrap(), 2);
let padded_bytes = padded.encode().unwrap();
assert_eq!(padded_bytes.len(), 16);
assert_eq!(padded_bytes[1], 2);
assert_eq!(&padded_bytes[2..9], &[9; 7]);
assert_eq!(&padded_bytes[9..16], &[0; 7]);
let empty = NdpOption::generic(NDP_OPT_NONCE, []);
assert_eq!(empty.effective_length().unwrap(), 1);
assert_eq!(
empty.encode().unwrap(),
[NDP_OPT_NONCE, 1, 0, 0, 0, 0, 0, 0]
);
}
#[test]
fn explicit_wrong_length_is_preserved() {
let wrong = NdpOption::generic(NDP_OPT_MTU, [0, 0, 0, 0, 5, 0xdc]).length(4);
assert_eq!(wrong.explicit_length(), Some(4));
assert_eq!(wrong.effective_length().unwrap(), 4);
let bytes = wrong.encode().unwrap();
assert_eq!(bytes[1], 4, "pinned length survives untouched");
assert_eq!(bytes.len(), 32, "pinned length drives the encoded size");
assert_eq!(&bytes[2..8], &[0, 0, 0, 0, 5, 0xdc]);
assert_eq!(&bytes[8..32], &[0; 24]);
let tiny = NdpOption::unknown(0x99, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).length(1);
let tiny_bytes = tiny.encode().unwrap();
assert_eq!(tiny_bytes.len(), 8);
assert_eq!(tiny_bytes[1], 1);
assert_eq!(&tiny_bytes[2..8], &[1, 2, 3, 4, 5, 6]);
let restored = wrong.clear_length();
assert_eq!(restored.explicit_length(), None);
assert_eq!(restored.effective_length().unwrap(), 1);
}
#[test]
fn zero_length_is_a_structured_error() {
let bytes = [NDP_OPT_MTU, 0, 0, 0, 0, 0, 0, 0];
let err = NdpOption::decode_one(&bytes).unwrap_err();
assert_eq!(
err,
CrafterError::invalid_field_value(
"ndp.option.length",
"NDP option length field must not be zero",
)
);
assert_eq!(NdpOptions::decode(&bytes).unwrap_err(), err);
}
#[test]
fn truncated_option_is_a_structured_error() {
let bytes = [NDP_OPT_PREFIX_INFORMATION, 2, 0, 0, 0, 0, 0, 0];
let err = NdpOption::decode_one(&bytes).unwrap_err();
assert_eq!(
err,
CrafterError::buffer_too_short("ndp.option.value", 16, 8)
);
let stub = [NDP_OPT_MTU];
let header_err = NdpOption::decode_one(&stub).unwrap_err();
assert_eq!(
header_err,
CrafterError::buffer_too_short("ndp.option.header", 2, 1)
);
assert_eq!(NdpOptions::decode(&bytes).unwrap_err(), err);
}
#[test]
fn prefix_information_round_trips() {
let prefix = Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0);
let opt = NdpOption::prefix_information(
prefix, 64, true, true, 2_592_000, 604_800, );
assert_eq!(opt.option_type(), NDP_OPT_PREFIX_INFORMATION);
assert!(opt.is_known());
assert_eq!(opt.prefix_length(), Some(64));
assert_eq!(opt.prefix_on_link(), Some(true));
assert_eq!(opt.prefix_autonomous(), Some(true));
assert_eq!(opt.prefix_reserved1(), Some(0));
assert_eq!(opt.prefix_valid_lifetime(), Some(2_592_000));
assert_eq!(opt.prefix_preferred_lifetime(), Some(604_800));
assert_eq!(opt.prefix_reserved2(), Some(0));
assert_eq!(opt.prefix(), Some(prefix));
let bytes = opt.encode().unwrap();
assert_eq!(bytes.len(), NDP_PREFIX_INFORMATION_LEN);
assert_eq!(
&bytes[0..2],
&[NDP_OPT_PREFIX_INFORMATION, NDP_PREFIX_INFORMATION_UNITS]
);
assert_eq!(bytes[2], 64);
assert_eq!(
bytes[3],
NDP_PREFIX_FLAG_ON_LINK | NDP_PREFIX_FLAG_AUTONOMOUS
);
assert_eq!(&bytes[4..8], &2_592_000u32.to_be_bytes());
assert_eq!(&bytes[8..12], &604_800u32.to_be_bytes());
assert_eq!(&bytes[12..16], &[0, 0, 0, 0]);
assert_eq!(&bytes[16..32], &prefix.octets());
let (decoded, consumed) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(consumed, NDP_PREFIX_INFORMATION_LEN);
assert_eq!(decoded.prefix_length(), Some(64));
assert_eq!(decoded.prefix_on_link(), Some(true));
assert_eq!(decoded.prefix_autonomous(), Some(true));
assert_eq!(decoded.prefix_valid_lifetime(), Some(2_592_000));
assert_eq!(decoded.prefix_preferred_lifetime(), Some(604_800));
assert_eq!(decoded.prefix(), Some(prefix));
assert_eq!(decoded.encode().unwrap(), bytes);
}
#[test]
fn prefix_information_flag_independence() {
let prefix = Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0);
for (on_link, autonomous) in [(false, false), (true, false), (false, true), (true, true)] {
let opt = NdpOption::prefix_information(prefix, 64, on_link, autonomous, 0, 0);
let bytes = opt.encode().unwrap();
let (decoded, _) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(
decoded.prefix_on_link(),
Some(on_link),
"L read independently (L={on_link} A={autonomous})"
);
assert_eq!(
decoded.prefix_autonomous(),
Some(autonomous),
"A read independently (L={on_link} A={autonomous})"
);
let mut expected = 0u8;
if on_link {
expected |= NDP_PREFIX_FLAG_ON_LINK;
}
if autonomous {
expected |= NDP_PREFIX_FLAG_AUTONOMOUS;
}
assert_eq!(bytes[3], expected);
assert_eq!(decoded.prefix_reserved1(), Some(0));
}
}
#[test]
fn prefix_information_reserved_and_infinity_preserved() {
let prefix = Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0);
let flags =
NDP_PREFIX_FLAG_ON_LINK | NDP_PREFIX_FLAG_AUTONOMOUS | NDP_PREFIX_FLAGS_RESERVED;
let opt = NdpOption::prefix_information_raw(
prefix,
128,
flags,
NDP_PREFIX_LIFETIME_INFINITY,
NDP_PREFIX_LIFETIME_INFINITY,
0xdead_beef,
);
let bytes = opt.encode().unwrap();
let (decoded, _) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(decoded.prefix_on_link(), Some(true));
assert_eq!(decoded.prefix_autonomous(), Some(true));
assert_eq!(decoded.prefix_reserved1(), Some(NDP_PREFIX_FLAGS_RESERVED));
assert_eq!(
decoded.prefix_valid_lifetime(),
Some(NDP_PREFIX_LIFETIME_INFINITY)
);
assert_eq!(
decoded.prefix_preferred_lifetime(),
Some(NDP_PREFIX_LIFETIME_INFINITY)
);
assert_eq!(decoded.prefix_reserved2(), Some(0xdead_beef));
assert_eq!(decoded.prefix(), Some(prefix));
}
#[test]
fn prefix_accessors_reject_other_options() {
let mtu = NdpOption::mtu(1500);
assert_eq!(mtu.prefix_length(), None);
assert_eq!(mtu.prefix_on_link(), None);
assert_eq!(mtu.prefix_autonomous(), None);
assert_eq!(mtu.prefix_valid_lifetime(), None);
assert_eq!(mtu.prefix(), None);
}
#[test]
fn mtu_option_round_trips() {
let opt = NdpOption::mtu(1500);
assert_eq!(opt.option_type(), NDP_OPT_MTU);
assert!(opt.is_known());
assert_eq!(opt.mtu_value(), Some(1500));
let bytes = opt.encode().unwrap();
assert_eq!(bytes.len(), NDP_MTU_OPTION_LEN);
assert_eq!(&bytes[0..2], &[NDP_OPT_MTU, NDP_MTU_OPTION_UNITS]);
assert_eq!(&bytes[2..4], &[0, 0]);
assert_eq!(&bytes[4..8], &1500u32.to_be_bytes());
let (decoded, consumed) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(consumed, NDP_MTU_OPTION_LEN);
assert_eq!(decoded.option_type(), NDP_OPT_MTU);
assert_eq!(decoded.mtu_value(), Some(1500));
assert_eq!(NdpOption::mtu(9000).mtu_value(), Some(9000));
assert_eq!(NdpOption::generic(NDP_OPT_NONCE, []).mtu_value(), None);
assert_eq!(decoded.encode().unwrap(), bytes);
}
#[test]
fn typed_option_explicit_wrong_length_is_preserved() {
let prefix = Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0);
let wrong = NdpOption::prefix_information(prefix, 64, true, false, 0, 0).length(5);
assert_eq!(wrong.explicit_length(), Some(5));
let bytes = wrong.encode().unwrap();
assert_eq!(bytes[1], 5, "pinned length survives untouched");
assert_eq!(bytes.len(), 40, "pinned length drives the encoded size");
assert_eq!(&bytes[16..32], &prefix.octets());
assert_eq!(wrong.clear_length().effective_length().unwrap(), 4);
let mtu_wrong = NdpOption::mtu(1500).length(2);
let mtu_bytes = mtu_wrong.encode().unwrap();
assert_eq!(mtu_bytes[1], 2);
assert_eq!(mtu_bytes.len(), 16);
}
#[test]
fn redirected_header_round_trips() {
let original = [0x60u8, 0x00, 0x00, 0x00];
let opt = NdpOption::redirected_header(&original);
assert_eq!(opt.option_type(), NDP_OPT_REDIRECTED_HEADER);
assert!(opt.is_known());
assert_eq!(opt.redirected_header_data(), Some(&original[..]));
let bytes = opt.encode().unwrap();
assert_eq!(bytes.len(), 16);
assert_eq!(&bytes[0..2], &[NDP_OPT_REDIRECTED_HEADER, 2]);
assert_eq!(&bytes[2..8], &[0; NDP_REDIRECTED_HEADER_RESERVED_LEN]);
assert_eq!(&bytes[8..12], &original);
assert_eq!(&bytes[12..16], &[0, 0, 0, 0]);
let (decoded, consumed) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(consumed, 16);
assert_eq!(decoded.option_type(), NDP_OPT_REDIRECTED_HEADER);
assert_eq!(
decoded.redirected_header_data(),
Some(&[0x60, 0x00, 0x00, 0x00, 0, 0, 0, 0][..])
);
assert_eq!(decoded.encode().unwrap(), bytes);
}
#[test]
fn redirected_header_aligned_embedded_has_no_padding() {
let original = [0x11u8, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88];
let opt = NdpOption::redirected_header(&original);
let bytes = opt.encode().unwrap();
assert_eq!(bytes.len(), 16);
assert_eq!(bytes[1], 2);
let (decoded, _) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(decoded.redirected_header_data(), Some(&original[..]));
}
#[test]
fn redirected_header_accessor_rejects_other_options() {
assert_eq!(NdpOption::mtu(1500).redirected_header_data(), None);
assert_eq!(
NdpOption::generic(NDP_OPT_REDIRECTED_HEADER, [0, 0, 0]).redirected_header_data(),
None
);
let empty = NdpOption::redirected_header(&[]);
assert_eq!(empty.redirected_header_data(), Some(&[][..]));
}
#[test]
fn malformed_trailing_option_after_valid_one_errors() {
let mut area = Vec::new();
area.extend_from_slice(
&NdpOption::generic(NDP_OPT_MTU, [0, 0, 0, 0, 5, 0xdc])
.encode()
.unwrap(),
);
area.extend_from_slice(&[NDP_OPT_RDNSS, 5, 0, 0]);
let err = NdpOptions::decode(&area).unwrap_err();
assert!(matches!(err, CrafterError::BufferTooShort { .. }));
}
#[test]
fn prf_encoding_round_trips() {
assert_eq!(Prf::High.to_bits(), 0b01);
assert_eq!(Prf::Medium.to_bits(), 0b00);
assert_eq!(Prf::Low.to_bits(), 0b11);
assert_eq!(Prf::Reserved.to_bits(), 0b10);
assert_eq!(Prf::default(), Prf::Medium);
for prf in [Prf::High, Prf::Medium, Prf::Low, Prf::Reserved] {
assert_eq!(Prf::from_bits(prf.to_bits()), prf);
let flag_bits = prf.to_flag_bits();
assert_eq!(flag_bits & !NDP_PRF_MASK, 0, "Prf bits stay within 0x18");
assert_eq!(Prf::from_flag_byte(flag_bits), prf);
assert_eq!(Prf::from_flag_byte(flag_bits | !NDP_PRF_MASK), prf);
}
assert_eq!(Prf::High.to_flag_bits(), 0x08);
assert_eq!(Prf::Medium.to_flag_bits(), 0x00);
assert_eq!(Prf::Low.to_flag_bits(), 0x18);
assert_eq!(Prf::Reserved.to_flag_bits(), 0x10);
}
#[test]
fn route_information_round_trips_each_prefix_form() {
let cases = [
(Ipv6Addr::UNSPECIFIED, 0u8, 1u8, 0usize),
(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0), 32, 2, 8),
(
Ipv6Addr::new(0x2001, 0x0db8, 0x0001, 0, 0, 0, 0, 0),
48,
2,
8,
),
(
Ipv6Addr::new(0x2001, 0x0db8, 0x0001, 0x0002, 0, 0, 0, 0),
96,
3,
16,
),
];
for (prefix, prefix_len, expected_length, expected_carried) in cases {
let opt = NdpOption::route_information(prefix, prefix_len, Prf::High, 1800);
assert_eq!(opt.option_type(), NDP_OPT_ROUTE_INFORMATION);
assert!(opt.is_known());
assert_eq!(opt.route_prefix_length(), Some(prefix_len));
assert_eq!(opt.route_preference(), Some(Prf::High));
assert_eq!(opt.route_lifetime(), Some(1800));
let bytes = opt.encode().unwrap();
assert_eq!(
bytes[0], NDP_OPT_ROUTE_INFORMATION,
"type 24 (prefix_len={prefix_len})"
);
assert_eq!(
bytes[1], expected_length,
"Length field in 8-octet units (prefix_len={prefix_len})"
);
assert_eq!(
bytes.len(),
expected_length as usize * NDP_OPTION_LENGTH_UNIT,
"encoded size matches Length (prefix_len={prefix_len})"
);
assert_eq!(bytes[2], prefix_len);
assert_eq!(bytes[3], Prf::High.to_flag_bits());
assert_eq!(&bytes[4..8], &1800u32.to_be_bytes());
assert_eq!(
&bytes[8..8 + expected_carried],
&prefix.octets()[..expected_carried],
"carried prefix octets (prefix_len={prefix_len})"
);
let (decoded, consumed) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(consumed, bytes.len());
assert_eq!(decoded.route_prefix_length(), Some(prefix_len));
assert_eq!(decoded.route_preference(), Some(Prf::High));
assert_eq!(decoded.route_lifetime(), Some(1800));
let mut expected_prefix = [0u8; 16];
expected_prefix[..expected_carried]
.copy_from_slice(&prefix.octets()[..expected_carried]);
assert_eq!(
decoded.route_prefix(),
Some(Ipv6Addr::from(expected_prefix))
);
assert_eq!(decoded.encode().unwrap(), bytes);
}
}
#[test]
fn route_information_round_trips_each_preference() {
let prefix = Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0);
for prf in [Prf::High, Prf::Medium, Prf::Low, Prf::Reserved] {
let opt = NdpOption::route_information(prefix, 32, prf, NDP_ROUTE_LIFETIME_INFINITY);
let bytes = opt.encode().unwrap();
assert_eq!(bytes[3], prf.to_flag_bits());
assert_eq!(bytes[3] & !NDP_PRF_MASK, 0);
let (decoded, _) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(decoded.route_preference(), Some(prf), "Prf={prf:?}");
assert_eq!(
decoded.route_lifetime(),
Some(NDP_ROUTE_LIFETIME_INFINITY),
"infinity lifetime round-trips (Prf={prf:?})"
);
}
}
#[test]
fn route_information_raw_preserves_reserved_and_explicit_octets() {
let prefix = Ipv6Addr::new(0x2001, 0x0db8, 0x0001, 0, 0, 0, 0, 0);
let flags = Prf::Low.to_flag_bits() | !NDP_PRF_MASK;
let opt = NdpOption::route_information_raw(prefix, 48, flags, 600, 16);
let bytes = opt.encode().unwrap();
assert_eq!(bytes[1], 3);
assert_eq!(bytes.len(), NDP_ROUTE_INFORMATION_LEN_FULL_PREFIX);
assert_eq!(bytes[3], flags);
let (decoded, _) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(decoded.route_preference(), Some(Prf::Low));
assert_eq!(decoded.route_prefix(), Some(prefix));
}
#[test]
fn route_information_accessors_reject_other_options() {
let mtu = NdpOption::mtu(1500);
assert_eq!(mtu.route_prefix_length(), None);
assert_eq!(mtu.route_preference(), None);
assert_eq!(mtu.route_lifetime(), None);
assert_eq!(mtu.route_prefix(), None);
}
#[test]
fn rdnss_single_server_round_trips() {
let server = Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1);
let opt = NdpOption::rdnss(1800, &[server]);
assert_eq!(opt.option_type(), NDP_OPT_RDNSS);
assert!(opt.is_known());
assert_eq!(opt.rdnss_lifetime(), Some(1800));
assert_eq!(opt.rdnss_servers(), Some(vec![server]));
let bytes = opt.encode().unwrap();
assert_eq!(bytes[0], NDP_OPT_RDNSS);
assert_eq!(bytes[1] as usize, ndp_rdnss_length_units(1));
assert_eq!(bytes[1], 3);
assert_eq!(bytes.len(), 3 * NDP_OPTION_LENGTH_UNIT);
assert_eq!(&bytes[2..4], &[0, 0]);
assert_eq!(&bytes[4..8], &1800u32.to_be_bytes());
assert_eq!(&bytes[8..24], &server.octets());
let (decoded, consumed) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(consumed, bytes.len());
assert_eq!(decoded.rdnss_lifetime(), Some(1800));
assert_eq!(decoded.rdnss_servers(), Some(vec![server]));
assert_eq!(decoded.encode().unwrap(), bytes);
}
#[test]
fn rdnss_two_servers_round_trip() {
let servers = [
Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1),
Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 2),
];
let opt = NdpOption::rdnss(NDP_DNS_LIFETIME_INFINITY, &servers);
let bytes = opt.encode().unwrap();
assert_eq!(bytes[1] as usize, ndp_rdnss_length_units(2));
assert_eq!(bytes[1], 5);
assert_eq!(bytes.len(), 5 * NDP_OPTION_LENGTH_UNIT);
assert_eq!(&bytes[8..24], &servers[0].octets());
assert_eq!(&bytes[24..40], &servers[1].octets());
let (decoded, _) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(
decoded.rdnss_lifetime(),
Some(NDP_DNS_LIFETIME_INFINITY),
"infinity lifetime round-trips"
);
assert_eq!(decoded.rdnss_servers(), Some(servers.to_vec()));
assert_eq!(decoded.encode().unwrap(), bytes);
}
#[test]
fn rdnss_accessors_reject_other_options() {
let mtu = NdpOption::mtu(1500);
assert_eq!(mtu.rdnss_lifetime(), None);
assert_eq!(mtu.rdnss_servers(), None);
}
#[test]
fn dnssl_multiple_domains_round_trip() {
let domains = ["example.com", "example.net"];
let opt = NdpOption::dnssl(86_400, &domains);
assert_eq!(opt.option_type(), NDP_OPT_DNSSL);
assert!(opt.is_known());
assert_eq!(opt.dnssl_lifetime(), Some(86_400));
assert_eq!(
opt.dnssl_domains(),
Some(vec!["example.com.".to_string(), "example.net.".to_string()])
);
let mut expected_names = Vec::new();
for domain in domains {
expected_names.extend_from_slice(
&DnsName::parse(domain)
.unwrap()
.encode_uncompressed()
.unwrap(),
);
}
assert_eq!(
&opt.value()[NDP_DNS_RESERVED_LEN + 4..],
&expected_names[..]
);
let bytes = opt.encode().unwrap();
assert_eq!(bytes[0], NDP_OPT_DNSSL);
assert_eq!(bytes.len() % NDP_OPTION_LENGTH_UNIT, 0);
assert_eq!(bytes.len(), bytes[1] as usize * NDP_OPTION_LENGTH_UNIT);
assert_eq!(&bytes[2..4], &[0, 0]);
assert_eq!(&bytes[4..8], &86_400u32.to_be_bytes());
let names_len = expected_names.len();
assert_eq!(names_len, 26);
let padding = &bytes[8 + names_len..];
assert!(
!padding.is_empty(),
"the value is padded to the 8-octet boundary"
);
assert!(
padding.iter().all(|&b| b == 0),
"RFC 8106 sec 5.2 pads with zero octets, got {padding:?}"
);
let (decoded, consumed) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(consumed, bytes.len());
assert_eq!(decoded.dnssl_lifetime(), Some(86_400));
assert_eq!(
decoded.dnssl_domains(),
Some(vec!["example.com.".to_string(), "example.net.".to_string()])
);
assert_eq!(decoded.encode().unwrap(), bytes);
}
#[test]
fn dnssl_single_and_subdomain_round_trip() {
let opt = NdpOption::dnssl(0, &["lab.example.org"]);
assert_eq!(opt.dnssl_lifetime(), Some(0));
let bytes = opt.encode().unwrap();
let (decoded, _) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(
decoded.dnssl_domains(),
Some(vec!["lab.example.org.".to_string()])
);
}
#[test]
fn dns_option_explicit_wrong_length_is_preserved() {
let server = Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1);
let wrong = NdpOption::rdnss(1800, &[server]).length(7);
assert_eq!(wrong.explicit_length(), Some(7));
let bytes = wrong.encode().unwrap();
assert_eq!(bytes[1], 7, "pinned length survives untouched");
assert_eq!(bytes.len(), 56, "pinned length drives the encoded size");
assert_eq!(&bytes[8..24], &server.octets());
assert_eq!(wrong.clear_length().effective_length().unwrap(), 3);
let dnssl_wrong = NdpOption::dnssl(60, &["example.com"]).length(1);
let dnssl_bytes = dnssl_wrong.encode().unwrap();
assert_eq!(dnssl_bytes[1], 1);
assert_eq!(dnssl_bytes.len(), 8);
}
#[test]
fn dnssl_accessors_reject_other_options() {
let mtu = NdpOption::mtu(1500);
assert_eq!(mtu.dnssl_lifetime(), None);
assert_eq!(mtu.dnssl_domains(), None);
}
#[test]
fn ra_flags_extension_round_trips() {
let bits = [0x80u8, 0x01, 0x02, 0x03, 0x04, 0x05];
let opt = NdpOption::ra_flags_extension(bits);
assert_eq!(opt.option_type(), NDP_OPT_RA_FLAGS_EXTENSION);
assert!(opt.is_known());
assert_eq!(opt.ra_flags_extension_bits(), Some(bits));
let bytes = opt.encode().unwrap();
assert_eq!(bytes.len(), NDP_RA_FLAGS_EXTENSION_LEN);
assert_eq!(
&bytes[0..2],
&[NDP_OPT_RA_FLAGS_EXTENSION, NDP_RA_FLAGS_EXTENSION_UNITS]
);
assert_eq!(&bytes[2..8], &bits);
let (decoded, consumed) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(consumed, NDP_RA_FLAGS_EXTENSION_LEN);
assert_eq!(decoded.option_type(), NDP_OPT_RA_FLAGS_EXTENSION);
assert_eq!(decoded.ra_flags_extension_bits(), Some(bits));
assert_eq!(decoded.encode().unwrap(), bytes);
assert_eq!(NdpOption::mtu(1500).ra_flags_extension_bits(), None);
}
#[test]
fn nonce_round_trips() {
let nonce = [0xa1u8, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6];
let opt = NdpOption::nonce(&nonce);
assert_eq!(opt.option_type(), NDP_OPT_NONCE);
assert!(opt.is_known());
assert_eq!(opt.nonce_value(), Some(&nonce[..]));
let bytes = opt.encode().unwrap();
assert_eq!(bytes.len(), 8);
assert_eq!(&bytes[0..2], &[NDP_OPT_NONCE, 1]);
assert_eq!(&bytes[2..8], &nonce);
let (decoded, consumed) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(consumed, 8);
assert_eq!(decoded.nonce_value(), Some(&nonce[..]));
assert_eq!(decoded.encode().unwrap(), bytes);
assert_eq!(NdpOption::mtu(1500).nonce_value(), None);
}
#[test]
fn nonce_padding_extends_the_value_area() {
let nonce = [1u8, 2, 3, 4, 5, 6, 7, 8];
let opt = NdpOption::nonce(&nonce);
let bytes = opt.encode().unwrap();
assert_eq!(bytes.len(), 16);
assert_eq!(bytes[1], 2);
assert_eq!(&bytes[2..10], &nonce);
assert_eq!(&bytes[10..16], &[0; 6]);
let (decoded, _) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(
decoded.nonce_value(),
Some(&[1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0][..])
);
}
#[test]
fn pref64_plc_round_trips() {
let assigned = [(0u8, 96u8), (1, 64), (2, 56), (3, 48), (4, 40), (5, 32)];
for (code, bits) in assigned {
let plc = Pref64Plc::from_plc(code);
assert_eq!(plc, Pref64Plc::PrefixLength(bits));
assert_eq!(plc.prefix_length_bits(), Some(bits));
assert_eq!(plc.to_plc(), Some(code));
assert_eq!(
Pref64Plc::from_prefix_length_bits(bits).to_plc(),
Some(code)
);
}
for code in [6u8, 7] {
let plc = Pref64Plc::from_plc(code);
assert_eq!(plc, Pref64Plc::Reserved(code));
assert_eq!(plc.prefix_length_bits(), None);
assert_eq!(plc.to_plc(), Some(code));
}
assert_eq!(Pref64Plc::from_prefix_length_bits(80).to_plc(), None);
}
#[test]
fn pref64_round_trips() {
let prefix = Ipv6Addr::new(0x0064, 0xff9b, 0, 0, 0, 0, 0, 0);
let opt = NdpOption::pref64(600, 96, prefix).unwrap();
assert_eq!(opt.option_type(), NDP_OPT_PREF64);
assert!(opt.is_known());
assert_eq!(opt.pref64_scaled_lifetime(), Some(600));
assert_eq!(opt.pref64_plc(), Some(Pref64Plc::PrefixLength(96)));
assert_eq!(opt.pref64_prefix_length(), Some(96));
assert_eq!(opt.pref64_prefix(), Some(prefix));
let bytes = opt.encode().unwrap();
assert_eq!(bytes.len(), NDP_PREF64_LEN);
assert_eq!(&bytes[0..2], &[NDP_OPT_PREF64, NDP_PREF64_UNITS]);
assert_eq!(&bytes[2..4], &(600u16 << 3).to_be_bytes());
assert_eq!(&bytes[4..16], &prefix.octets()[..NDP_PREF64_PREFIX_LEN]);
let (decoded, consumed) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(consumed, NDP_PREF64_LEN);
assert_eq!(decoded.pref64_scaled_lifetime(), Some(600));
assert_eq!(decoded.pref64_prefix_length(), Some(96));
assert_eq!(decoded.pref64_prefix(), Some(prefix));
assert_eq!(decoded.encode().unwrap(), bytes);
for bits in [96u8, 64, 56, 48, 40, 32] {
let o = NdpOption::pref64(0, bits, prefix).unwrap();
let (d, _) = NdpOption::decode_one(&o.encode().unwrap()).unwrap();
assert_eq!(d.pref64_prefix_length(), Some(bits), "PLC for /{bits}");
}
}
#[test]
fn pref64_rejects_bad_length_but_raw_allows_reserved_plc() {
let prefix = Ipv6Addr::new(0x0064, 0xff9b, 0, 0, 0, 0, 0, 0);
assert!(NdpOption::pref64(600, 80, prefix).is_err());
let opt = NdpOption::pref64_raw(600, 7, prefix);
let bytes = opt.encode().unwrap();
assert_eq!(bytes[1], NDP_PREF64_UNITS);
let word = u16::from_be_bytes([bytes[2], bytes[3]]);
assert_eq!(word & NDP_PREF64_PLC_MASK, 7);
assert_eq!(word >> NDP_PREF64_SCALED_LIFETIME_SHIFT, 600);
let (decoded, _) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(decoded.pref64_plc(), Some(Pref64Plc::Reserved(7)));
assert_eq!(decoded.pref64_prefix_length(), None);
assert_eq!(decoded.pref64_scaled_lifetime(), Some(600));
assert_eq!(NdpOption::mtu(1500).pref64_scaled_lifetime(), None);
assert_eq!(NdpOption::mtu(1500).pref64_plc(), None);
assert_eq!(NdpOption::mtu(1500).pref64_prefix(), None);
}
#[test]
fn captive_portal_round_trips_and_strips_padding() {
let uri = "https://example.com/captive-portal/api";
let opt = NdpOption::captive_portal(uri);
assert_eq!(opt.option_type(), NDP_OPT_CAPTIVE_PORTAL);
assert!(opt.is_known());
assert_eq!(opt.captive_portal_uri().as_deref(), Some(uri));
let bytes = opt.encode().unwrap();
assert_eq!(bytes[0], NDP_OPT_CAPTIVE_PORTAL);
assert_eq!(bytes.len() % NDP_OPTION_LENGTH_UNIT, 0);
assert_eq!(bytes.len(), bytes[1] as usize * NDP_OPTION_LENGTH_UNIT);
assert_eq!(&bytes[2..2 + uri.len()], uri.as_bytes());
let padding = &bytes[2 + uri.len()..];
assert!(
padding.iter().all(|&b| b == 0),
"RFC 8910 sec 2.3 pads with NUL, got {padding:?}"
);
let (decoded, consumed) = NdpOption::decode_one(&bytes).unwrap();
assert_eq!(consumed, bytes.len());
assert_eq!(decoded.captive_portal_uri().as_deref(), Some(uri));
assert_eq!(decoded.encode().unwrap(), bytes);
assert_eq!(NdpOption::mtu(1500).captive_portal_uri(), None);
}
#[test]
fn captive_portal_boundary_and_short_uri() {
let exact = NdpOption::captive_portal("ftp://");
let exact_bytes = exact.encode().unwrap();
assert_eq!(exact_bytes.len(), 8);
assert_eq!(exact_bytes[1], 1);
let (d, _) = NdpOption::decode_one(&exact_bytes).unwrap();
assert_eq!(d.captive_portal_uri().as_deref(), Some("ftp://"));
let short = NdpOption::captive_portal("a");
let (d2, _) = NdpOption::decode_one(&short.encode().unwrap()).unwrap();
assert_eq!(d2.captive_portal_uri().as_deref(), Some("a"));
}
#[test]
fn unrecognized_send_option_is_preserved_as_unknown() {
const NDP_OPT_CGA: u8 = 11;
assert!(!ndp_option_type_is_known(NDP_OPT_CGA));
let cga_bytes = [0xde, 0xad, 0xbe, 0xef, 0x01, 0x02];
let cga = NdpOption::unknown(NDP_OPT_CGA, cga_bytes);
let nonce = NdpOption::nonce(&[0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6]);
let portal = NdpOption::captive_portal("https://example.com/cp");
let options = NdpOptions::new()
.push(nonce.clone())
.push(cga.clone())
.push(portal.clone());
let encoded = options.encode().unwrap();
let decoded = NdpOptions::decode(&encoded).unwrap();
assert_eq!(decoded.len(), 3);
assert_eq!(decoded.options()[0].option_type(), NDP_OPT_NONCE);
assert_eq!(decoded.options()[2].option_type(), NDP_OPT_CAPTIVE_PORTAL);
let mid = &decoded.options()[1];
assert_eq!(mid.option_type(), NDP_OPT_CGA);
assert!(!mid.is_known());
match mid {
NdpOption::Unknown { bytes, .. } => {
assert_eq!(bytes, &cga_bytes);
}
other => panic!("expected Unknown SEND option, got {other:?}"),
}
assert_eq!(
decoded.options()[2].captive_portal_uri().as_deref(),
Some("https://example.com/cp")
);
assert_eq!(decoded.encode().unwrap(), encoded);
}
#[test]
fn new_option_explicit_wrong_length_is_preserved() {
let prefix = Ipv6Addr::new(0x0064, 0xff9b, 0, 0, 0, 0, 0, 0);
let wrong = NdpOption::pref64(600, 96, prefix).unwrap().length(4);
assert_eq!(wrong.explicit_length(), Some(4));
let bytes = wrong.encode().unwrap();
assert_eq!(bytes[1], 4, "pinned length survives untouched");
assert_eq!(bytes.len(), 32, "pinned length drives the encoded size");
assert_eq!(wrong.clear_length().effective_length().unwrap(), 2);
let raext = NdpOption::ra_flags_extension([0; 6]).length(3);
let raext_bytes = raext.encode().unwrap();
assert_eq!(raext_bytes[1], 3);
assert_eq!(raext_bytes.len(), 24);
}
}