#![cfg(feature = "icmp")]
use std::net::Ipv6Addr;
use flowscope::icmp::*;
fn v4_header(ty: u8, code: u8, bytes4: [u8; 4]) -> Vec<u8> {
let mut v = Vec::new();
v.push(ty);
v.push(code);
v.extend_from_slice(&[0, 0]); v.extend_from_slice(&bytes4);
v
}
fn v6_header(ty: u8, code: u8, bytes4: [u8; 4]) -> Vec<u8> {
let mut v = Vec::new();
v.push(ty);
v.push(code);
v.extend_from_slice(&[0, 0]); v.extend_from_slice(&bytes4);
v
}
#[test]
fn parses_v4_echo_request() {
let payload = v4_header(8, 0, [0x12, 0x34, 0x56, 0x78]);
let msg = parse_v4(&payload).expect("parses");
match msg.ty {
IcmpType::V4(Icmpv4Type::EchoRequest { id, seq }) => {
assert_eq!(id, 0x1234);
assert_eq!(seq, 0x5678);
}
other => panic!("expected EchoRequest, got {other:?}"),
}
}
#[test]
fn parses_v4_echo_reply() {
let payload = v4_header(0, 0, [0xAA, 0xBB, 0x00, 0x01]);
let msg = parse_v4(&payload).expect("parses");
assert!(matches!(
msg.ty,
IcmpType::V4(Icmpv4Type::EchoReply { id: 0xAABB, seq: 1 })
));
}
#[test]
fn parses_v6_echo_request() {
let payload = v6_header(128, 0, [0x0F, 0x0F, 0x00, 0x01]);
let msg = parse_v6(&payload).expect("parses");
assert!(matches!(
msg.ty,
IcmpType::V6(Icmpv6Type::EchoRequest { id: 0x0F0F, seq: 1 })
));
}
fn ipv4_tcp_inner(src: [u8; 4], dst: [u8; 4], sport: u16, dport: u16) -> Vec<u8> {
let mut v = Vec::new();
v.push(0x45); v.push(0); v.extend_from_slice(&[0x00, 0x28]); v.extend_from_slice(&[0, 0]); v.extend_from_slice(&[0, 0]); v.push(64); v.push(6); v.extend_from_slice(&[0, 0]); v.extend_from_slice(&src);
v.extend_from_slice(&dst);
v.extend_from_slice(&sport.to_be_bytes());
v.extend_from_slice(&dport.to_be_bytes());
v.extend_from_slice(&[0, 0, 0, 1]); v
}
#[test]
fn v4_dest_unreach_extracts_tcp_inner() {
use std::net::IpAddr;
use flowscope::extractor::L4Proto;
let mut payload = v4_header(3, 3, [0, 0, 0, 0]); payload.extend(ipv4_tcp_inner([10, 0, 0, 1], [10, 0, 0, 2], 12345, 80));
let msg = parse_v4(&payload).expect("parses");
let IcmpType::V4(Icmpv4Type::DestinationUnreachable {
code,
inner: Some(inner),
}) = msg.ty
else {
panic!("expected DestinationUnreachable with inner");
};
assert_eq!(code, Icmpv4DestUnreachCode::Port);
assert_eq!(inner.src, IpAddr::V4("10.0.0.1".parse().unwrap()));
assert_eq!(inner.dst, IpAddr::V4("10.0.0.2".parse().unwrap()));
assert_eq!(inner.proto, L4Proto::Tcp);
assert_eq!(inner.src_port, Some(12345));
assert_eq!(inner.dst_port, Some(80));
}
fn ipv4_udp_inner(src: [u8; 4], dst: [u8; 4], sport: u16, dport: u16) -> Vec<u8> {
let mut v = Vec::new();
v.push(0x45);
v.push(0);
v.extend_from_slice(&[0x00, 0x1C]); v.extend_from_slice(&[0, 0]);
v.extend_from_slice(&[0, 0]);
v.push(64);
v.push(17); v.extend_from_slice(&[0, 0]);
v.extend_from_slice(&src);
v.extend_from_slice(&dst);
v.extend_from_slice(&sport.to_be_bytes());
v.extend_from_slice(&dport.to_be_bytes());
v.extend_from_slice(&[0, 8, 0, 0]); v
}
#[test]
fn v4_dest_unreach_extracts_udp_inner() {
use flowscope::extractor::L4Proto;
let mut payload = v4_header(3, 3, [0, 0, 0, 0]);
payload.extend(ipv4_udp_inner([1, 2, 3, 4], [5, 6, 7, 8], 53000, 53));
let msg = parse_v4(&payload).expect("parses");
let IcmpType::V4(Icmpv4Type::DestinationUnreachable {
inner: Some(inner), ..
}) = msg.ty
else {
panic!("expected inner");
};
assert_eq!(inner.proto, L4Proto::Udp);
assert_eq!(inner.src_port, Some(53000));
assert_eq!(inner.dst_port, Some(53));
}
#[test]
fn v4_dest_unreach_truncated_inner_returns_none_ports() {
let mut payload = v4_header(3, 0, [0, 0, 0, 0]);
let mut inner = Vec::new();
inner.push(0x45);
inner.push(0);
inner.extend_from_slice(&[0, 22]); inner.extend_from_slice(&[0, 0]);
inner.extend_from_slice(&[0, 0]);
inner.push(64);
inner.push(6); inner.extend_from_slice(&[0, 0]);
inner.extend_from_slice(&[10, 0, 0, 1]);
inner.extend_from_slice(&[10, 0, 0, 2]);
inner.extend_from_slice(&[0x12, 0x34]); payload.extend(&inner);
let msg = parse_v4(&payload).expect("parses");
let IcmpType::V4(Icmpv4Type::DestinationUnreachable {
inner: Some(inner), ..
}) = msg.ty
else {
panic!("expected inner with ip data");
};
assert_eq!(inner.src_port, None);
assert_eq!(inner.dst_port, None);
}
#[test]
fn parses_v4_time_exceeded_with_inner() {
let mut payload = v4_header(11, 0, [0, 0, 0, 0]); payload.extend(ipv4_tcp_inner([10, 0, 0, 1], [10, 0, 0, 2], 1234, 80));
let msg = parse_v4(&payload).expect("parses");
let IcmpType::V4(Icmpv4Type::TimeExceeded {
code,
inner: Some(_),
}) = msg.ty
else {
panic!("expected TimeExceeded");
};
assert_eq!(code, Icmpv4TimeExceededCode::HopLimitExceeded);
}
#[test]
fn parses_v6_time_exceeded() {
let payload = v6_header(3, 0, [0, 0, 0, 0]);
let msg = parse_v6(&payload).expect("parses");
let IcmpType::V6(Icmpv6Type::TimeExceeded { code, inner: _ }) = msg.ty else {
panic!("expected TimeExceeded");
};
assert_eq!(code, Icmpv6TimeExceededCode::HopLimitExceeded);
}
#[test]
fn parses_v4_redirect_with_gateway() {
use std::net::Ipv4Addr;
let payload = v4_header(5, 1, [192, 168, 1, 1]);
let msg = parse_v4(&payload).expect("parses");
let IcmpType::V4(Icmpv4Type::Redirect { code, gateway, .. }) = msg.ty else {
panic!("expected Redirect");
};
assert_eq!(code, Icmpv4RedirectCode::Host);
assert_eq!(gateway, Ipv4Addr::new(192, 168, 1, 1));
}
#[test]
fn parses_v6_packet_too_big_with_mtu() {
let mtu_be: [u8; 4] = 1500u32.to_be_bytes();
let payload = v6_header(2, 0, mtu_be);
let msg = parse_v6(&payload).expect("parses");
let IcmpType::V6(Icmpv6Type::PacketTooBig { mtu, .. }) = msg.ty else {
panic!("expected PacketTooBig");
};
assert_eq!(mtu, 1500);
}
#[test]
fn parses_v6_neighbor_solicitation() {
let mut payload = v6_header(135, 0, [0, 0, 0, 0]);
let target = Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1).octets();
payload.extend_from_slice(&target);
let msg = parse_v6(&payload).expect("parses");
let IcmpType::V6(Icmpv6Type::NeighborSolicitation { target }) = msg.ty else {
panic!("expected NeighborSolicitation");
};
assert_eq!(target, Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1));
}
#[test]
fn parses_v6_neighbor_advertisement_with_flags() {
let mut payload = v6_header(136, 0, [0xE0, 0, 0, 0]);
let target = Ipv6Addr::LOCALHOST.octets();
payload.extend_from_slice(&target);
let msg = parse_v6(&payload).expect("parses");
let IcmpType::V6(Icmpv6Type::NeighborAdvertisement {
target,
router,
solicited,
override_,
}) = msg.ty
else {
panic!("expected NeighborAdvertisement");
};
assert_eq!(target, Ipv6Addr::LOCALHOST);
assert!(router);
assert!(solicited);
assert!(override_);
}
#[test]
fn unknown_v4_type_surfaces_as_other() {
let payload = v4_header(42, 7, [0x11, 0x22, 0x33, 0x44]);
let msg = parse_v4(&payload).expect("parses");
let IcmpType::V4(Icmpv4Type::Other {
raw_type,
raw_code,
raw_body,
}) = msg.ty
else {
panic!("expected Other");
};
assert_eq!(raw_type, 42);
assert_eq!(raw_code, 7);
assert!(raw_body.starts_with(&[0x11, 0x22, 0x33, 0x44]));
}
#[test]
fn datagram_parser_returns_one_message_per_call() {
use flowscope::{DatagramParser, FlowSide, Timestamp};
let mut p = IcmpParser::new();
let payload = v4_header(8, 0, [0, 1, 0, 1]);
let mut msgs = Vec::new();
p.parse(
&payload,
FlowSide::Initiator,
Timestamp::default(),
&mut msgs,
);
assert_eq!(msgs.len(), 1);
assert!(matches!(
msgs[0].ty,
IcmpType::V4(Icmpv4Type::EchoRequest { id: 1, seq: 1 })
));
}
#[test]
fn datagram_parser_kind_is_icmp() {
use flowscope::DatagramParser;
let p = IcmpParser::new();
assert_eq!(p.parser_kind().as_str(), "icmp");
}
#[test]
fn datagram_parser_v6_only_skips_v4() {
use flowscope::{DatagramParser, FlowSide, Timestamp};
let mut p = IcmpParser::new().v6_only();
let payload = v4_header(8, 0, [0, 1, 0, 1]);
let mut msgs = Vec::new();
p.parse(
&payload,
FlowSide::Initiator,
Timestamp::default(),
&mut msgs,
);
assert!(msgs.is_empty() || matches!(msgs[0].family, IcmpFamily::V6));
}
fn icmp_error(ty: u8, code: u8, inner_version: u8) -> Vec<u8> {
let mut v = vec![ty, code, 0, 0, 0, 0, 0, 0];
if inner_version == 6 {
v.extend_from_slice(&[0x60, 0, 0, 0, 0, 0, 59, 64]);
v.extend_from_slice(&[0u8; 32]);
} else {
v.extend_from_slice(&[
0x45, 0, 0, 0x1c, 0, 0, 0, 0, 64, 6, 0, 0, 10, 0, 0, 1, 10, 0, 0, 2,
]);
v.extend_from_slice(&[0x30, 0x39, 0, 0x50]); }
v
}
#[test]
fn datagram_parser_v4_host_unreachable() {
use flowscope::{DatagramParser, FlowSide, Timestamp};
let mut p = IcmpParser::new();
let mut msgs = Vec::new();
p.parse(
&icmp_error(3, 1, 4),
FlowSide::Initiator,
Timestamp::default(),
&mut msgs,
);
assert_eq!(msgs.len(), 1);
assert!(matches!(msgs[0].family, IcmpFamily::V4));
assert_eq!(
msgs[0].ty.dest_unreachable_kind(),
Some(DestUnreachableKind::Host),
);
}
#[test]
fn datagram_parser_v6_address_unreachable_not_misread_as_v4() {
use flowscope::{DatagramParser, FlowSide, Timestamp};
let mut p = IcmpParser::new();
let mut msgs = Vec::new();
p.parse(
&icmp_error(1, 3, 6),
FlowSide::Initiator,
Timestamp::default(),
&mut msgs,
);
assert_eq!(msgs.len(), 1);
assert!(matches!(msgs[0].family, IcmpFamily::V6), "must be v6");
assert!(msgs[0].ty.is_error());
assert_eq!(
msgs[0].ty.dest_unreachable_kind(),
Some(DestUnreachableKind::Host),
);
}
#[test]
fn datagram_parser_v6_time_exceeded_not_misread_as_v4_dest_unreach() {
use flowscope::{DatagramParser, FlowSide, Timestamp};
let mut p = IcmpParser::new();
let mut msgs = Vec::new();
p.parse(
&icmp_error(3, 0, 6),
FlowSide::Initiator,
Timestamp::default(),
&mut msgs,
);
assert_eq!(msgs.len(), 1);
assert!(matches!(msgs[0].family, IcmpFamily::V6));
assert!(matches!(
msgs[0].ty,
IcmpType::V6(Icmpv6Type::TimeExceeded { .. })
));
assert_eq!(msgs[0].ty.dest_unreachable_kind(), None);
}
#[test]
fn empty_payload_returns_err() {
assert!(parse_v4(&[]).is_err());
assert!(parse_v6(&[]).is_err());
}
#[test]
fn malformed_short_header_does_not_panic() {
let _ = parse_v4(&[1, 2, 3]);
let _ = parse_v6(&[1, 2, 3]);
}
#[test]
fn is_error_returns_true_for_error_variants() {
let p = v4_header(3, 0, [0, 0, 0, 0]); assert!(parse_v4(&p).unwrap().ty.is_error());
let p = v4_header(11, 0, [0, 0, 0, 0]); assert!(parse_v4(&p).unwrap().ty.is_error());
let p = v4_header(5, 0, [1, 2, 3, 4]); assert!(parse_v4(&p).unwrap().ty.is_error());
let p = v6_header(1, 0, [0, 0, 0, 0]); assert!(parse_v6(&p).unwrap().ty.is_error());
let p = v6_header(2, 0, [0, 0, 5, 220]); assert!(parse_v6(&p).unwrap().ty.is_error());
let p = v6_header(3, 0, [0, 0, 0, 0]); assert!(parse_v6(&p).unwrap().ty.is_error());
}
#[test]
fn is_error_returns_false_for_non_error_variants() {
let p = v4_header(8, 0, [0, 1, 0, 1]); assert!(!parse_v4(&p).unwrap().ty.is_error());
let p = v4_header(0, 0, [0, 1, 0, 1]); assert!(!parse_v4(&p).unwrap().ty.is_error());
let p = v6_header(128, 0, [0, 1, 0, 1]); assert!(!parse_v6(&p).unwrap().ty.is_error());
let mut p = v6_header(135, 0, [0, 0, 0, 0]);
p.extend_from_slice(&[0u8; 16]);
assert!(!parse_v6(&p).unwrap().ty.is_error());
}
#[test]
fn error_inner_returns_label_and_inner() {
let mut payload = v4_header(3, 3, [0, 0, 0, 0]);
payload.extend(ipv4_tcp_inner([10, 0, 0, 1], [10, 0, 0, 2], 12345, 80));
let msg = parse_v4(&payload).unwrap();
let (label, inner) = msg.ty.error_inner().expect("inner");
assert_eq!(label, "dest_unreachable");
assert_eq!(inner.src_port, Some(12345));
assert_eq!(inner.dst_port, Some(80));
}
#[test]
fn error_inner_returns_none_for_non_error() {
let p = v4_header(8, 0, [0, 1, 0, 1]); assert!(parse_v4(&p).unwrap().ty.error_inner().is_none());
}
#[test]
fn error_inner_returns_none_for_truncated() {
let payload = v4_header(3, 0, [0, 0, 0, 0]); let msg = parse_v4(&payload).unwrap();
assert!(msg.ty.error_inner().is_none());
}
#[test]
fn short_kind_table() {
assert_eq!(
parse_v4(&v4_header(8, 0, [0, 1, 0, 1]))
.unwrap()
.short_kind(),
"echo_request"
);
assert_eq!(
parse_v4(&v4_header(0, 0, [0, 1, 0, 1]))
.unwrap()
.short_kind(),
"echo_reply"
);
assert_eq!(
parse_v4(&v4_header(3, 3, [0, 0, 0, 0]))
.unwrap()
.short_kind(),
"dest_unreachable"
);
assert_eq!(
parse_v4(&v4_header(5, 1, [1, 2, 3, 4]))
.unwrap()
.short_kind(),
"redirect"
);
assert_eq!(
parse_v4(&v4_header(11, 0, [0, 0, 0, 0]))
.unwrap()
.short_kind(),
"time_exceeded"
);
assert_eq!(
parse_v6(&v6_header(128, 0, [0, 1, 0, 1]))
.unwrap()
.short_kind(),
"echo_request"
);
assert_eq!(
parse_v6(&v6_header(2, 0, [0, 0, 5, 220]))
.unwrap()
.short_kind(),
"packet_too_big"
);
let mut ns = v6_header(135, 0, [0, 0, 0, 0]);
ns.extend_from_slice(&[0u8; 16]);
assert_eq!(parse_v6(&ns).unwrap().short_kind(), "neighbor_solicitation");
}
#[test]
fn short_kind_v4_v6_share_slug_for_shared_semantics() {
let v4_echo = parse_v4(&v4_header(8, 0, [0, 1, 0, 1])).unwrap();
let v6_echo = parse_v6(&v6_header(128, 0, [0, 1, 0, 1])).unwrap();
assert_eq!(v4_echo.short_kind(), v6_echo.short_kind());
assert_ne!(v4_echo.family, v6_echo.family);
}
#[test]
fn short_kind_is_static_str() {
let m = parse_v4(&v4_header(8, 0, [0, 1, 0, 1])).unwrap();
let _s: &'static str = m.ty.short_kind();
let _s: &'static str = m.short_kind();
}
#[test]
fn message_helpers_mirror_type_helpers() {
let mut payload = v4_header(3, 3, [0, 0, 0, 0]);
payload.extend(ipv4_tcp_inner([10, 0, 0, 1], [10, 0, 0, 2], 1, 2));
let msg = parse_v4(&payload).unwrap();
assert_eq!(msg.is_error(), msg.ty.is_error());
assert_eq!(msg.short_kind(), msg.ty.short_kind());
assert_eq!(msg.error_inner().is_some(), msg.ty.error_inner().is_some());
}