use std::collections::VecDeque;
use std::io;
use std::net::Ipv4Addr;
use std::os::fd::RawFd;
use smoltcp::phy::{Device, DeviceCapabilities, Medium, RxToken, TxToken};
use smoltcp::time::Instant;
use crate::ethernet::ETH_HEADER_LEN;
const ETHERNET_MTU: usize = 1500;
const MAX_FRAME_SIZE: usize = 65535;
const PROTO_ICMP: u8 = 1;
const PROTO_TCP: u8 = 6;
const PROTO_UDP: u8 = 17;
#[derive(Debug, Clone)]
pub struct TcpSynInfo {
pub dst_port: u16,
pub src_ip: Ipv4Addr,
pub src_port: u16,
pub dst_ip: Ipv4Addr,
pub syn_seq: u32,
pub frame: Vec<u8>,
}
#[derive(Debug)]
pub struct InterceptedFrame {
pub frame: Vec<u8>,
pub kind: InterceptedKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InterceptedKind {
Dhcp,
Dns,
Udp,
Icmp,
}
pub struct SmoltcpDevice {
fd: RawFd,
gateway_ip: Ipv4Addr,
rx_queue: VecDeque<Vec<u8>>,
tx_pending: Vec<Vec<u8>>,
intercepted: Vec<InterceptedFrame>,
gated_syns: Vec<TcpSynInfo>,
read_buf: Vec<u8>,
}
impl SmoltcpDevice {
pub fn new(fd: RawFd, gateway_ip: Ipv4Addr) -> Self {
Self {
fd,
gateway_ip,
rx_queue: VecDeque::new(),
tx_pending: Vec::new(),
intercepted: Vec::new(),
gated_syns: Vec::new(),
read_buf: vec![0u8; MAX_FRAME_SIZE],
}
}
pub fn drain_guest_fd(&mut self, guest_mac: &mut Option<[u8; 6]>) {
loop {
match fd_read(self.fd, &mut self.read_buf) {
Ok(n) if n > 0 => {
tracing::debug!("drain_guest_fd: read {n} bytes");
let frame = self.read_buf[..n].to_vec();
self.classify_frame(frame, guest_mac);
}
Ok(_) => break,
Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => {
tracing::warn!("Guest FD read error: {}", e);
break;
}
}
}
}
pub fn inject_rx(&mut self, frame: Vec<u8>) {
self.rx_queue.push_back(frame);
}
pub fn take_intercepted(&mut self) -> Vec<InterceptedFrame> {
std::mem::take(&mut self.intercepted)
}
pub fn take_tx_pending(&mut self) -> Vec<Vec<u8>> {
std::mem::take(&mut self.tx_pending)
}
pub fn take_gated_syns(&mut self) -> Vec<TcpSynInfo> {
std::mem::take(&mut self.gated_syns)
}
fn classify_frame(&mut self, frame: Vec<u8>, guest_mac: &mut Option<[u8; 6]>) {
if frame.len() < ETH_HEADER_LEN {
return;
}
let src_mac = [frame[6], frame[7], frame[8], frame[9], frame[10], frame[11]];
learn_guest_mac(src_mac, guest_mac);
let ethertype = u16::from_be_bytes([frame[12], frame[13]]);
match ethertype {
0x0806 => {
self.rx_queue.push_back(frame);
}
0x0800 => {
self.classify_ipv4(frame);
}
_ => {
tracing::debug!(
"Dropping frame with EtherType {:#06x}, len={}",
ethertype,
frame.len()
);
}
}
}
fn classify_ipv4(&mut self, frame: Vec<u8>) {
let ip_start = ETH_HEADER_LEN;
if frame.len() < ip_start + 20 {
return;
}
let protocol = frame[ip_start + 9];
let ihl = ((frame[ip_start] & 0x0F) as usize) * 4;
let l4_start = ip_start + ihl;
match protocol {
PROTO_TCP => {
if l4_start + 14 <= frame.len() {
let dst_port = u16::from_be_bytes([frame[l4_start + 2], frame[l4_start + 3]]);
let src_port = u16::from_be_bytes([frame[l4_start], frame[l4_start + 1]]);
let flags = frame[l4_start + 13];
let dst_ip = Ipv4Addr::new(
frame[ip_start + 16],
frame[ip_start + 17],
frame[ip_start + 18],
frame[ip_start + 19],
);
let src_ip = Ipv4Addr::new(
frame[ip_start + 12],
frame[ip_start + 13],
frame[ip_start + 14],
frame[ip_start + 15],
);
tracing::debug!(
"TCP frame: {src_ip}:{src_port} → {dst_ip}:{dst_port} flags={flags:#04x} len={}",
frame.len()
);
if flags & 0x02 != 0 && flags & 0x10 == 0 {
let syn_seq = u32::from_be_bytes([
frame[l4_start + 4],
frame[l4_start + 5],
frame[l4_start + 6],
frame[l4_start + 7],
]);
tracing::debug!("TCP SYN gated: {src_ip}:{src_port} → {dst_ip}:{dst_port}");
self.gated_syns.push(TcpSynInfo {
dst_port,
src_ip,
src_port,
dst_ip,
syn_seq,
frame,
});
return;
}
}
self.rx_queue.push_back(frame);
}
PROTO_UDP => {
if l4_start + 8 <= frame.len() {
let dst_port = u16::from_be_bytes([frame[l4_start + 2], frame[l4_start + 3]]);
let dst_ip = Ipv4Addr::new(
frame[ip_start + 16],
frame[ip_start + 17],
frame[ip_start + 18],
frame[ip_start + 19],
);
let kind = if dst_port == 67 {
InterceptedKind::Dhcp
} else if dst_port == 53 && dst_ip == self.gateway_ip {
InterceptedKind::Dns
} else {
InterceptedKind::Udp
};
self.intercepted.push(InterceptedFrame { frame, kind });
}
}
PROTO_ICMP => {
self.intercepted.push(InterceptedFrame {
frame,
kind: InterceptedKind::Icmp,
});
}
_ => {
tracing::debug!("Dropping IPv4 protocol {}", protocol);
}
}
}
}
impl Device for SmoltcpDevice {
type RxToken<'a> = SmoltcpRxToken;
type TxToken<'a> = SmoltcpTxToken<'a>;
fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> {
let frame = self.rx_queue.pop_front()?;
Some((
SmoltcpRxToken { frame },
SmoltcpTxToken {
tx_pending: &mut self.tx_pending,
},
))
}
fn transmit(&mut self, _timestamp: Instant) -> Option<Self::TxToken<'_>> {
Some(SmoltcpTxToken {
tx_pending: &mut self.tx_pending,
})
}
fn capabilities(&self) -> DeviceCapabilities {
let mut caps = DeviceCapabilities::default();
caps.medium = Medium::Ethernet;
caps.max_transmission_unit = ETHERNET_MTU;
caps.max_burst_size = Some(32);
caps
}
}
pub struct SmoltcpRxToken {
frame: Vec<u8>,
}
impl RxToken for SmoltcpRxToken {
fn consume<R, F>(self, f: F) -> R
where
F: FnOnce(&[u8]) -> R,
{
f(&self.frame)
}
}
pub struct SmoltcpTxToken<'a> {
tx_pending: &'a mut Vec<Vec<u8>>,
}
impl TxToken for SmoltcpTxToken<'_> {
fn consume<R, F>(self, len: usize, f: F) -> R
where
F: FnOnce(&mut [u8]) -> R,
{
let mut buf = vec![0u8; len];
let result = f(&mut buf);
self.tx_pending.push(buf);
result
}
}
fn fd_read(fd: RawFd, buf: &mut [u8]) -> io::Result<usize> {
let n = unsafe { libc::read(fd, buf.as_mut_ptr().cast(), buf.len()) };
if n < 0 {
Err(io::Error::last_os_error())
} else {
#[allow(clippy::cast_sign_loss)]
Ok(n as usize)
}
}
fn learn_guest_mac(mac: [u8; 6], guest_mac: &mut Option<[u8; 6]>) {
if mac[0] & 0x01 != 0 || mac == [0; 6] {
return;
}
if guest_mac.is_none() {
tracing::info!(
"Learned guest MAC: {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
mac[0],
mac[1],
mac[2],
mac[3],
mac[4],
mac[5]
);
}
*guest_mac = Some(mac);
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::fd::FromRawFd;
fn socketpair() -> (std::os::fd::OwnedFd, std::os::fd::OwnedFd) {
use std::os::fd::OwnedFd;
let mut fds: [i32; 2] = [0; 2];
let ret = unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_DGRAM, 0, fds.as_mut_ptr()) };
assert_eq!(ret, 0, "socketpair() failed");
unsafe { (OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])) }
}
fn set_nonblocking(fd: RawFd) {
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFL);
libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
}
}
fn fd_write_raw(fd: RawFd, data: &[u8]) {
unsafe { libc::write(fd, data.as_ptr().cast(), data.len()) };
}
fn make_arp_frame() -> Vec<u8> {
let mut frame = vec![0u8; 42];
let guest_mac = [0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE];
frame[0..6].copy_from_slice(&[0xFF; 6]);
frame[6..12].copy_from_slice(&guest_mac);
frame[12..14].copy_from_slice(&[0x08, 0x06]);
frame
}
fn make_tcp_frame() -> Vec<u8> {
let mut frame = vec![0u8; ETH_HEADER_LEN + 40];
let guest_mac = [0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE];
frame[0..6].copy_from_slice(&[0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
frame[6..12].copy_from_slice(&guest_mac);
frame[12..14].copy_from_slice(&[0x08, 0x00]); let ip = ETH_HEADER_LEN;
frame[ip] = 0x45;
frame[ip + 2..ip + 4].copy_from_slice(&40u16.to_be_bytes());
frame[ip + 9] = PROTO_TCP;
frame[ip + 12..ip + 16].copy_from_slice(&[192, 168, 64, 2]);
frame[ip + 16..ip + 20].copy_from_slice(&[1, 1, 1, 1]);
frame
}
fn make_udp_frame(dst_ip: [u8; 4], dst_port: u16) -> Vec<u8> {
let mut frame = vec![0u8; ETH_HEADER_LEN + 28];
let guest_mac = [0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE];
frame[0..6].copy_from_slice(&[0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
frame[6..12].copy_from_slice(&guest_mac);
frame[12..14].copy_from_slice(&[0x08, 0x00]);
let ip = ETH_HEADER_LEN;
frame[ip] = 0x45;
frame[ip + 2..ip + 4].copy_from_slice(&28u16.to_be_bytes());
frame[ip + 9] = PROTO_UDP;
frame[ip + 12..ip + 16].copy_from_slice(&[192, 168, 64, 2]);
frame[ip + 16..ip + 20].copy_from_slice(&dst_ip);
let l4 = ip + 20;
frame[l4..l4 + 2].copy_from_slice(&1234u16.to_be_bytes());
frame[l4 + 2..l4 + 4].copy_from_slice(&dst_port.to_be_bytes());
frame[l4 + 4..l4 + 6].copy_from_slice(&8u16.to_be_bytes());
frame
}
fn make_icmp_frame() -> Vec<u8> {
let mut frame = vec![0u8; ETH_HEADER_LEN + 28];
let guest_mac = [0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE];
frame[0..6].copy_from_slice(&[0x02, 0x00, 0x00, 0x00, 0x00, 0x01]);
frame[6..12].copy_from_slice(&guest_mac);
frame[12..14].copy_from_slice(&[0x08, 0x00]);
let ip = ETH_HEADER_LEN;
frame[ip] = 0x45;
frame[ip + 2..ip + 4].copy_from_slice(&28u16.to_be_bytes());
frame[ip + 9] = PROTO_ICMP;
frame[ip + 12..ip + 16].copy_from_slice(&[192, 168, 64, 2]);
frame[ip + 16..ip + 20].copy_from_slice(&[8, 8, 8, 8]);
frame
}
#[test]
fn classify_arp_goes_to_rx_queue() {
let gateway_ip = Ipv4Addr::new(192, 168, 64, 1);
let mut device = SmoltcpDevice::new(0, gateway_ip);
let mut guest_mac = None;
device.classify_frame(make_arp_frame(), &mut guest_mac);
assert_eq!(device.rx_queue.len(), 1);
assert!(device.intercepted.is_empty());
}
#[test]
fn classify_tcp_goes_to_rx_queue() {
let gateway_ip = Ipv4Addr::new(192, 168, 64, 1);
let mut device = SmoltcpDevice::new(0, gateway_ip);
let mut guest_mac = None;
device.classify_frame(make_tcp_frame(), &mut guest_mac);
assert_eq!(device.rx_queue.len(), 1);
assert!(device.intercepted.is_empty());
}
#[test]
fn classify_dhcp_intercepted() {
let gateway_ip = Ipv4Addr::new(192, 168, 64, 1);
let mut device = SmoltcpDevice::new(0, gateway_ip);
let mut guest_mac = None;
device.classify_frame(make_udp_frame([255, 255, 255, 255], 67), &mut guest_mac);
assert!(device.rx_queue.is_empty());
assert_eq!(device.intercepted.len(), 1);
assert_eq!(device.intercepted[0].kind, InterceptedKind::Dhcp);
}
#[test]
fn classify_dns_to_gateway_intercepted() {
let gateway_ip = Ipv4Addr::new(192, 168, 64, 1);
let mut device = SmoltcpDevice::new(0, gateway_ip);
let mut guest_mac = None;
device.classify_frame(make_udp_frame([192, 168, 64, 1], 53), &mut guest_mac);
assert!(device.rx_queue.is_empty());
assert_eq!(device.intercepted.len(), 1);
assert_eq!(device.intercepted[0].kind, InterceptedKind::Dns);
}
#[test]
fn classify_dns_to_external_is_udp() {
let gateway_ip = Ipv4Addr::new(192, 168, 64, 1);
let mut device = SmoltcpDevice::new(0, gateway_ip);
let mut guest_mac = None;
device.classify_frame(make_udp_frame([8, 8, 8, 8], 53), &mut guest_mac);
assert!(device.rx_queue.is_empty());
assert_eq!(device.intercepted.len(), 1);
assert_eq!(device.intercepted[0].kind, InterceptedKind::Udp);
}
#[test]
fn classify_icmp_intercepted() {
let gateway_ip = Ipv4Addr::new(192, 168, 64, 1);
let mut device = SmoltcpDevice::new(0, gateway_ip);
let mut guest_mac = None;
device.classify_frame(make_icmp_frame(), &mut guest_mac);
assert!(device.rx_queue.is_empty());
assert_eq!(device.intercepted.len(), 1);
assert_eq!(device.intercepted[0].kind, InterceptedKind::Icmp);
}
#[test]
fn classify_regular_udp_intercepted() {
let gateway_ip = Ipv4Addr::new(192, 168, 64, 1);
let mut device = SmoltcpDevice::new(0, gateway_ip);
let mut guest_mac = None;
device.classify_frame(make_udp_frame([1, 1, 1, 1], 443), &mut guest_mac);
assert!(device.rx_queue.is_empty());
assert_eq!(device.intercepted.len(), 1);
assert_eq!(device.intercepted[0].kind, InterceptedKind::Udp);
}
#[test]
fn drain_guest_fd_reads_and_classifies() {
use std::os::fd::AsRawFd;
let (host_fd, guest_fd) = socketpair();
set_nonblocking(host_fd.as_raw_fd());
let gateway_ip = Ipv4Addr::new(192, 168, 64, 1);
let mut device = SmoltcpDevice::new(host_fd.as_raw_fd(), gateway_ip);
let mut guest_mac = None;
fd_write_raw(guest_fd.as_raw_fd(), &make_arp_frame());
fd_write_raw(guest_fd.as_raw_fd(), &make_icmp_frame());
device.drain_guest_fd(&mut guest_mac);
assert_eq!(device.rx_queue.len(), 1, "ARP should go to rx_queue");
assert_eq!(device.intercepted.len(), 1, "ICMP should be intercepted");
assert!(guest_mac.is_some(), "Guest MAC should be learned");
}
#[test]
fn tx_token_collects_frames() {
let gateway_ip = Ipv4Addr::new(192, 168, 64, 1);
let mut device = SmoltcpDevice::new(0, gateway_ip);
let token = device.transmit(Instant::from_millis(0)).unwrap();
token.consume(42, |buf| {
buf[0..6].copy_from_slice(&[0xFF; 6]);
});
let pending = device.take_tx_pending();
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].len(), 42);
assert_eq!(&pending[0][0..6], &[0xFF; 6]);
}
#[test]
fn learn_guest_mac_ignores_broadcast() {
let mut mac = None;
learn_guest_mac([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], &mut mac);
assert!(mac.is_none());
}
#[test]
fn learn_guest_mac_records_unicast() {
let mut mac = None;
learn_guest_mac([0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE], &mut mac);
assert_eq!(mac, Some([0x02, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE]));
}
#[test]
fn classify_tcp_syn_is_gated() {
let gateway_ip = Ipv4Addr::new(192, 168, 64, 1);
let mut device = SmoltcpDevice::new(0, gateway_ip);
let mut guest_mac = None;
let mut frame = make_tcp_frame();
let ip = ETH_HEADER_LEN;
let l4 = ip + 20;
frame[l4 + 13] = 0x02; frame[l4..l4 + 2].copy_from_slice(&12345u16.to_be_bytes());
frame[l4 + 2..l4 + 4].copy_from_slice(&443u16.to_be_bytes());
frame[l4 + 4..l4 + 8].copy_from_slice(&1000u32.to_be_bytes());
device.classify_frame(frame.clone(), &mut guest_mac);
assert!(device.rx_queue.is_empty(), "SYN should NOT go to rx_queue");
assert_eq!(device.gated_syns.len(), 1);
assert_eq!(device.gated_syns[0].dst_port, 443);
assert_eq!(device.gated_syns[0].src_port, 12345);
assert_eq!(device.gated_syns[0].syn_seq, 1000);
assert_eq!(device.gated_syns[0].dst_ip, Ipv4Addr::new(1, 1, 1, 1));
assert_eq!(device.gated_syns[0].src_ip, Ipv4Addr::new(192, 168, 64, 2));
}
#[test]
fn classify_tcp_non_syn_goes_to_rx_queue() {
let gateway_ip = Ipv4Addr::new(192, 168, 64, 1);
let mut device = SmoltcpDevice::new(0, gateway_ip);
let mut guest_mac = None;
let mut frame = make_tcp_frame();
let ip = ETH_HEADER_LEN;
let l4 = ip + 20;
frame[l4 + 13] = 0x10;
device.classify_frame(frame, &mut guest_mac);
assert_eq!(
device.rx_queue.len(),
1,
"Non-SYN TCP should go to rx_queue"
);
assert!(device.gated_syns.is_empty());
}
}