use std::cell::Cell;
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use crate::afxdp::ring::{AddrRing, DescRing};
use crate::xdp::{self, Capture, CaptureConfig, Mode};
use crate::{Frame, IpPrefix, L2Handler, MacAddr, Result};
const SOL_XDP: libc::c_int = 283;
const XDP_MMAP_OFFSETS: libc::c_int = 1;
const XDP_RX_RING: libc::c_int = 2;
const XDP_TX_RING: libc::c_int = 3;
const XDP_UMEM_REG: libc::c_int = 4;
const XDP_UMEM_FILL_RING: libc::c_int = 5;
const XDP_UMEM_COMPLETION_RING: libc::c_int = 6;
const XDP_STATISTICS: libc::c_int = 7;
const XDP_OPTIONS: libc::c_int = 8;
const XDP_OPTIONS_ZEROCOPY: u32 = 1 << 0;
const XDP_PGOFF_RX_RING: libc::off_t = 0;
const XDP_PGOFF_TX_RING: libc::off_t = 0x8000_0000;
const XDP_UMEM_PGOFF_FILL_RING: libc::off_t = 0x1_0000_0000;
const XDP_UMEM_PGOFF_COMPLETION_RING: libc::off_t = 0x1_8000_0000;
const XDP_COPY: u16 = 1 << 1;
const XDP_ZEROCOPY: u16 = 1 << 2;
const XDP_USE_NEED_WAKEUP: u16 = 1 << 3;
const SO_BUSY_POLL: libc::c_int = 46;
const SO_PREFER_BUSY_POLL: libc::c_int = 69;
const SO_BUSY_POLL_BUDGET: libc::c_int = 70;
const MIN_FRAME_SIZE: u32 = 2048;
const BATCH: usize = 64;
const POLL_TIMEOUT_MS: libc::c_int = 1000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Zerocopy {
#[default]
Auto,
Require,
Off,
}
#[derive(Debug, Clone, Copy)]
pub struct BusyPoll {
pub timeout_us: u32,
pub budget: u32,
}
impl Default for BusyPoll {
fn default() -> BusyPoll {
BusyPoll {
timeout_us: 20,
budget: BATCH as u32,
}
}
}
#[derive(Debug, Clone)]
pub enum ProgramSource {
Capture(CaptureConfig),
External { xskmap_fd: RawFd },
}
impl Default for ProgramSource {
fn default() -> ProgramSource {
ProgramSource::Capture(CaptureConfig::default())
}
}
#[derive(Debug, Clone)]
pub struct Config {
pub interface: String,
pub queue_ids: Vec<u32>,
pub ring_size: u32,
pub frame_size: u32,
pub num_frames: u32,
pub zerocopy: Zerocopy,
pub mode: Mode,
pub program: ProgramSource,
pub busy_poll: Option<BusyPoll>,
pub huge_pages: bool,
pub flags: u16,
}
impl Default for Config {
fn default() -> Config {
Config {
interface: String::new(),
queue_ids: Vec::new(),
ring_size: 2048,
frame_size: 4096,
num_frames: 4096,
zerocopy: Zerocopy::Auto,
mode: Mode::AUTO,
program: ProgramSource::default(),
busy_poll: None,
huge_pages: false,
flags: 0,
}
}
}
impl Config {
fn normalize(&self) -> Result<Config> {
let mut c = self.clone();
if c.ring_size == 0 {
c.ring_size = 2048;
}
if c.frame_size == 0 {
c.frame_size = 4096;
}
if c.num_frames == 0 {
c.num_frames = 4096;
}
if !c.ring_size.is_power_of_two() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("afxdp: ring_size must be a power of 2, got {}", c.ring_size),
));
}
if !c.frame_size.is_power_of_two() || c.frame_size < MIN_FRAME_SIZE {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"afxdp: frame_size must be a power of 2 >= {MIN_FRAME_SIZE}, got {}",
c.frame_size
),
));
}
let page = page_size();
if c.frame_size as usize > page {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"afxdp: frame_size {} exceeds the page size {page}",
c.frame_size
),
));
}
if c.num_frames < 2 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("afxdp: num_frames must be at least 2, got {}", c.num_frames),
));
}
Ok(c)
}
}
#[derive(Debug)]
struct Mapping {
ptr: *mut u8,
len: usize,
}
impl Mapping {
#[inline]
fn ptr(&self) -> *mut u8 {
self.ptr
}
}
impl Drop for Mapping {
fn drop(&mut self) {
unsafe { libc::munmap(self.ptr as *mut libc::c_void, self.len) };
}
}
unsafe impl Send for Mapping {}
unsafe impl Sync for Mapping {}
struct Socket {
fd: OwnedFd,
queue_id: u32,
frame_size: usize,
zerocopy: bool,
umem: Mapping,
_fill_map: Mapping,
_comp_map: Mapping,
_rx_map: Mapping,
_tx_map: Mapping,
fill_ring: AddrRing,
comp_ring: AddrRing,
rx_ring: DescRing,
tx_ring: DescRing,
tx_free: Mutex<Vec<u64>>,
handler: Arc<Mutex<Option<L2Handler>>>,
closed: Arc<AtomicBool>,
}
impl std::fmt::Debug for Socket {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Socket")
.field("fd", &self.fd.as_raw_fd())
.field("queue_id", &self.queue_id)
.field("zerocopy", &self.zerocopy)
.finish()
}
}
impl Socket {
#[inline]
fn raw(&self) -> RawFd {
self.fd.as_raw_fd()
}
}
struct Inner {
ifindex: u32,
mac: MacAddr,
sockets: Vec<Arc<Socket>>,
capture: Option<Capture>,
handler: Arc<Mutex<Option<L2Handler>>>,
closed: Arc<AtomicBool>,
tx_cursor: AtomicUsize,
}
impl Drop for Inner {
fn drop(&mut self) {
self.closed.store(true, Ordering::Release);
}
}
thread_local! {
static TX_SLOT: Cell<usize> = const { Cell::new(usize::MAX) };
}
pub struct Device {
inner: Arc<Inner>,
}
impl std::fmt::Debug for Device {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Device")
.field("ifindex", &self.inner.ifindex)
.field("mac", &self.inner.mac)
.field("queues", &self.inner.sockets.len())
.field("zerocopy", &self.zerocopy())
.finish()
}
}
impl Device {
pub fn open(cfg: Config) -> Result<Device> {
let cfg = cfg.normalize()?;
let ifindex = if_nametoindex(&cfg.interface)?;
let mac = read_hw_addr(&cfg.interface).unwrap_or_else(|_| MacAddr::zero());
let queue_ids = if cfg.queue_ids.is_empty() {
let n = rx_queue_count(&cfg.interface).unwrap_or(1).max(1);
(0..n).collect()
} else {
let mut q = cfg.queue_ids.clone();
q.sort_unstable();
q.dedup();
q
};
let capture = match &cfg.program {
ProgramSource::Capture(ccfg) => Some(Capture::attach(ifindex, ccfg.clone(), cfg.mode)?),
ProgramSource::External { .. } => None,
};
let xskmap_fd = match (&capture, &cfg.program) {
(Some(c), _) => c.xskmap().as_raw_fd(),
(None, ProgramSource::External { xskmap_fd }) => *xskmap_fd,
(None, _) => unreachable!("capture is Some for ProgramSource::Capture"),
};
let attached_mode = capture.as_ref().map(|c| c.mode());
let want_zc = match cfg.zerocopy {
Zerocopy::Off => false,
_ => attached_mode.map(|m| m.supports_zerocopy()).unwrap_or(true),
};
if cfg.zerocopy == Zerocopy::Require && !want_zc {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
format!(
"afxdp: zero-copy requires a native XDP attachment, got {:?}",
attached_mode
),
));
}
let closed = Arc::new(AtomicBool::new(false));
let handler: Arc<Mutex<Option<L2Handler>>> = Arc::new(Mutex::new(None));
let mut sockets = Vec::with_capacity(queue_ids.len());
for &queue_id in &queue_ids {
let sock = Socket::open(
ifindex,
queue_id,
&cfg,
want_zc,
handler.clone(),
closed.clone(),
)?;
if cfg.zerocopy == Zerocopy::Require && !sock.zerocopy {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
format!("afxdp: queue {queue_id} bound in copy mode"),
));
}
xdp::set_socket_raw(xskmap_fd, queue_id, sock.raw())?;
sockets.push(Arc::new(sock));
}
let inner = Arc::new(Inner {
ifindex,
mac,
sockets,
capture,
handler,
closed,
tx_cursor: AtomicUsize::new(0),
});
for sock in &inner.sockets {
let s = sock.clone();
std::thread::spawn(move || poll_loop(s));
}
Ok(Device { inner })
}
#[inline]
pub fn hw_addr(&self) -> MacAddr {
self.inner.mac
}
pub fn queue_ids(&self) -> Vec<u32> {
self.inner.sockets.iter().map(|s| s.queue_id).collect()
}
pub fn zerocopy(&self) -> bool {
!self.inner.sockets.is_empty() && self.inner.sockets.iter().all(|s| s.zerocopy)
}
pub fn mode(&self) -> Option<Mode> {
self.inner.capture.as_ref().map(|c| c.mode())
}
pub fn capture(&self) -> Option<&Capture> {
self.inner.capture.as_ref()
}
pub fn capture_add(&self, prefix: IpPrefix) -> Result<()> {
self.require_capture()?.add(prefix)
}
pub fn capture_remove(&self, prefix: IpPrefix) -> Result<bool> {
self.require_capture()?.remove(prefix)
}
fn require_capture(&self) -> Result<&Capture> {
self.inner.capture.as_ref().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"afxdp: device uses an external XDP program; manage its maps directly",
)
})
}
pub fn statistics(&self) -> Result<libc::xdp_statistics> {
let mut total = libc::xdp_statistics {
rx_dropped: 0,
rx_invalid_descs: 0,
tx_invalid_descs: 0,
rx_ring_full: 0,
rx_fill_ring_empty_descs: 0,
tx_ring_empty_descs: 0,
};
for s in &self.inner.sockets {
let st = getsockopt_statistics(s.raw())?;
total.rx_dropped += st.rx_dropped;
total.rx_invalid_descs += st.rx_invalid_descs;
total.tx_invalid_descs += st.tx_invalid_descs;
total.rx_ring_full += st.rx_ring_full;
total.rx_fill_ring_empty_descs += st.rx_fill_ring_empty_descs;
total.tx_ring_empty_descs += st.tx_ring_empty_descs;
}
Ok(total)
}
fn tx_socket(&self) -> &Arc<Socket> {
let n = self.inner.sockets.len();
if n == 1 {
return &self.inner.sockets[0];
}
let slot = TX_SLOT.with(|c| {
if c.get() == usize::MAX {
c.set(self.inner.tx_cursor.fetch_add(1, Ordering::Relaxed));
}
c.get()
});
&self.inner.sockets[slot % n]
}
}
impl crate::L2Device for Device {
fn set_handler(&self, h: L2Handler) {
*self.inner.handler.lock().unwrap() = Some(h);
}
fn send(&self, frame: &Frame) -> Result<()> {
self.tx_socket().send(frame.as_bytes())
}
fn hw_addr(&self) -> MacAddr {
self.inner.mac
}
fn close(&self) -> Result<()> {
self.inner.closed.store(true, Ordering::Release);
Ok(())
}
}
impl Socket {
fn open(
ifindex: u32,
queue_id: u32,
cfg: &Config,
want_zerocopy: bool,
handler: Arc<Mutex<Option<L2Handler>>>,
closed: Arc<AtomicBool>,
) -> Result<Socket> {
let ring_size = cfg.ring_size;
let frame_size = cfg.frame_size;
let num_frames = cfg.num_frames;
let fd = unsafe { libc::socket(libc::AF_XDP, libc::SOCK_RAW, 0) };
if fd < 0 {
return Err(io::Error::last_os_error());
}
let fd = unsafe { OwnedFd::from_raw_fd(fd) };
let raw = fd.as_raw_fd();
let umem_size = num_frames as usize * frame_size as usize;
let umem = mmap_umem(umem_size, cfg.huge_pages)?;
let reg = libc::xdp_umem_reg {
addr: umem.ptr() as u64,
len: umem_size as u64,
chunk_size: frame_size,
headroom: 0,
flags: 0,
tx_metadata_len: 0,
};
setsockopt_umem_reg(raw, ®)?;
for opt in [
XDP_UMEM_FILL_RING,
XDP_UMEM_COMPLETION_RING,
XDP_RX_RING,
XDP_TX_RING,
] {
setsockopt_u32(raw, opt, ring_size)?;
}
let offs = getsockopt_mmap_offsets(raw)?;
let fill_map = mmap_ring(raw, XDP_UMEM_PGOFF_FILL_RING, &offs.fr, ring_size)?;
let comp_map = mmap_ring(raw, XDP_UMEM_PGOFF_COMPLETION_RING, &offs.cr, ring_size)?;
let rx_map = mmap_ring(raw, XDP_PGOFF_RX_RING, &offs.rx, ring_size)?;
let tx_map = mmap_ring(raw, XDP_PGOFF_TX_RING, &offs.tx, ring_size)?;
let fill_ring = unsafe { AddrRing::new(fill_map.ptr(), (&offs.fr).into(), ring_size) };
let comp_ring = unsafe { AddrRing::new(comp_map.ptr(), (&offs.cr).into(), ring_size) };
let rx_ring = unsafe { DescRing::new(rx_map.ptr(), (&offs.rx).into(), ring_size) };
let tx_ring = unsafe { DescRing::new(tx_map.ptr(), (&offs.tx).into(), ring_size) };
let (rx_frames, tx_frames) = umem_split(num_frames);
let rx_addrs: Vec<u64> = (0..rx_frames)
.map(|i| (i as u64) * frame_size as u64)
.collect();
fill_ring.produce(&rx_addrs);
let tx_free: Vec<u64> = (0..tx_frames)
.map(|i| ((rx_frames + i) as u64) * frame_size as u64)
.collect();
let zerocopy = bind_xdp(raw, ifindex, queue_id, cfg, want_zerocopy)?;
if let Some(bp) = cfg.busy_poll {
set_busy_poll(raw, bp)?;
}
Ok(Socket {
fd,
queue_id,
frame_size: frame_size as usize,
zerocopy,
umem,
_fill_map: fill_map,
_comp_map: comp_map,
_rx_map: rx_map,
_tx_map: tx_map,
fill_ring,
comp_ring,
rx_ring,
tx_ring,
tx_free: Mutex::new(tx_free),
handler,
closed,
})
}
fn send(&self, frame: &[u8]) -> Result<()> {
if frame.len() < 14 {
return Ok(());
}
if frame.len() > self.frame_size {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"afxdp: frame of {} bytes exceeds the {}-byte UMEM chunk",
frame.len(),
self.frame_size
),
));
}
if self.closed.load(Ordering::Acquire) {
return Err(io::Error::new(io::ErrorKind::NotConnected, "afxdp: closed"));
}
let mut free = self.tx_free.lock().unwrap();
let addr = match free.pop() {
Some(a) => a,
None => {
self.reclaim_tx(&mut free);
match free.pop() {
Some(a) => a,
None => {
return Err(io::Error::new(
io::ErrorKind::WouldBlock,
"afxdp: no free TX buffers",
));
}
}
}
};
let len = frame.len();
unsafe {
std::ptr::copy_nonoverlapping(frame.as_ptr(), self.umem.ptr().add(addr as usize), len);
}
let desc = [libc::xdp_desc {
addr,
len: len as u32,
options: 0,
}];
if self.tx_ring.produce(&desc) == 0 {
free.push(addr);
return Err(io::Error::new(
io::ErrorKind::WouldBlock,
"afxdp: TX ring full",
));
}
drop(free);
if self.tx_ring.need_wakeup() {
self.kick_tx();
}
Ok(())
}
fn reclaim_tx(&self, free: &mut Vec<u64>) {
let mut batch = [0u64; BATCH];
loop {
let n = self.comp_ring.consume(&mut batch);
if n == 0 {
return;
}
free.extend_from_slice(&batch[..n]);
}
}
fn kick_tx(&self) {
unsafe {
libc::sendto(
self.raw(),
std::ptr::null(),
0,
libc::MSG_DONTWAIT,
std::ptr::null(),
0,
);
}
}
fn wait(&self, timeout_ms: libc::c_int) {
let mut pfd = libc::pollfd {
fd: self.raw(),
events: libc::POLLIN,
revents: 0,
};
unsafe { libc::poll(&mut pfd, 1, timeout_ms) };
}
}
fn umem_split(num_frames: u32) -> (u32, u32) {
let rx = (num_frames / 2).max(1);
(rx, num_frames - rx)
}
fn poll_loop(sock: Arc<Socket>) {
let mut rx_batch = [libc::xdp_desc {
addr: 0,
len: 0,
options: 0,
}; BATCH];
let mut fill_batch = [0u64; BATCH];
while !sock.closed.load(Ordering::Acquire) {
let got = sock.rx_ring.consume(&mut rx_batch);
if got == 0 {
{
let mut free = sock.tx_free.lock().unwrap();
sock.reclaim_tx(&mut free);
}
sock.wait(POLL_TIMEOUT_MS);
continue;
}
let handler = sock.handler.lock().unwrap().clone();
let mut fill_count = 0;
for desc in &rx_batch[..got] {
let addr = desc.addr;
let len = desc.len as usize;
if len >= 14
&& let Some(h) = &handler
{
let slice =
unsafe { std::slice::from_raw_parts(sock.umem.ptr().add(addr as usize), len) };
let _ = h(Frame::from_slice(slice));
}
fill_batch[fill_count] = addr;
fill_count += 1;
}
if fill_count > 0 {
sock.fill_ring.produce(&fill_batch[..fill_count]);
if sock.fill_ring.need_wakeup() {
sock.wait(0);
}
}
}
}
fn page_size() -> usize {
let n = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
if n <= 0 { 4096 } else { n as usize }
}
fn if_nametoindex(name: &str) -> Result<u32> {
let c = std::ffi::CString::new(name).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "afxdp: interface name has NUL")
})?;
let idx = unsafe { libc::if_nametoindex(c.as_ptr()) };
if idx == 0 {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("afxdp: interface {name:?} not found"),
));
}
Ok(idx)
}
fn read_hw_addr(name: &str) -> Result<MacAddr> {
let sock = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
if sock < 0 {
return Err(io::Error::last_os_error());
}
let sock = unsafe { OwnedFd::from_raw_fd(sock) };
let mut req = IfReq::new(name)?;
let r = unsafe { libc::ioctl(sock.as_raw_fd(), libc::SIOCGIFHWADDR, &mut req) };
if r < 0 {
return Err(io::Error::last_os_error());
}
let b = req.union_bytes();
Ok(MacAddr::new([b[2], b[3], b[4], b[5], b[6], b[7]]))
}
#[repr(C)]
struct IfReq {
name: [u8; 16],
union_: [u8; 24],
}
impl IfReq {
fn new(name: &str) -> Result<IfReq> {
let b = name.as_bytes();
if b.len() >= 16 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("afxdp: interface name {name:?} too long"),
));
}
let mut req = IfReq {
name: [0; 16],
union_: [0; 24],
};
req.name[..b.len()].copy_from_slice(b);
Ok(req)
}
fn set_data_ptr(&mut self, p: *mut libc::c_void) {
self.union_[..8].copy_from_slice(&(p as usize as u64).to_ne_bytes());
}
fn union_bytes(&self) -> &[u8; 24] {
&self.union_
}
}
const SIOCETHTOOL: libc::c_ulong = 0x8946;
const ETHTOOL_GRXRINGS: u32 = 0x0000_002f;
const ETHTOOL_GCHANNELS: u32 = 0x0000_003c;
#[repr(C)]
#[derive(Default)]
struct EthtoolChannels {
cmd: u32,
max_rx: u32,
max_tx: u32,
max_other: u32,
max_combined: u32,
rx_count: u32,
tx_count: u32,
other_count: u32,
combined_count: u32,
}
#[repr(C)]
#[derive(Default)]
struct EthtoolRxnfc {
cmd: u32,
flow_type: u32,
data: u64,
_rest: [u64; 8],
}
fn rx_queue_count(name: &str) -> Result<u32> {
let sock = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
if sock < 0 {
return Err(io::Error::last_os_error());
}
let sock = unsafe { OwnedFd::from_raw_fd(sock) };
let raw = sock.as_raw_fd();
let mut ch = EthtoolChannels {
cmd: ETHTOOL_GCHANNELS,
..Default::default()
};
let mut req = IfReq::new(name)?;
req.set_data_ptr(&mut ch as *mut _ as *mut libc::c_void);
if unsafe { libc::ioctl(raw, SIOCETHTOOL, &mut req) } >= 0 {
let n = ch.combined_count + ch.rx_count;
if n > 0 {
return Ok(n);
}
}
let mut nfc = EthtoolRxnfc {
cmd: ETHTOOL_GRXRINGS,
..Default::default()
};
let mut req = IfReq::new(name)?;
req.set_data_ptr(&mut nfc as *mut _ as *mut libc::c_void);
if unsafe { libc::ioctl(raw, SIOCETHTOOL, &mut req) } >= 0 && nfc.data > 0 {
return Ok(nfc.data as u32);
}
Ok(1)
}
fn setsockopt_umem_reg(fd: RawFd, reg: &libc::xdp_umem_reg) -> Result<()> {
let r = unsafe {
libc::setsockopt(
fd,
SOL_XDP,
XDP_UMEM_REG,
reg as *const _ as *const libc::c_void,
std::mem::size_of::<libc::xdp_umem_reg>() as libc::socklen_t,
)
};
if r < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
fn setsockopt_u32(fd: RawFd, opt: libc::c_int, val: u32) -> Result<()> {
setsockopt_u32_level(fd, SOL_XDP, opt, val)
}
fn setsockopt_u32_level(fd: RawFd, level: libc::c_int, opt: libc::c_int, val: u32) -> Result<()> {
let r = unsafe {
libc::setsockopt(
fd,
level,
opt,
&val as *const u32 as *const libc::c_void,
std::mem::size_of::<u32>() as libc::socklen_t,
)
};
if r < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
fn set_busy_poll(fd: RawFd, bp: BusyPoll) -> Result<()> {
let _ = setsockopt_u32_level(fd, libc::SOL_SOCKET, SO_PREFER_BUSY_POLL, 1);
setsockopt_u32_level(fd, libc::SOL_SOCKET, SO_BUSY_POLL, bp.timeout_us)?;
let _ = setsockopt_u32_level(fd, libc::SOL_SOCKET, SO_BUSY_POLL_BUDGET, bp.budget);
Ok(())
}
fn getsockopt_mmap_offsets(fd: RawFd) -> Result<libc::xdp_mmap_offsets> {
let mut offs: libc::xdp_mmap_offsets = unsafe { std::mem::zeroed() };
let mut len = std::mem::size_of::<libc::xdp_mmap_offsets>() as libc::socklen_t;
let r = unsafe {
libc::getsockopt(
fd,
SOL_XDP,
XDP_MMAP_OFFSETS,
&mut offs as *mut _ as *mut libc::c_void,
&mut len,
)
};
if r < 0 {
return Err(io::Error::last_os_error());
}
Ok(offs)
}
fn getsockopt_statistics(fd: RawFd) -> Result<libc::xdp_statistics> {
let mut stats: libc::xdp_statistics = unsafe { std::mem::zeroed() };
let mut len = std::mem::size_of::<libc::xdp_statistics>() as libc::socklen_t;
let r = unsafe {
libc::getsockopt(
fd,
SOL_XDP,
XDP_STATISTICS,
&mut stats as *mut _ as *mut libc::c_void,
&mut len,
)
};
if r < 0 {
return Err(io::Error::last_os_error());
}
Ok(stats)
}
fn socket_is_zerocopy(fd: RawFd) -> bool {
let mut opts: libc::xdp_options = unsafe { std::mem::zeroed() };
let mut len = std::mem::size_of::<libc::xdp_options>() as libc::socklen_t;
let r = unsafe {
libc::getsockopt(
fd,
SOL_XDP,
XDP_OPTIONS,
&mut opts as *mut _ as *mut libc::c_void,
&mut len,
)
};
r >= 0 && opts.flags & XDP_OPTIONS_ZEROCOPY != 0
}
fn mmap_umem(len: usize, huge_pages: bool) -> Result<Mapping> {
if huge_pages && let Ok(m) = mmap_anon(len, libc::MAP_HUGETLB) {
return Ok(m);
}
mmap_anon(len, 0)
}
fn mmap_anon(len: usize, extra_flags: libc::c_int) -> Result<Mapping> {
let ptr = unsafe {
libc::mmap(
std::ptr::null_mut(),
len,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_POPULATE | extra_flags,
-1,
0,
)
};
if ptr == libc::MAP_FAILED {
return Err(io::Error::last_os_error());
}
Ok(Mapping {
ptr: ptr as *mut u8,
len,
})
}
fn mmap_ring(
fd: RawFd,
pgoff: libc::off_t,
off: &libc::xdp_ring_offset,
size: u32,
) -> Result<Mapping> {
let total = ring_map_len(off.desc, size);
let ptr = unsafe {
libc::mmap(
std::ptr::null_mut(),
total,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_SHARED | libc::MAP_POPULATE,
fd,
pgoff,
)
};
if ptr == libc::MAP_FAILED {
return Err(io::Error::last_os_error());
}
Ok(Mapping {
ptr: ptr as *mut u8,
len: total,
})
}
fn ring_map_len(desc_off: u64, size: u32) -> usize {
desc_off as usize + size as usize * std::mem::size_of::<libc::xdp_desc>()
}
fn bind_flag_candidates(extra: u16, want_zerocopy: bool) -> Vec<u16> {
let mut v = Vec::with_capacity(3);
if want_zerocopy {
v.push(XDP_ZEROCOPY | XDP_USE_NEED_WAKEUP | extra);
}
v.push(XDP_COPY | XDP_USE_NEED_WAKEUP | extra);
v.push(XDP_COPY | extra);
v
}
fn bind_xdp(
fd: RawFd,
ifindex: u32,
queue_id: u32,
cfg: &Config,
want_zerocopy: bool,
) -> Result<bool> {
let mut last = None;
for flags in bind_flag_candidates(cfg.flags, want_zerocopy) {
let sa = libc::sockaddr_xdp {
sxdp_family: libc::AF_XDP as u16,
sxdp_flags: flags,
sxdp_ifindex: ifindex,
sxdp_queue_id: queue_id,
sxdp_shared_umem_fd: 0,
};
match bind_once(fd, &sa) {
Ok(()) => return Ok(socket_is_zerocopy(fd)),
Err(e) => last = Some(e),
}
}
Err(last.unwrap_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "afxdp: no bind flags to try")
}))
}
fn bind_once(fd: RawFd, sa: &libc::sockaddr_xdp) -> Result<()> {
let r = unsafe {
libc::bind(
fd,
sa as *const _ as *const libc::sockaddr,
std::mem::size_of::<libc::sockaddr_xdp>() as libc::socklen_t,
)
};
if r < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_values() {
let c = Config::default();
assert_eq!(c.ring_size, 2048);
assert_eq!(c.frame_size, 4096);
assert_eq!(c.num_frames, 4096);
assert_eq!(c.zerocopy, Zerocopy::Auto);
assert_eq!(c.mode, Mode::AUTO);
assert!(c.queue_ids.is_empty());
}
#[test]
fn default_program_captures_nothing_and_passes_everything() {
match Config::default().program {
ProgramSource::Capture(c) => {
assert_eq!(c.default_action, crate::xdp::Action::PASS);
assert!(c.arp);
}
_ => panic!("default should be a capture program"),
}
}
#[test]
fn zero_fields_normalize_to_defaults() {
let c = Config {
interface: "eth0".into(),
ring_size: 0,
frame_size: 0,
num_frames: 0,
..Default::default()
}
.normalize()
.unwrap();
assert_eq!(c.ring_size, 2048);
assert_eq!(c.frame_size, 4096);
assert_eq!(c.num_frames, 4096);
}
#[test]
fn ring_size_must_be_power_of_two() {
let e = Config {
ring_size: 1000,
..Default::default()
}
.normalize()
.unwrap_err();
assert_eq!(e.kind(), io::ErrorKind::InvalidInput);
}
#[test]
fn frame_size_must_be_a_valid_umem_chunk() {
assert!(
Config {
frame_size: 1024,
..Default::default()
}
.normalize()
.is_err()
);
assert!(
Config {
frame_size: 3000,
..Default::default()
}
.normalize()
.is_err()
);
assert!(
Config {
frame_size: (page_size() * 2) as u32,
..Default::default()
}
.normalize()
.is_err()
);
assert!(
Config {
frame_size: 2048,
..Default::default()
}
.normalize()
.is_ok()
);
}
#[test]
fn umem_always_splits_into_a_usable_rx_and_tx_pool() {
for n in [2u32, 3, 4096, 4097] {
let (rx, tx) = umem_split(n);
assert_eq!(rx + tx, n);
assert!(rx >= 1 && tx >= 1, "n={n} split {rx}/{tx}");
}
}
#[test]
fn umem_split_offsets() {
let frame_size = 4096u64;
let num_frames = 8u32;
let (rx, tx) = umem_split(num_frames);
let last_tx = ((rx + tx - 1) as u64) * frame_size;
assert_eq!((rx as u64) * frame_size, 4 * frame_size);
assert!(last_tx + frame_size <= num_frames as u64 * frame_size);
}
#[test]
fn ring_mmap_size_covers_descs() {
assert_eq!(ring_map_len(64, 8), 64 + 8 * 16);
}
#[test]
fn zerocopy_is_tried_first_then_copy() {
let c = bind_flag_candidates(0, true);
assert_eq!(c[0], XDP_ZEROCOPY | XDP_USE_NEED_WAKEUP);
assert_eq!(c[1], XDP_COPY | XDP_USE_NEED_WAKEUP);
assert_eq!(c[2], XDP_COPY);
}
#[test]
fn copy_mode_never_attempts_a_zerocopy_bind() {
let c = bind_flag_candidates(0, false);
assert!(c.iter().all(|f| f & XDP_ZEROCOPY == 0));
}
#[test]
fn extra_bind_flags_are_preserved() {
let extra = 1 << 6;
for f in bind_flag_candidates(extra, true) {
assert_eq!(f & extra, extra);
}
}
#[test]
fn ifreq_layout_matches_the_kernel_struct() {
assert_eq!(std::mem::size_of::<IfReq>(), 40);
let req = IfReq::new("eth0").unwrap();
assert_eq!(&req.name[..5], b"eth0\0");
assert!(IfReq::new("an-interface-name-that-is-far-too-long").is_err());
}
#[test]
fn unknown_interface_is_not_found() {
let e = if_nametoindex("pktkit-no-such-if").unwrap_err();
assert_eq!(e.kind(), io::ErrorKind::NotFound);
}
#[test]
fn opening_an_unknown_interface_fails_before_touching_bpf() {
let e = Device::open(Config {
interface: "pktkit-no-such-if".into(),
..Default::default()
})
.unwrap_err();
assert_eq!(e.kind(), io::ErrorKind::NotFound);
}
#[test]
fn busy_poll_defaults_match_the_rx_batch() {
let bp = BusyPoll::default();
assert_eq!(bp.budget as usize, BATCH);
assert!(bp.timeout_us > 0);
}
}