use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::os::fd::AsRawFd;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use socket2::{Domain, Protocol, Type};
use tokio::io::unix::AsyncFd;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use arcbox_packet::ethernet::ETH_HEADER_LEN;
pub(super) struct IcmpProxy {
reply_tx: mpsc::Sender<Vec<u8>>,
gateway_mac: [u8; 6],
disabled: Arc<AtomicBool>,
permission_warned: Arc<AtomicBool>,
}
impl IcmpProxy {
pub(super) fn new(reply_tx: mpsc::Sender<Vec<u8>>, gateway_mac: [u8; 6]) -> Self {
Self {
reply_tx,
gateway_mac,
disabled: Arc::new(AtomicBool::new(false)),
permission_warned: Arc::new(AtomicBool::new(false)),
}
}
pub(super) fn proxy_icmp(&self, frame: &[u8], guest_mac: [u8; 6], cancel: CancellationToken) {
if self.disabled.load(Ordering::Relaxed) {
tracing::trace!("ICMP proxy: disabled, dropping frame");
return;
}
tracing::trace!("ICMP proxy: received frame ({} bytes)", frame.len());
let ip_start = ETH_HEADER_LEN;
let ihl = ((frame[ip_start] & 0x0F) as usize) * 4;
let icmp_start = ip_start + ihl;
if frame.len() < icmp_start + 8 {
return;
}
let src_ip = Ipv4Addr::new(
frame[ip_start + 12],
frame[ip_start + 13],
frame[ip_start + 14],
frame[ip_start + 15],
);
let dst_ip = Ipv4Addr::new(
frame[ip_start + 16],
frame[ip_start + 17],
frame[ip_start + 18],
frame[ip_start + 19],
);
let icmp_payload = frame[icmp_start..].to_vec();
let orig_icmp_id: [u8; 2] = if icmp_payload.len() >= 6 {
[icmp_payload[4], icmp_payload[5]]
} else {
[0, 0]
};
let reply_tx = self.reply_tx.clone();
let gateway_mac = self.gateway_mac;
let disabled = Arc::clone(&self.disabled);
let permission_warned = Arc::clone(&self.permission_warned);
tokio::spawn(async move {
let icmp_socket = match socket2::Socket::new(
Domain::IPV4,
Type::DGRAM,
Some(Protocol::ICMPV4),
) {
Ok(s) => s,
Err(e) => {
if e.kind() == std::io::ErrorKind::PermissionDenied {
disabled.store(true, Ordering::Relaxed);
if !permission_warned.swap(true, Ordering::Relaxed) {
tracing::warn!(
"ICMP proxy disabled: failed to create ICMP datagram socket: {}",
e
);
}
} else {
tracing::debug!(
"ICMP proxy: failed to create ICMP datagram socket (will retry): {}",
e
);
}
return;
}
};
icmp_socket.set_nonblocking(true).ok();
let dst_addr: SocketAddr = SocketAddrV4::new(dst_ip, 0).into();
match icmp_socket.send_to(&icmp_payload, &dst_addr.into()) {
Ok(_) => {}
Err(e) => {
tracing::warn!("ICMP proxy: sendto {} -> {} failed: {}", src_ip, dst_ip, e);
return;
}
}
let async_fd = match AsyncFd::new(RawSocketWrapper(icmp_socket)) {
Ok(fd) => fd,
Err(e) => {
tracing::warn!("ICMP proxy: AsyncFd failed: {}", e);
return;
}
};
let mut buf = vec![0u8; 65535];
let recv_fut = tokio::time::timeout(std::time::Duration::from_secs(10), async {
loop {
let readable = async_fd.readable().await;
match readable {
Ok(mut guard) => {
match guard.try_io(|inner| {
let n = unsafe {
libc::recv(
inner.get_ref().as_raw_fd(),
buf.as_mut_ptr().cast(),
buf.len(),
0,
)
};
if n < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(n as usize)
}
}) {
Ok(Ok(n)) if n > 0 => return Ok(n),
Ok(Err(e)) => return Err(e),
_ => {} }
}
Err(e) => return Err(e),
}
}
});
let recv = tokio::select! {
r = recv_fut => r,
() = cancel.cancelled() => return,
};
match recv {
Ok(Ok(n)) if n > 0 => {
let reply_packet = &buf[..n];
let mut icmp_data = if looks_like_ipv4_icmp(reply_packet) {
let ihl = ((reply_packet[0] & 0x0F) as usize) * 4;
reply_packet[ihl..].to_vec()
} else {
reply_packet.to_vec()
};
if icmp_data.len() >= 6 && icmp_data[0] == 0 {
icmp_data[4] = orig_icmp_id[0];
icmp_data[5] = orig_icmp_id[1];
}
let reply_frame = build_icmp_ipv4_ethernet(
dst_ip, src_ip, &icmp_data,
gateway_mac,
guest_mac,
);
if reply_tx.send(reply_frame).await.is_err() {
tracing::warn!("ICMP proxy: reply_tx channel closed");
}
}
_ => {
tracing::trace!(
"ICMP proxy: no reply (timeout/error) for {} -> {}",
src_ip,
dst_ip
);
}
}
});
}
}
struct RawSocketWrapper(socket2::Socket);
impl AsRawFd for RawSocketWrapper {
fn as_raw_fd(&self) -> std::os::fd::RawFd {
self.0.as_raw_fd()
}
}
fn looks_like_ipv4_icmp(packet: &[u8]) -> bool {
if packet.len() < 20 {
return false;
}
let version = packet[0] >> 4;
let ihl = (packet[0] & 0x0F) as usize * 4;
version == 4 && ihl >= 20 && packet.len() >= ihl && packet[9] == 1
}
fn build_icmp_ipv4_ethernet(
src_ip: Ipv4Addr,
dst_ip: Ipv4Addr,
icmp_payload: &[u8],
src_mac: [u8; 6],
dst_mac: [u8; 6],
) -> Vec<u8> {
let ip_total_len = 20 + icmp_payload.len();
let frame_len = ETH_HEADER_LEN + ip_total_len;
let mut frame = vec![0u8; frame_len];
frame[0..6].copy_from_slice(&dst_mac);
frame[6..12].copy_from_slice(&src_mac);
frame[12..14].copy_from_slice(&0x0800u16.to_be_bytes());
let ip_start = ETH_HEADER_LEN;
let ip = &mut frame[ip_start..ip_start + 20];
ip[0] = 0x45; ip[2..4].copy_from_slice(&(ip_total_len as u16).to_be_bytes());
ip[8] = 64; ip[9] = 1; ip[12..16].copy_from_slice(&src_ip.octets());
ip[16..20].copy_from_slice(&dst_ip.octets());
let ip_cksum = ipv4_checksum(ip);
ip[10..12].copy_from_slice(&ip_cksum.to_be_bytes());
let icmp_start = ETH_HEADER_LEN + 20;
frame[icmp_start..].copy_from_slice(icmp_payload);
if icmp_payload.len() >= 4 {
frame[icmp_start + 2] = 0;
frame[icmp_start + 3] = 0;
let icmp_cksum = internet_checksum(&frame[icmp_start..]);
frame[icmp_start + 2..icmp_start + 4].copy_from_slice(&icmp_cksum.to_be_bytes());
}
frame
}
fn internet_checksum(data: &[u8]) -> u16 {
let mut sum: u32 = 0;
let mut i = 0;
while i + 1 < data.len() {
sum += u32::from(u16::from_be_bytes([data[i], data[i + 1]]));
i += 2;
}
if i < data.len() {
sum += u32::from(data[i]) << 8;
}
while sum > 0xFFFF {
sum = (sum & 0xFFFF) + (sum >> 16);
}
!sum as u16
}
fn ipv4_checksum(header: &[u8]) -> u16 {
let mut sum: u32 = 0;
let mut i = 0;
while i + 1 < header.len() {
if i != 10 {
sum += u32::from(u16::from_be_bytes([header[i], header[i + 1]]));
}
i += 2;
}
while sum > 0xFFFF {
sum = (sum & 0xFFFF) + (sum >> 16);
}
!sum as u16
}
#[cfg(test)]
mod tests {
#[test]
fn test_icmp_identifier_restore() {
let orig_icmp_id: [u8; 2] = [0xAA, 0xBB];
let wrong_id: [u8; 2] = [0x00, 0x42];
let mut icmp_data: Vec<u8> = vec![
0x00, 0x00, 0x00,
0x00, wrong_id[0],
wrong_id[1], 0x00,
0x01, ];
if icmp_data.len() >= 6 && icmp_data[0] == 0 {
icmp_data[4] = orig_icmp_id[0];
icmp_data[5] = orig_icmp_id[1];
}
assert_eq!(
[icmp_data[4], icmp_data[5]],
orig_icmp_id,
"Echo Reply identifier should be restored to the original guest value"
);
let mut icmp_unreach: Vec<u8> = vec![
0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
let before = [icmp_unreach[4], icmp_unreach[5]];
if icmp_unreach.len() >= 6 && icmp_unreach[0] == 0 {
icmp_unreach[4] = orig_icmp_id[0];
icmp_unreach[5] = orig_icmp_id[1];
}
assert_eq!(
[icmp_unreach[4], icmp_unreach[5]],
before,
"Non-Echo ICMP types must not have their identifier bytes patched"
);
}
}