use std::fs;
use std::io;
use std::mem;
const SEND_TIMEOUT_US: i64 = 100_000;
const ETHERTYPE_LOCAL_EXPERIMENTAL: u16 = 0x88B5;
pub(super) struct NetTrafficSender {
fd: i32,
frame: Vec<u8>,
dst: libc::sockaddr_ll,
}
impl NetTrafficSender {
pub(super) fn setup(frame_bytes: u16) -> io::Result<Self> {
let iface = virtio_net_iface()?;
let ifindex = iface_ifindex(&iface)?;
let mac = iface_mac(&iface)?;
let proto = (libc::ETH_P_ALL as u16).to_be() as libc::c_int;
let fd = unsafe { libc::socket(libc::AF_PACKET, libc::SOCK_RAW, proto) };
if fd < 0 {
return Err(io::Error::last_os_error());
}
let sender = NetTrafficSender {
fd,
frame: build_frame(mac, frame_bytes),
dst: dst_sockaddr(ifindex, mac),
};
sender.bring_up(&iface)?;
sender.bind_to(ifindex)?;
sender.set_send_timeout()?;
Ok(sender)
}
pub(super) fn send_one(&self) -> bool {
let sent = unsafe {
libc::sendto(
self.fd,
self.frame.as_ptr() as *const libc::c_void,
self.frame.len(),
0,
&self.dst as *const _ as *const libc::sockaddr,
mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
)
};
sent > 0
}
fn bring_up(&self, iface: &str) -> io::Result<()> {
set_iface_up(self.fd, iface)
}
fn bind_to(&self, ifindex: i32) -> io::Result<()> {
bind_socket_to_ifindex(self.fd, ifindex)
}
fn set_send_timeout(&self) -> io::Result<()> {
set_socket_timeout(self.fd, libc::SO_SNDTIMEO, SEND_TIMEOUT_US)
}
}
impl Drop for NetTrafficSender {
fn drop(&mut self) {
unsafe {
libc::close(self.fd);
}
}
}
const RECV_TIMEOUT_US: i64 = 100_000;
fn set_iface_up(fd: i32, iface: &str) -> io::Result<()> {
let mut ifr: libc::ifreq = unsafe { mem::zeroed() };
copy_ifname(&mut ifr.ifr_name, iface)?;
let rc = unsafe { libc::ioctl(fd, libc::SIOCGIFFLAGS, &mut ifr) };
if rc != 0 {
return Err(io::Error::last_os_error());
}
unsafe {
ifr.ifr_ifru.ifru_flags |= (libc::IFF_UP | libc::IFF_RUNNING) as libc::c_short;
}
let rc = unsafe { libc::ioctl(fd, libc::SIOCSIFFLAGS, &ifr) };
if rc != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
fn bind_socket_to_ifindex(fd: i32, ifindex: i32) -> io::Result<()> {
let mut sll: libc::sockaddr_ll = unsafe { mem::zeroed() };
sll.sll_family = libc::AF_PACKET as u16;
sll.sll_protocol = (libc::ETH_P_ALL as u16).to_be();
sll.sll_ifindex = ifindex;
let rc = unsafe {
libc::bind(
fd,
&sll as *const _ as *const libc::sockaddr,
mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
)
};
if rc != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
fn set_socket_timeout(fd: i32, optname: libc::c_int, timeout_us: i64) -> io::Result<()> {
let tv = libc::timeval {
tv_sec: timeout_us / 1_000_000,
tv_usec: (timeout_us % 1_000_000) as libc::suseconds_t,
};
let rc = unsafe {
libc::setsockopt(
fd,
libc::SOL_SOCKET,
optname,
&tv as *const _ as *const libc::c_void,
mem::size_of::<libc::timeval>() as libc::socklen_t,
)
};
if rc != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
pub(super) struct IrqWakeReceiver {
fd: i32,
buf: Vec<u8>,
}
impl IrqWakeReceiver {
pub(super) fn setup() -> io::Result<Self> {
let iface = virtio_net_iface()?;
let ifindex = iface_ifindex(&iface)?;
let proto = (libc::ETH_P_ALL as u16).to_be() as libc::c_int;
let fd = unsafe { libc::socket(libc::AF_PACKET, libc::SOCK_RAW, proto) };
if fd < 0 {
return Err(io::Error::last_os_error());
}
let receiver = IrqWakeReceiver {
fd,
buf: vec![0u8; 1514],
};
set_iface_up(receiver.fd, &iface)?;
bind_socket_to_ifindex(receiver.fd, ifindex)?;
set_socket_timeout(receiver.fd, libc::SO_RCVTIMEO, RECV_TIMEOUT_US)?;
Ok(receiver)
}
pub(super) fn recv_one(&mut self) -> bool {
let n = unsafe {
libc::recvfrom(
self.fd,
self.buf.as_mut_ptr() as *mut libc::c_void,
self.buf.len(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
n > 0
}
}
impl Drop for IrqWakeReceiver {
fn drop(&mut self) {
unsafe {
libc::close(self.fd);
}
}
}
pub(super) fn warn_irq_wake_setup_failed_once(err: &io::Error) {
static WARNED: std::sync::Once = std::sync::Once::new();
WARNED.call_once(|| {
eprintln!(
"workload: WorkType::IrqWake could not open an AF_PACKET socket \
({err}); the sender/receiver pair is a no-op (work_units=0). Attach \
a NIC with #[ktstr_test(networks = [...])]. See the WorkType::IrqWake \
variant doc."
);
});
}
fn virtio_net_iface() -> io::Result<String> {
virtio_net_iface_in(std::path::Path::new("/sys/class/net"))
}
fn virtio_net_iface_in(root: &std::path::Path) -> io::Result<String> {
for ent in fs::read_dir(root)? {
let ent = ent?;
let name = ent.file_name().to_string_lossy().into_owned();
if name == "lo" {
continue;
}
if ent.path().join("device").exists() {
return Ok(name);
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
"no non-loopback network interface with a device under /sys/class/net \
(attach a NIC with #[ktstr_test(networks = [...])])",
))
}
fn iface_ifindex(iface: &str) -> io::Result<i32> {
let s = fs::read_to_string(format!("/sys/class/net/{iface}/ifindex"))?;
s.trim().parse::<i32>().map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("ifindex for {iface}: {e}"),
)
})
}
fn iface_mac(iface: &str) -> io::Result<[u8; 6]> {
let s = fs::read_to_string(format!("/sys/class/net/{iface}/address"))?;
parse_mac(s.trim()).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("bad MAC for {iface}: {s:?}"),
)
})
}
fn parse_mac(s: &str) -> Option<[u8; 6]> {
let mut mac = [0u8; 6];
let mut n = 0;
for (i, octet) in s.split(':').enumerate() {
if i >= 6 {
return None;
}
mac[i] = u8::from_str_radix(octet, 16).ok()?;
n = i + 1;
}
(n == 6).then_some(mac)
}
fn build_frame(mac: [u8; 6], frame_bytes: u16) -> Vec<u8> {
debug_assert!(
frame_bytes as usize >= 18,
"frame_bytes {frame_bytes} < 18 leaves no room for the L2 header + marker; \
validate_workload_admission enforces >= 60"
);
let mut frame = vec![0u8; (frame_bytes as usize).max(18)];
frame[0..6].copy_from_slice(&mac);
frame[6..12].copy_from_slice(&mac);
frame[12..14].copy_from_slice(ÐERTYPE_LOCAL_EXPERIMENTAL.to_be_bytes());
frame[14..18].copy_from_slice(b"KTST");
frame
}
fn dst_sockaddr(ifindex: i32, mac: [u8; 6]) -> libc::sockaddr_ll {
let mut dst: libc::sockaddr_ll = unsafe { mem::zeroed() };
dst.sll_family = libc::AF_PACKET as u16;
dst.sll_protocol = (libc::ETH_P_ALL as u16).to_be();
dst.sll_ifindex = ifindex;
dst.sll_halen = 6;
dst.sll_addr[0..6].copy_from_slice(&mac);
dst
}
fn copy_ifname(buf: &mut [libc::c_char; libc::IFNAMSIZ], iface: &str) -> io::Result<()> {
let bytes = iface.as_bytes();
if bytes.len() >= libc::IFNAMSIZ {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("interface name {iface} exceeds IFNAMSIZ"),
));
}
for (i, &b) in bytes.iter().enumerate() {
buf[i] = b as libc::c_char;
}
Ok(())
}
pub(super) fn warn_net_traffic_setup_failed_once(err: &io::Error) {
static WARNED: std::sync::Once = std::sync::Once::new();
WARNED.call_once(|| {
eprintln!(
"workload: WorkType::NetTraffic could not open an AF_PACKET sender \
({err}); the worker is a no-op (work_units=0). Attach a NIC with \
#[ktstr_test(networks = [...])]. See the WorkType::NetTraffic variant doc."
);
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_frame_is_self_addressed_and_sized() {
let mac = [0x52, 0x54, 0x00, 0x12, 0x34, 0x56];
let f = build_frame(mac, 60);
assert_eq!(f.len(), 60, "frame is the requested size");
assert_eq!(
&f[0..6],
&mac,
"dst MAC = our MAC (loopback echoes verbatim)"
);
assert_eq!(&f[6..12], &mac, "src MAC = our MAC");
assert_eq!(
&f[12..14],
ÐERTYPE_LOCAL_EXPERIMENTAL.to_be_bytes(),
"ethertype is the local-experimental marker, big-endian"
);
assert_eq!(&f[14..18], b"KTST", "payload marker present");
assert!(f[18..].iter().all(|&b| b == 0), "remainder is zero-padded");
}
#[test]
fn build_frame_larger_size_zero_pads_tail() {
let mac = [1, 2, 3, 4, 5, 6];
let f = build_frame(mac, 1514);
assert_eq!(f.len(), 1514);
assert_eq!(&f[14..18], b"KTST");
assert!(
f[18..].iter().all(|&b| b == 0),
"the tail past the marker stays zero"
);
}
#[test]
fn parse_mac_roundtrips_and_rejects_malformed() {
assert_eq!(
parse_mac("52:54:00:12:34:56"),
Some([0x52, 0x54, 0x00, 0x12, 0x34, 0x56])
);
assert_eq!(parse_mac("01:02:03:04:05"), None, "5 octets rejected");
assert_eq!(
parse_mac("01:02:03:04:05:06:07"),
None,
">6 octets rejected"
);
assert_eq!(parse_mac("zz:02:03:04:05:06"), None, "non-hex rejected");
}
#[test]
fn dst_sockaddr_carries_ifindex_and_mac() {
let mac = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
let d = dst_sockaddr(7, mac);
assert_eq!(d.sll_family, libc::AF_PACKET as u16);
assert_eq!(d.sll_ifindex, 7);
assert_eq!(d.sll_halen, 6);
assert_eq!(&d.sll_addr[0..6], &mac);
}
#[test]
fn virtio_net_iface_in_lo_only_is_not_found() {
let tmp = tempfile::TempDir::new().expect("tempdir");
std::fs::create_dir(tmp.path().join("lo")).unwrap();
let err = virtio_net_iface_in(tmp.path()).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
#[test]
fn virtio_net_iface_in_picks_the_device_backed_iface() {
let tmp = tempfile::TempDir::new().expect("tempdir");
std::fs::create_dir(tmp.path().join("lo")).unwrap();
let eth = tmp.path().join("eth0");
std::fs::create_dir(ð).unwrap();
std::fs::create_dir(eth.join("device")).unwrap();
assert_eq!(virtio_net_iface_in(tmp.path()).unwrap(), "eth0");
}
}