use std::net::Ipv6Addr;
use super::types::{NdpKind, NdpMessage};
use crate::MacAddr;
const ICMPV6_NS: u8 = 135;
const ICMPV6_NA: u8 = 136;
const OPT_SOURCE_LLADDR: u8 = 1;
const OPT_TARGET_LLADDR: u8 = 2;
const NA_FLAG_ROUTER: u8 = 0x80;
const NA_FLAG_SOLICITED: u8 = 0x40;
const NA_FLAG_OVERRIDE: u8 = 0x20;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseError {
Truncated {
need: usize,
have: usize,
},
NotNdp,
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Truncated { need, have } => {
write!(f, "truncated NDP message: need {need}, have {have}")
}
Self::NotNdp => f.write_str("not an NDP message (ICMPv6 type isn't 135/136)"),
}
}
}
impl std::error::Error for ParseError {}
impl From<ParseError> for crate::Error {
fn from(e: ParseError) -> Self {
use crate::error::{ErrorCode, Module};
let code = match &e {
ParseError::Truncated { .. } => ErrorCode::Truncated,
ParseError::NotNdp => ErrorCode::Parse,
};
crate::Error::with_code(Module::Ndp, code, e.to_string())
}
}
pub fn parse_icmpv6(icmpv6: &[u8]) -> Result<NdpMessage, ParseError> {
if icmpv6.len() < 4 {
return Err(ParseError::Truncated {
need: 4,
have: icmpv6.len(),
});
}
let ty = icmpv6[0];
if ty != ICMPV6_NS && ty != ICMPV6_NA {
return Err(ParseError::NotNdp);
}
let body = &icmpv6[4..];
parse_body(ty, body)
}
pub fn parse(icmpv6_type: u8, body: &[u8]) -> Result<NdpMessage, ParseError> {
parse_body(icmpv6_type, body)
}
fn parse_body(icmpv6_type: u8, body: &[u8]) -> Result<NdpMessage, ParseError> {
if body.len() < 20 {
return Err(ParseError::Truncated {
need: 20,
have: body.len(),
});
}
let (kind, router_flag, solicited_flag, override_flag, expected_opt) = match icmpv6_type {
ICMPV6_NS => (
NdpKind::Solicitation,
false,
false,
false,
OPT_SOURCE_LLADDR,
),
ICMPV6_NA => {
let flag_byte = body[0];
(
NdpKind::Advertisement,
flag_byte & NA_FLAG_ROUTER != 0,
flag_byte & NA_FLAG_SOLICITED != 0,
flag_byte & NA_FLAG_OVERRIDE != 0,
OPT_TARGET_LLADDR,
)
}
_ => return Err(ParseError::NotNdp),
};
let mut target_octets = [0u8; 16];
target_octets.copy_from_slice(&body[4..20]);
let target = Ipv6Addr::from(target_octets);
let lladdr = parse_lladdr_option(&body[20..], expected_opt);
Ok(NdpMessage {
kind,
target,
lladdr,
router_flag,
solicited_flag,
override_flag,
})
}
fn parse_lladdr_option(mut opts: &[u8], expected_opt: u8) -> Option<MacAddr> {
for _ in 0..16 {
if opts.len() < 2 {
return None;
}
let opt_type = opts[0];
let opt_len_units = opts[1];
if opt_len_units == 0 {
return None;
}
let opt_len = (opt_len_units as usize) * 8;
if opts.len() < opt_len {
return None;
}
if opt_type == expected_opt && opt_len >= 8 {
let mut mac = [0u8; 6];
mac.copy_from_slice(&opts[2..8]);
return Some(MacAddr(mac));
}
opts = &opts[opt_len..];
}
None
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NdpParser;
impl NdpParser {
#[inline]
pub fn parse_icmpv6(&self, icmpv6: &[u8]) -> Result<NdpMessage, ParseError> {
parse_icmpv6(icmpv6)
}
#[inline]
pub fn parse(&self, icmpv6_type: u8, body: &[u8]) -> Result<NdpMessage, ParseError> {
parse(icmpv6_type, body)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ns_body(target: [u8; 16], lladdr: Option<[u8; 6]>) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&[0; 4]); b.extend_from_slice(&target);
if let Some(mac) = lladdr {
b.push(OPT_SOURCE_LLADDR);
b.push(1); b.extend_from_slice(&mac);
}
b
}
fn na_body(target: [u8; 16], lladdr: Option<[u8; 6]>, flags: u8) -> Vec<u8> {
let mut b = Vec::new();
b.push(flags);
b.extend_from_slice(&[0; 3]); b.extend_from_slice(&target);
if let Some(mac) = lladdr {
b.push(OPT_TARGET_LLADDR);
b.push(1);
b.extend_from_slice(&mac);
}
b
}
fn icmpv6(ty: u8, body: &[u8]) -> Vec<u8> {
let mut m = vec![ty, 0, 0, 0]; m.extend_from_slice(body);
m
}
#[test]
fn parses_neighbor_solicitation() {
let body = ns_body(
[0x20, 0x01, 0xdb, 0x08, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
None,
);
let m = parse_icmpv6(&icmpv6(135, &body)).unwrap();
assert_eq!(m.kind, NdpKind::Solicitation);
assert_eq!(m.target.octets()[15], 1);
assert_eq!(m.lladdr, None);
}
#[test]
fn parses_neighbor_solicitation_with_source_lladdr() {
let body = ns_body([0; 16], Some([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]));
let m = parse_icmpv6(&icmpv6(135, &body)).unwrap();
assert_eq!(m.kind, NdpKind::Solicitation);
assert_eq!(
m.lladdr,
Some(MacAddr([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]))
);
}
#[test]
fn parses_neighbor_advertisement_solicited_override() {
let flags = NA_FLAG_SOLICITED | NA_FLAG_OVERRIDE;
let body = na_body([0; 16], Some([0xaa; 6]), flags);
let m = parse_icmpv6(&icmpv6(136, &body)).unwrap();
assert_eq!(m.kind, NdpKind::Advertisement);
assert!(m.solicited_flag);
assert!(m.override_flag);
assert!(!m.router_flag);
assert_eq!(m.lladdr, Some(MacAddr([0xaa; 6])));
}
#[test]
fn parses_neighbor_advertisement_router_flag() {
let body = na_body([0; 16], None, NA_FLAG_ROUTER);
let m = parse_icmpv6(&icmpv6(136, &body)).unwrap();
assert!(m.router_flag);
}
#[test]
fn na_unsolicited_override_classifies_as_spoof() {
let body = na_body([0; 16], Some([0xbb; 6]), NA_FLAG_OVERRIDE);
let m = parse_icmpv6(&icmpv6(136, &body)).unwrap();
assert!(m.is_unsolicited_override());
assert!(m.is_likely_spoof());
}
#[test]
fn rejects_truncated_icmpv6_header() {
assert!(parse_icmpv6(&[]).is_err());
assert!(parse_icmpv6(&[135, 0, 0]).is_err());
}
#[test]
fn rejects_truncated_body() {
let mut m = vec![135, 0, 0, 0];
m.extend_from_slice(&[0; 5]);
assert!(parse_icmpv6(&m).is_err());
}
#[test]
fn rejects_non_ndp_type() {
let mut m = vec![128, 0, 0, 0];
m.extend_from_slice(&[0; 24]);
assert!(parse_icmpv6(&m).is_err());
}
#[test]
fn lladdr_option_length_zero_is_rejected() {
let mut body = na_body([0; 16], None, 0);
body.push(OPT_TARGET_LLADDR);
body.push(0); body.extend_from_slice(&[0xaa; 6]);
let m = parse_icmpv6(&icmpv6(136, &body)).unwrap();
assert_eq!(m.lladdr, None);
}
#[test]
fn unknown_option_is_skipped_to_find_lladdr() {
let mut body = vec![0; 20]; body.extend_from_slice(&[99, 1, 0, 0, 0, 0, 0, 0]);
body.extend_from_slice(&[OPT_TARGET_LLADDR, 1, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11]);
let m = parse_icmpv6(&icmpv6(136, &body)).unwrap();
assert_eq!(
m.lladdr,
Some(MacAddr([0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11]))
);
}
#[test]
fn option_walker_caps_iterations() {
let mut body = vec![0; 20];
for _ in 0..16 {
body.extend_from_slice(&[99, 1, 0, 0, 0, 0, 0, 0]);
}
body.extend_from_slice(&[OPT_TARGET_LLADDR, 1, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11]);
let m = parse_icmpv6(&icmpv6(136, &body)).unwrap();
assert_eq!(m.lladdr, None);
}
#[test]
fn ndp_parser_marker_delegates() {
let body = na_body([0; 16], Some([0xaa; 6]), NA_FLAG_OVERRIDE);
let p = NdpParser;
let m = p.parse_icmpv6(&icmpv6(136, &body)).unwrap();
assert!(m.is_likely_spoof());
let m2 = p.parse(136, &body).unwrap();
assert!(m2.is_likely_spoof());
}
}