use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::os::fd::FromRawFd;
use std::sync::Arc;
use smoltcp::wire::{
EthernetAddress, EthernetFrame, EthernetProtocol, EthernetRepr, Icmpv4Packet, Icmpv4Repr,
Icmpv6Packet, Icmpv6Repr, IpProtocol, Ipv4Packet, Ipv4Repr, Ipv6Packet, Ipv6Repr,
};
use crate::policy::{NetworkPolicy, Protocol};
use crate::shared::SharedState;
use crate::stack::PollLoopConfig;
const ECHO_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
const RECV_BUF_SIZE: usize = 1500;
const ETH_HDR_LEN: usize = 14;
const IPV4_HDR_LEN: usize = 20;
const IPV6_HDR_LEN: usize = 40;
#[derive(Debug, Clone, Copy)]
enum EchoBackend {
Available,
Unavailable,
}
pub struct IcmpRelay {
shared: Arc<SharedState>,
gateway_mac: EthernetAddress,
guest_mac: EthernetAddress,
tokio_handle: tokio::runtime::Handle,
backend_v4: EchoBackend,
backend_v6: EchoBackend,
}
impl IcmpRelay {
pub fn new(
shared: Arc<SharedState>,
gateway_mac: [u8; 6],
guest_mac: [u8; 6],
tokio_handle: tokio::runtime::Handle,
) -> Self {
let backend_v4 = probe_icmp_socket_v4();
let backend_v6 = probe_icmp_socket_v6();
if matches!(backend_v4, EchoBackend::Unavailable) {
tracing::debug!(
"unprivileged ICMPv4 echo sockets unavailable — external ICMPv4 relay disabled"
);
}
if matches!(backend_v6, EchoBackend::Unavailable) {
tracing::debug!(
"unprivileged ICMPv6 echo sockets unavailable — external ICMPv6 relay disabled"
);
}
Self {
shared,
gateway_mac: EthernetAddress(gateway_mac),
guest_mac: EthernetAddress(guest_mac),
tokio_handle,
backend_v4,
backend_v6,
}
}
pub fn relay_outbound_if_echo(
&self,
frame: &[u8],
config: &PollLoopConfig,
policy: &NetworkPolicy,
) -> bool {
let Ok(eth) = EthernetFrame::new_checked(frame) else {
return false;
};
match eth.ethertype() {
EthernetProtocol::Ipv4 if matches!(self.backend_v4, EchoBackend::Available) => {
self.try_relay_icmpv4(ð, config, policy)
}
EthernetProtocol::Ipv6 if matches!(self.backend_v6, EchoBackend::Available) => {
self.try_relay_icmpv6(ð, config, policy)
}
_ => false,
}
}
}
impl IcmpRelay {
fn try_relay_icmpv4(
&self,
eth: &EthernetFrame<&[u8]>,
config: &PollLoopConfig,
policy: &NetworkPolicy,
) -> bool {
let Ok(ipv4) = Ipv4Packet::new_checked(eth.payload()) else {
return false;
};
if ipv4.next_header() != IpProtocol::Icmp {
return false;
}
let dst_ip: Ipv4Addr = ipv4.dst_addr();
if dst_ip == config.gateway_ipv4 {
return false;
}
let Ok(icmp) = Icmpv4Packet::new_checked(ipv4.payload()) else {
return false;
};
let Ok(Icmpv4Repr::EchoRequest {
ident,
seq_no,
data,
}) = Icmpv4Repr::parse(&icmp, &smoltcp::phy::ChecksumCapabilities::default())
else {
return false; };
if policy
.evaluate_egress_ip(IpAddr::V4(dst_ip), Protocol::Icmpv4)
.is_deny()
{
tracing::debug!(dst = %dst_ip, "ICMP echo denied by policy");
return true; }
let src_ip: Ipv4Addr = ipv4.src_addr();
let guest_ident = ident;
let echo_data = data.to_vec();
let shared = self.shared.clone();
let gateway_mac = self.gateway_mac;
let guest_mac = self.guest_mac;
tracing::debug!(dst = %dst_ip, seq_no, bytes = echo_data.len(), "relaying ICMPv4 echo request");
self.tokio_handle.spawn(async move {
if let Err(e) = icmpv4_echo_task(
dst_ip,
src_ip,
guest_ident,
seq_no,
echo_data,
shared,
gateway_mac,
guest_mac,
)
.await
{
tracing::debug!(dst = %dst_ip, error = %e, "ICMPv4 echo relay failed");
}
});
true
}
fn try_relay_icmpv6(
&self,
eth: &EthernetFrame<&[u8]>,
config: &PollLoopConfig,
policy: &NetworkPolicy,
) -> bool {
let Ok(ipv6) = Ipv6Packet::new_checked(eth.payload()) else {
return false;
};
if ipv6.next_header() != IpProtocol::Icmpv6 {
return false;
}
let dst_ip: Ipv6Addr = ipv6.dst_addr();
if dst_ip == config.gateway_ipv6 {
return false;
}
let Ok(icmp) = Icmpv6Packet::new_checked(ipv6.payload()) else {
return false;
};
let Ok(Icmpv6Repr::EchoRequest {
ident,
seq_no,
data,
}) = Icmpv6Repr::parse(
&ipv6.src_addr(),
&ipv6.dst_addr(),
&icmp,
&smoltcp::phy::ChecksumCapabilities::default(),
)
else {
return false; };
if policy
.evaluate_egress_ip(IpAddr::V6(dst_ip), Protocol::Icmpv6)
.is_deny()
{
tracing::debug!(dst = %dst_ip, "ICMPv6 echo denied by policy");
return true;
}
let src_ip: Ipv6Addr = ipv6.src_addr();
let guest_ident = ident;
let echo_data = data.to_vec();
let shared = self.shared.clone();
let gateway_mac = self.gateway_mac;
let guest_mac = self.guest_mac;
tracing::debug!(dst = %dst_ip, seq_no, bytes = echo_data.len(), "relaying ICMPv6 echo request");
self.tokio_handle.spawn(async move {
if let Err(e) = icmpv6_echo_task(
dst_ip,
src_ip,
guest_ident,
seq_no,
echo_data,
shared,
gateway_mac,
guest_mac,
)
.await
{
tracing::debug!(dst = %dst_ip, error = %e, "ICMPv6 echo relay failed");
}
});
true
}
}
fn probe_icmp_socket_v4() -> EchoBackend {
let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, libc::IPPROTO_ICMP) };
if fd >= 0 {
unsafe { libc::close(fd) };
EchoBackend::Available
} else {
EchoBackend::Unavailable
}
}
fn probe_icmp_socket_v6() -> EchoBackend {
let fd = unsafe { libc::socket(libc::AF_INET6, libc::SOCK_DGRAM, libc::IPPROTO_ICMPV6) };
if fd >= 0 {
unsafe { libc::close(fd) };
EchoBackend::Available
} else {
EchoBackend::Unavailable
}
}
fn open_icmp_socket_v4(dst: Ipv4Addr) -> std::io::Result<tokio::net::UdpSocket> {
let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, libc::IPPROTO_ICMP) };
if fd < 0 {
return Err(std::io::Error::last_os_error());
}
if let Err(e) = set_nonblock_cloexec(fd) {
unsafe { libc::close(fd) };
return Err(e);
}
let addr = libc::sockaddr_in {
sin_family: libc::AF_INET as libc::sa_family_t,
sin_port: 0,
sin_addr: libc::in_addr {
s_addr: u32::from(dst).to_be(),
},
sin_zero: [0; 8],
#[cfg(target_os = "macos")]
sin_len: std::mem::size_of::<libc::sockaddr_in>() as u8,
};
let ret = unsafe {
libc::connect(
fd,
&addr as *const libc::sockaddr_in as *const libc::sockaddr,
std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
)
};
if ret < 0 {
let err = std::io::Error::last_os_error();
unsafe { libc::close(fd) };
return Err(err);
}
let std_sock = unsafe { std::net::UdpSocket::from_raw_fd(fd) };
tokio::net::UdpSocket::from_std(std_sock)
}
fn open_icmp_socket_v6(dst: Ipv6Addr) -> std::io::Result<tokio::net::UdpSocket> {
let fd = unsafe { libc::socket(libc::AF_INET6, libc::SOCK_DGRAM, libc::IPPROTO_ICMPV6) };
if fd < 0 {
return Err(std::io::Error::last_os_error());
}
if let Err(e) = set_nonblock_cloexec(fd) {
unsafe { libc::close(fd) };
return Err(e);
}
let addr = libc::sockaddr_in6 {
sin6_family: libc::AF_INET6 as libc::sa_family_t,
sin6_port: 0,
sin6_flowinfo: 0,
sin6_addr: libc::in6_addr {
s6_addr: dst.octets(),
},
sin6_scope_id: 0,
#[cfg(target_os = "macos")]
sin6_len: std::mem::size_of::<libc::sockaddr_in6>() as u8,
};
let ret = unsafe {
libc::connect(
fd,
&addr as *const libc::sockaddr_in6 as *const libc::sockaddr,
std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t,
)
};
if ret < 0 {
let err = std::io::Error::last_os_error();
unsafe { libc::close(fd) };
return Err(err);
}
let std_sock = unsafe { std::net::UdpSocket::from_raw_fd(fd) };
tokio::net::UdpSocket::from_std(std_sock)
}
fn set_nonblock_cloexec(fd: libc::c_int) -> std::io::Result<()> {
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFL);
if flags < 0 {
return Err(std::io::Error::last_os_error());
}
if libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
return Err(std::io::Error::last_os_error());
}
let flags = libc::fcntl(fd, libc::F_GETFD);
if flags < 0 {
return Err(std::io::Error::last_os_error());
}
if libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) < 0 {
return Err(std::io::Error::last_os_error());
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn icmpv4_echo_task(
dst_ip: Ipv4Addr,
guest_src_ip: Ipv4Addr,
guest_ident: u16,
seq_no: u16,
echo_data: Vec<u8>,
shared: Arc<SharedState>,
gateway_mac: EthernetAddress,
guest_mac: EthernetAddress,
) -> std::io::Result<()> {
let socket = open_icmp_socket_v4(dst_ip)?;
let icmp_repr = Icmpv4Repr::EchoRequest {
ident: guest_ident,
seq_no,
data: &echo_data,
};
let mut icmp_buf = vec![0u8; icmp_repr.buffer_len()];
icmp_repr.emit(
&mut Icmpv4Packet::new_unchecked(&mut icmp_buf),
&smoltcp::phy::ChecksumCapabilities::default(),
);
socket.send(&icmp_buf).await?;
let mut recv_buf = vec![0u8; RECV_BUF_SIZE];
let n = tokio::time::timeout(ECHO_TIMEOUT, socket.recv(&mut recv_buf))
.await
.map_err(|_| std::io::Error::new(std::io::ErrorKind::TimedOut, "ICMP echo timeout"))??;
let (reply_seq, reply_data) = parse_icmpv4_echo_reply(&recv_buf[..n])?;
let frame = construct_icmpv4_echo_reply(
dst_ip,
guest_src_ip,
guest_ident,
reply_seq,
reply_data,
gateway_mac,
guest_mac,
);
let frame_len = frame.len();
if shared.rx_ring.push(frame).is_ok() {
shared.add_rx_bytes(frame_len);
shared.rx_wake.wake();
tracing::debug!(dst = %dst_ip, seq_no = reply_seq, frame_len, "ICMPv4 echo reply injected");
} else {
tracing::debug!("ICMP echo reply dropped — rx_ring full");
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn icmpv6_echo_task(
dst_ip: Ipv6Addr,
guest_src_ip: Ipv6Addr,
guest_ident: u16,
seq_no: u16,
echo_data: Vec<u8>,
shared: Arc<SharedState>,
gateway_mac: EthernetAddress,
guest_mac: EthernetAddress,
) -> std::io::Result<()> {
let socket = open_icmp_socket_v6(dst_ip)?;
let icmp_repr = Icmpv6Repr::EchoRequest {
ident: guest_ident,
seq_no,
data: &echo_data,
};
let mut icmp_buf = vec![0u8; icmp_repr.buffer_len()];
icmp_repr.emit(
&guest_src_ip,
&dst_ip,
&mut Icmpv6Packet::new_unchecked(&mut icmp_buf),
&smoltcp::phy::ChecksumCapabilities::default(),
);
socket.send(&icmp_buf).await?;
let mut recv_buf = vec![0u8; RECV_BUF_SIZE];
let n = tokio::time::timeout(ECHO_TIMEOUT, socket.recv(&mut recv_buf))
.await
.map_err(|_| std::io::Error::new(std::io::ErrorKind::TimedOut, "ICMPv6 echo timeout"))??;
let (reply_seq, reply_data) = parse_icmpv6_echo_reply(&recv_buf[..n], dst_ip, guest_src_ip)?;
let frame = construct_icmpv6_echo_reply(
dst_ip,
guest_src_ip,
guest_ident,
reply_seq,
reply_data,
gateway_mac,
guest_mac,
);
let frame_len = frame.len();
if shared.rx_ring.push(frame).is_ok() {
shared.add_rx_bytes(frame_len);
shared.rx_wake.wake();
tracing::debug!(dst = %dst_ip, seq_no = reply_seq, frame_len, "ICMPv6 echo reply injected");
} else {
tracing::debug!("ICMPv6 echo reply dropped — rx_ring full");
}
Ok(())
}
fn parse_icmpv4_echo_reply(buf: &[u8]) -> std::io::Result<(u16, &[u8])> {
if let Ok(reply_icmp) = Icmpv4Packet::new_checked(buf)
&& let Ok(Icmpv4Repr::EchoReply {
ident: _,
seq_no,
data,
}) = Icmpv4Repr::parse(&reply_icmp, &smoltcp::phy::ChecksumCapabilities::default())
{
return Ok((seq_no, data));
}
let reply_icmp = Icmpv4Packet::new_checked(extract_ipv4_icmp_payload(buf)?)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
let Icmpv4Repr::EchoReply {
ident: _,
seq_no,
data,
} = Icmpv4Repr::parse(&reply_icmp, &smoltcp::phy::ChecksumCapabilities::default())
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?
else {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"host ICMPv4 reply was not an echo reply",
));
};
Ok((seq_no, data))
}
fn parse_icmpv6_echo_reply(
buf: &[u8],
remote_ip: Ipv6Addr,
guest_ip: Ipv6Addr,
) -> std::io::Result<(u16, &[u8])> {
if let Ok(reply_icmp) = Icmpv6Packet::new_checked(buf)
&& let Ok(Icmpv6Repr::EchoReply {
ident: _,
seq_no,
data,
}) = Icmpv6Repr::parse(
&remote_ip,
&guest_ip,
&reply_icmp,
&smoltcp::phy::ChecksumCapabilities::default(),
)
{
return Ok((seq_no, data));
}
let reply_icmp = Icmpv6Packet::new_checked(extract_ipv6_icmp_payload(buf)?)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
let Icmpv6Repr::EchoReply {
ident: _,
seq_no,
data,
} = Icmpv6Repr::parse(
&remote_ip,
&guest_ip,
&reply_icmp,
&smoltcp::phy::ChecksumCapabilities::default(),
)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?
else {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"host ICMPv6 reply was not an echo reply",
));
};
Ok((seq_no, data))
}
fn extract_ipv4_icmp_payload(buf: &[u8]) -> std::io::Result<&[u8]> {
if buf.len() < IPV4_HDR_LEN {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"host ICMPv4 reply was shorter than an IPv4 header",
));
}
let version = buf[0] >> 4;
let header_len = usize::from(buf[0] & 0x0f) * 4;
if version != 4 || header_len < IPV4_HDR_LEN || header_len > buf.len() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"host ICMPv4 reply did not contain a usable IPv4 header",
));
}
if buf[9] != IpProtocol::Icmp.into() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"host ICMPv4 reply did not contain an ICMP payload",
));
}
Ok(&buf[header_len..])
}
fn extract_ipv6_icmp_payload(buf: &[u8]) -> std::io::Result<&[u8]> {
if buf.len() < IPV6_HDR_LEN {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"host ICMPv6 reply was shorter than an IPv6 header",
));
}
let version = buf[0] >> 4;
if version != 6 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"host ICMPv6 reply did not contain a usable IPv6 header",
));
}
if buf[6] != IpProtocol::Icmpv6.into() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"host ICMPv6 reply did not contain an ICMPv6 payload",
));
}
Ok(&buf[IPV6_HDR_LEN..])
}
fn construct_icmpv4_echo_reply(
src_ip: Ipv4Addr,
dst_ip: Ipv4Addr,
ident: u16,
seq_no: u16,
data: &[u8],
gateway_mac: EthernetAddress,
guest_mac: EthernetAddress,
) -> Vec<u8> {
let icmp_repr = Icmpv4Repr::EchoReply {
ident,
seq_no,
data,
};
let ipv4_repr = Ipv4Repr {
src_addr: src_ip,
dst_addr: dst_ip,
next_header: IpProtocol::Icmp,
payload_len: icmp_repr.buffer_len(),
hop_limit: 64,
};
let frame_len = ETH_HDR_LEN + ipv4_repr.buffer_len() + icmp_repr.buffer_len();
let mut buf = vec![0u8; frame_len];
let mut eth_frame = EthernetFrame::new_unchecked(&mut buf);
EthernetRepr {
src_addr: gateway_mac,
dst_addr: guest_mac,
ethertype: EthernetProtocol::Ipv4,
}
.emit(&mut eth_frame);
ipv4_repr.emit(
&mut Ipv4Packet::new_unchecked(&mut buf[ETH_HDR_LEN..ETH_HDR_LEN + IPV4_HDR_LEN]),
&smoltcp::phy::ChecksumCapabilities::default(),
);
icmp_repr.emit(
&mut Icmpv4Packet::new_unchecked(&mut buf[ETH_HDR_LEN + IPV4_HDR_LEN..]),
&smoltcp::phy::ChecksumCapabilities::default(),
);
buf
}
fn construct_icmpv6_echo_reply(
src_ip: Ipv6Addr,
dst_ip: Ipv6Addr,
ident: u16,
seq_no: u16,
data: &[u8],
gateway_mac: EthernetAddress,
guest_mac: EthernetAddress,
) -> Vec<u8> {
let icmp_repr = Icmpv6Repr::EchoReply {
ident,
seq_no,
data,
};
let frame_len = ETH_HDR_LEN + IPV6_HDR_LEN + icmp_repr.buffer_len();
let mut buf = vec![0u8; frame_len];
let mut eth_frame = EthernetFrame::new_unchecked(&mut buf);
EthernetRepr {
src_addr: gateway_mac,
dst_addr: guest_mac,
ethertype: EthernetProtocol::Ipv6,
}
.emit(&mut eth_frame);
Ipv6Repr {
src_addr: src_ip,
dst_addr: dst_ip,
next_header: IpProtocol::Icmpv6,
payload_len: icmp_repr.buffer_len(),
hop_limit: 64,
}
.emit(&mut Ipv6Packet::new_unchecked(
&mut buf[ETH_HDR_LEN..ETH_HDR_LEN + IPV6_HDR_LEN],
));
icmp_repr.emit(
&src_ip,
&dst_ip,
&mut Icmpv6Packet::new_unchecked(&mut buf[ETH_HDR_LEN + IPV6_HDR_LEN..]),
&smoltcp::phy::ChecksumCapabilities::default(),
);
buf
}
#[cfg(test)]
mod tests {
use super::*;
use smoltcp::phy::ChecksumCapabilities;
#[test]
fn construct_icmpv4_reply_roundtrips() {
let frame = construct_icmpv4_echo_reply(
Ipv4Addr::new(8, 8, 8, 8),
Ipv4Addr::new(100, 96, 0, 2),
0x1234,
0x0001,
b"hello",
EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
);
let eth = EthernetFrame::new_checked(&frame).unwrap();
assert_eq!(eth.ethertype(), EthernetProtocol::Ipv4);
assert_eq!(
eth.src_addr(),
EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01])
);
assert_eq!(
eth.dst_addr(),
EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02])
);
let ipv4 = Ipv4Packet::new_checked(eth.payload()).unwrap();
assert_eq!(Ipv4Addr::from(ipv4.src_addr()), Ipv4Addr::new(8, 8, 8, 8));
assert_eq!(
Ipv4Addr::from(ipv4.dst_addr()),
Ipv4Addr::new(100, 96, 0, 2)
);
assert_eq!(ipv4.next_header(), IpProtocol::Icmp);
let icmp = Icmpv4Packet::new_checked(ipv4.payload()).unwrap();
let repr = Icmpv4Repr::parse(&icmp, &ChecksumCapabilities::default()).unwrap();
assert_eq!(
repr,
Icmpv4Repr::EchoReply {
ident: 0x1234,
seq_no: 0x0001,
data: b"hello",
}
);
}
#[test]
fn construct_icmpv6_reply_roundtrips() {
let src: Ipv6Addr = "2001:db8::1".parse().unwrap();
let dst: Ipv6Addr = "fd42:6d73:62::2".parse().unwrap();
let frame = construct_icmpv6_echo_reply(
src,
dst,
0x5678,
0x0002,
b"v6ping",
EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
);
let eth = EthernetFrame::new_checked(&frame).unwrap();
assert_eq!(eth.ethertype(), EthernetProtocol::Ipv6);
let ipv6 = Ipv6Packet::new_checked(eth.payload()).unwrap();
assert_eq!(ipv6.next_header(), IpProtocol::Icmpv6);
let icmp = Icmpv6Packet::new_checked(ipv6.payload()).unwrap();
let repr = Icmpv6Repr::parse(
&src.into(),
&dst.into(),
&icmp,
&ChecksumCapabilities::default(),
)
.unwrap();
assert_eq!(
repr,
Icmpv6Repr::EchoReply {
ident: 0x5678,
seq_no: 0x0002,
data: b"v6ping",
}
);
assert_ne!(icmp.checksum(), 0, "ICMPv6 checksum must not be zero");
assert!(
icmp.verify_checksum(
&smoltcp::wire::Ipv6Address::from(src),
&smoltcp::wire::Ipv6Address::from(dst),
),
"ICMPv6 checksum must be valid"
);
}
#[test]
fn construct_icmpv4_reply_preserves_ident_and_seqno() {
let frame = construct_icmpv4_echo_reply(
Ipv4Addr::new(1, 2, 3, 4),
Ipv4Addr::new(10, 0, 0, 2),
0xABCD,
0xEF01,
b"test-payload",
EthernetAddress([0; 6]),
EthernetAddress([0; 6]),
);
let eth = EthernetFrame::new_checked(&frame).unwrap();
let ipv4 = Ipv4Packet::new_checked(eth.payload()).unwrap();
let icmp = Icmpv4Packet::new_checked(ipv4.payload()).unwrap();
let repr = Icmpv4Repr::parse(&icmp, &ChecksumCapabilities::default()).unwrap();
assert_eq!(
repr,
Icmpv4Repr::EchoReply {
ident: 0xABCD,
seq_no: 0xEF01,
data: b"test-payload",
}
);
}
#[test]
fn construct_icmpv6_reply_preserves_ident_and_seqno() {
let src: Ipv6Addr = "2001:db8::1".parse().unwrap();
let dst: Ipv6Addr = "fd42:6d73:62::2".parse().unwrap();
let frame = construct_icmpv6_echo_reply(
src,
dst,
0xBEEF,
0xCAFE,
b"test6",
EthernetAddress([0; 6]),
EthernetAddress([0; 6]),
);
let eth = EthernetFrame::new_checked(&frame).unwrap();
let ipv6 = Ipv6Packet::new_checked(eth.payload()).unwrap();
let icmp = Icmpv6Packet::new_checked(ipv6.payload()).unwrap();
let repr = Icmpv6Repr::parse(
&src.into(),
&dst.into(),
&icmp,
&ChecksumCapabilities::default(),
)
.unwrap();
assert_eq!(
repr,
Icmpv6Repr::EchoReply {
ident: 0xBEEF,
seq_no: 0xCAFE,
data: b"test6",
}
);
}
#[test]
fn probe_does_not_panic() {
let _ = probe_icmp_socket_v4();
let _ = probe_icmp_socket_v6();
}
#[test]
fn parse_icmpv4_reply_accepts_bare_icmp() {
let icmp_repr = Icmpv4Repr::EchoReply {
ident: 0x1234,
seq_no: 0x0001,
data: b"hello",
};
let mut buf = vec![0u8; icmp_repr.buffer_len()];
icmp_repr.emit(
&mut Icmpv4Packet::new_unchecked(&mut buf),
&ChecksumCapabilities::default(),
);
let (seq_no, data) = parse_icmpv4_echo_reply(&buf).unwrap();
assert_eq!(seq_no, 0x0001);
assert_eq!(data, b"hello");
}
#[test]
fn parse_icmpv4_reply_accepts_ipv4_plus_icmp() {
let frame = construct_icmpv4_echo_reply(
Ipv4Addr::new(8, 8, 8, 8),
Ipv4Addr::new(100, 96, 0, 2),
0x1234,
0x0001,
b"hello",
EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
);
let eth = EthernetFrame::new_checked(&frame).unwrap();
let (seq_no, data) = parse_icmpv4_echo_reply(eth.payload()).unwrap();
assert_eq!(seq_no, 0x0001);
assert_eq!(data, b"hello");
}
#[test]
fn parse_icmpv4_reply_accepts_macos_ping_socket_shape() {
let buf = [
0x45, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0x01, 0x73, 0xef, 0x08, 0x08,
0x08, 0x08, 0xc0, 0xa8, 0x01, 0x35, 0x00, 0x00, 0xa9, 0xf8, 0x12, 0x34, 0x00, 0x01,
0x68, 0x65, 0x6c, 0x6c, 0x6f,
];
let (seq_no, data) = parse_icmpv4_echo_reply(&buf).unwrap();
assert_eq!(seq_no, 0x0001);
assert_eq!(data, b"hello");
}
#[test]
fn parse_icmpv6_reply_accepts_bare_icmpv6() {
let src: Ipv6Addr = "2001:db8::1".parse().unwrap();
let dst: Ipv6Addr = "fd42:6d73:62::2".parse().unwrap();
let icmp_repr = Icmpv6Repr::EchoReply {
ident: 0x1234,
seq_no: 0x0002,
data: b"hello6",
};
let mut buf = vec![0u8; icmp_repr.buffer_len()];
icmp_repr.emit(
&src.into(),
&dst.into(),
&mut Icmpv6Packet::new_unchecked(&mut buf),
&ChecksumCapabilities::default(),
);
let (seq_no, data) = parse_icmpv6_echo_reply(&buf, src, dst).unwrap();
assert_eq!(seq_no, 0x0002);
assert_eq!(data, b"hello6");
}
#[test]
fn parse_icmpv6_reply_accepts_ipv6_plus_icmpv6() {
let src: Ipv6Addr = "2001:db8::1".parse().unwrap();
let dst: Ipv6Addr = "fd42:6d73:62::2".parse().unwrap();
let frame = construct_icmpv6_echo_reply(
src,
dst,
0x5678,
0x0002,
b"v6ping",
EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
);
let eth = EthernetFrame::new_checked(&frame).unwrap();
let (seq_no, data) = parse_icmpv6_echo_reply(eth.payload(), src, dst).unwrap();
assert_eq!(seq_no, 0x0002);
assert_eq!(data, b"v6ping");
}
}