use std::net::Ipv6Addr;
pub const ICMPV6_DEST_UNREACHABLE: u8 = 1;
pub const ICMPV6_TIME_EXCEEDED: u8 = 3;
pub const ICMPV6_ECHO_REQUEST: u8 = 128;
pub const ICMPV6_ECHO_REPLY: u8 = 129;
pub const ICMPV6_HEADER_SIZE: usize = 8;
pub const IPV6_HEADER_SIZE: usize = 40;
pub const IPV6_NEXT_HEADER_ICMPV6: u8 = 58;
const MIN_ERROR_WITH_ECHO_LEN: usize = ICMPV6_HEADER_SIZE + IPV6_HEADER_SIZE + ICMPV6_HEADER_SIZE;
pub fn build_echo_request_v6(identifier: u16, sequence: u16, payload: &[u8]) -> Vec<u8> {
let mut buf = vec![0u8; ICMPV6_HEADER_SIZE + payload.len()];
buf[0] = ICMPV6_ECHO_REQUEST; buf[1] = 0; buf[4..6].copy_from_slice(&identifier.to_be_bytes());
buf[6..8].copy_from_slice(&sequence.to_be_bytes());
buf[ICMPV6_HEADER_SIZE..].copy_from_slice(payload);
buf
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Icmpv6Header {
pub icmpv6_type: u8,
pub icmpv6_code: u8,
}
pub fn parse_icmpv6_header(data: &[u8]) -> Option<Icmpv6Header> {
if data.len() < 4 {
return None;
}
Some(Icmpv6Header {
icmpv6_type: data[0],
icmpv6_code: data[1],
})
}
pub fn parse_echo_reply_v6(data: &[u8]) -> Option<(u16, u16)> {
if data.len() < ICMPV6_HEADER_SIZE || data[0] != ICMPV6_ECHO_REPLY {
return None;
}
let identifier = u16::from_be_bytes([data[4], data[5]]);
let sequence = u16::from_be_bytes([data[6], data[7]]);
Some((identifier, sequence))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EmbeddedProbe {
pub identifier: u16,
pub sequence: u16,
pub destination: Ipv6Addr,
}
pub fn parse_embedded_probe(data: &[u8]) -> Option<EmbeddedProbe> {
if data.len() < MIN_ERROR_WITH_ECHO_LEN {
return None;
}
let outer_type = data[0];
if outer_type != ICMPV6_TIME_EXCEEDED && outer_type != ICMPV6_DEST_UNREACHABLE {
return None;
}
let inner_ip = &data[ICMPV6_HEADER_SIZE..ICMPV6_HEADER_SIZE + IPV6_HEADER_SIZE];
if inner_ip[0] >> 4 != 6 {
return None; }
if inner_ip[6] != IPV6_NEXT_HEADER_ICMPV6 {
return None;
}
let destination = Ipv6Addr::from(<[u8; 16]>::try_from(&inner_ip[24..40]).ok()?);
let inner_icmp = &data[ICMPV6_HEADER_SIZE + IPV6_HEADER_SIZE..MIN_ERROR_WITH_ECHO_LEN];
if inner_icmp[0] != ICMPV6_ECHO_REQUEST {
return None;
}
Some(EmbeddedProbe {
identifier: u16::from_be_bytes([inner_icmp[4], inner_icmp[5]]),
sequence: u16::from_be_bytes([inner_icmp[6], inner_icmp[7]]),
destination,
})
}
pub fn is_ndp(icmpv6_type: u8) -> bool {
(133..=137).contains(&icmpv6_type)
}
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub fn format_ipv6_with_zone(addr: Ipv6Addr, scope_id: u32) -> String {
if scope_id == 0 {
return addr.to_string();
}
#[cfg(any(
target_os = "macos",
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd"
))]
{
let mut name_buf = [0u8; libc::IF_NAMESIZE];
let ret = unsafe { libc::if_indextoname(scope_id, name_buf.as_mut_ptr().cast()) };
if !ret.is_null() {
let len = name_buf.iter().position(|&b| b == 0).unwrap_or(0);
if let Ok(name) = std::str::from_utf8(&name_buf[..len]) {
if !name.is_empty() {
return format!("{addr}%{name}");
}
}
}
}
format!("{addr}%{scope_id}")
}
#[cfg(test)]
mod tests {
use super::*;
fn build_error_message(
outer_type: u8,
inner_version: u8,
inner_next_header: u8,
inner_icmp_type: u8,
identifier: u16,
sequence: u16,
dst: Ipv6Addr,
) -> Vec<u8> {
let mut buf = vec![0u8; MIN_ERROR_WITH_ECHO_LEN];
buf[0] = outer_type;
let inner_ip = &mut buf[8..48];
inner_ip[0] = inner_version << 4;
inner_ip[6] = inner_next_header;
inner_ip[7] = 1; inner_ip[24..40].copy_from_slice(&dst.octets());
let inner_icmp = &mut buf[48..56];
inner_icmp[0] = inner_icmp_type;
inner_icmp[4..6].copy_from_slice(&identifier.to_be_bytes());
inner_icmp[6..8].copy_from_slice(&sequence.to_be_bytes());
buf
}
const GOOGLE_V6: Ipv6Addr = Ipv6Addr::new(0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888);
#[test]
fn test_build_echo_request_v6_layout() {
let pkt = build_echo_request_v6(0x1234, 0x0007, &[0xAA, 0xBB]);
assert_eq!(pkt.len(), 10);
assert_eq!(pkt[0], ICMPV6_ECHO_REQUEST);
assert_eq!(pkt[1], 0); assert_eq!(&pkt[2..4], &[0, 0]); assert_eq!(u16::from_be_bytes([pkt[4], pkt[5]]), 0x1234);
assert_eq!(u16::from_be_bytes([pkt[6], pkt[7]]), 0x0007);
assert_eq!(&pkt[8..], &[0xAA, 0xBB]);
}
#[test]
fn test_build_echo_request_v6_empty_payload() {
let pkt = build_echo_request_v6(1, 2, &[]);
assert_eq!(pkt.len(), ICMPV6_HEADER_SIZE);
}
#[test]
fn test_parse_icmpv6_header() {
let hdr = parse_icmpv6_header(&[ICMPV6_TIME_EXCEEDED, 0, 0xde, 0xad])
.expect("4-byte header should parse");
assert_eq!(hdr.icmpv6_type, ICMPV6_TIME_EXCEEDED);
assert_eq!(hdr.icmpv6_code, 0);
assert!(parse_icmpv6_header(&[1, 2, 3]).is_none()); }
#[test]
fn test_parse_echo_reply_v6() {
let mut data = vec![0u8; ICMPV6_HEADER_SIZE];
data[0] = ICMPV6_ECHO_REPLY;
data[4..6].copy_from_slice(&0xBEEFu16.to_be_bytes());
data[6..8].copy_from_slice(&42u16.to_be_bytes());
assert_eq!(parse_echo_reply_v6(&data), Some((0xBEEF, 42)));
}
#[test]
fn test_parse_echo_reply_v6_rejects_wrong_type_and_short() {
let mut data = vec![0u8; ICMPV6_HEADER_SIZE];
data[0] = ICMPV6_ECHO_REQUEST; assert!(parse_echo_reply_v6(&data).is_none());
assert!(parse_echo_reply_v6(&[ICMPV6_ECHO_REPLY, 0, 0, 0]).is_none());
}
#[test]
fn test_parse_embedded_probe_time_exceeded() {
let msg = build_error_message(
ICMPV6_TIME_EXCEEDED,
6,
IPV6_NEXT_HEADER_ICMPV6,
ICMPV6_ECHO_REQUEST,
0x3333,
9,
GOOGLE_V6,
);
let probe = parse_embedded_probe(&msg).expect("valid TE should parse");
assert_eq!(probe.identifier, 0x3333);
assert_eq!(probe.sequence, 9);
assert_eq!(probe.destination, GOOGLE_V6);
}
#[test]
fn test_parse_embedded_probe_dest_unreachable() {
let msg = build_error_message(
ICMPV6_DEST_UNREACHABLE,
6,
IPV6_NEXT_HEADER_ICMPV6,
ICMPV6_ECHO_REQUEST,
7,
8,
GOOGLE_V6,
);
let probe = parse_embedded_probe(&msg).expect("valid unreachable should parse");
assert_eq!((probe.identifier, probe.sequence), (7, 8));
}
#[test]
fn test_parse_embedded_probe_rejects_echo_reply_outer_type() {
let msg = build_error_message(
ICMPV6_ECHO_REPLY, 6,
IPV6_NEXT_HEADER_ICMPV6,
ICMPV6_ECHO_REQUEST,
1,
1,
GOOGLE_V6,
);
assert!(parse_embedded_probe(&msg).is_none());
}
#[test]
fn test_parse_embedded_probe_rejects_bad_version() {
let msg = build_error_message(
ICMPV6_TIME_EXCEEDED,
4, IPV6_NEXT_HEADER_ICMPV6,
ICMPV6_ECHO_REQUEST,
1,
1,
GOOGLE_V6,
);
assert!(parse_embedded_probe(&msg).is_none());
}
#[test]
fn test_parse_embedded_probe_rejects_non_icmpv6_next_header() {
let msg = build_error_message(
ICMPV6_TIME_EXCEEDED,
6,
17, ICMPV6_ECHO_REQUEST,
1,
1,
GOOGLE_V6,
);
assert!(parse_embedded_probe(&msg).is_none());
}
#[test]
fn test_parse_embedded_probe_rejects_non_echo_inner() {
let msg = build_error_message(
ICMPV6_TIME_EXCEEDED,
6,
IPV6_NEXT_HEADER_ICMPV6,
ICMPV6_ECHO_REPLY, 1,
1,
GOOGLE_V6,
);
assert!(parse_embedded_probe(&msg).is_none());
}
#[test]
fn test_parse_embedded_probe_rejects_truncated() {
let msg = build_error_message(
ICMPV6_TIME_EXCEEDED,
6,
IPV6_NEXT_HEADER_ICMPV6,
ICMPV6_ECHO_REQUEST,
1,
1,
GOOGLE_V6,
);
assert!(parse_embedded_probe(&msg[..MIN_ERROR_WITH_ECHO_LEN - 1]).is_none());
}
#[test]
fn test_is_ndp_covers_discovery_types_only() {
for ty in 133..=137u8 {
assert!(is_ndp(ty), "type {ty} is NDP");
}
for ty in [
ICMPV6_DEST_UNREACHABLE,
ICMPV6_TIME_EXCEEDED,
ICMPV6_ECHO_REQUEST,
ICMPV6_ECHO_REPLY,
132,
138,
] {
assert!(!is_ndp(ty), "type {ty} is not NDP");
}
}
#[test]
fn test_ipv6_display_is_rfc5952_canonical() {
let cases: [(Ipv6Addr, &str); 7] = [
(Ipv6Addr::UNSPECIFIED, "::"),
(Ipv6Addr::LOCALHOST, "::1"),
(GOOGLE_V6, "2001:4860:4860::8888"),
(
Ipv6Addr::new(0x2001, 0xdb8, 0, 1, 0, 0, 0, 1),
"2001:db8:0:1::1",
),
(
Ipv6Addr::new(0x2001, 0xDB8, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF),
"2001:db8:a:b:c:d:e:f",
),
(
Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0x0102, 0x0304),
"::ffff:1.2.3.4",
),
(
Ipv6Addr::new(0x2001, 0xdb8, 1, 1, 1, 1, 0, 1),
"2001:db8:1:1:1:1:0:1",
),
];
for (addr, want) in cases {
assert_eq!(addr.to_string(), want);
assert_eq!(want.parse::<Ipv6Addr>().expect("round-trip parse"), addr);
}
}
#[test]
fn test_format_ipv6_with_zone() {
let ll = Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1);
assert_eq!(format_ipv6_with_zone(ll, 0), "fe80::1");
let formatted = format_ipv6_with_zone(ll, 1);
let (addr_part, zone) = formatted
.split_once('%')
.expect("non-zero scope must render a %zone suffix");
assert_eq!(addr_part, "fe80::1");
assert!(!zone.is_empty());
assert_eq!(
format_ipv6_with_zone(ll, 0x7fff_fff0),
format!("fe80::1%{}", 0x7fff_fff0)
);
}
}