use crate::libc::{iface, socket};
mod netlink;
macro_rules! flag_strings {
($table:expr, $v:expr, $f:expr) => {{
let mut count = 0;
for (flag, s) in $table {
if (flag as u64 & $v) == 0 {
continue;
}
if count > 0 {
$f.write_str(" | ")?;
}
count += 1;
$f.write_str(s)?;
}
if count == 0 {
$f.write_str("-")?;
}
Ok(())
}};
}
#[derive(Copy, Clone)]
pub enum XdpModes {
Skb = 1 << 1,
Drv = 1 << 2,
Hardware = 1 << 3,
}
#[derive(Copy, Clone, Debug)]
pub enum XdpZeroCopy {
Unavailable,
Available,
MultiBuffer(u32),
}
impl XdpZeroCopy {
#[inline]
pub fn is_available(&self) -> bool {
!matches!(self, Self::Unavailable)
}
}
#[derive(Copy, Clone)]
pub enum XdpAct {
Basic = 1 << 0,
Redirect = 1 << 1,
NdoXmit = 1 << 2,
XskZeroCopy = 1 << 3,
HwOffload = 1 << 4,
RxSg = 1 << 5,
NdoXmitSg = 1 << 6,
Mask = (1 << 7) - 1,
}
#[derive(Copy, Clone)]
pub struct XdpFeatures(pub u64);
impl fmt::Debug for XdpFeatures {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
flag_strings!(
[
(XdpAct::Basic, "NETDEV_XDP_ACT_BASIC"),
(XdpAct::Redirect, "NETDEV_XDP_ACT_REDIRECT"),
(XdpAct::NdoXmit, "NETDEV_XDP_ACT_NDO_XMIT"),
(XdpAct::XskZeroCopy, "NETDEV_XDP_ACT_XSK_ZEROCOPY"),
(XdpAct::HwOffload, "NETDEV_XDP_ACT_HW_OFFLOAD"),
(XdpAct::RxSg, "NETDEV_XDP_ACT_RX_SG"),
(XdpAct::NdoXmitSg, "NETDEV_XDP_ACT_NDO_XMIT_SG"),
],
self.0,
f
)
}
}
impl XdpFeatures {
#[inline]
pub fn basic(self) -> bool {
(self.0 & XdpAct::Basic as u64) != 0
}
#[inline]
pub fn redirect(self) -> bool {
(self.0 & XdpAct::Redirect as u64) != 0
}
#[inline]
pub fn zero_copy(self) -> bool {
(self.0 & XdpAct::XskZeroCopy as u64) != 0
}
}
#[repr(u64)]
pub enum RxMetadataFlags {
Timestamp = 1 << 0,
Hash = 1 << 1,
VlanTag = 1 << 2,
}
#[derive(Copy, Clone)]
pub struct XdpRxMetadata(pub u64);
impl fmt::Debug for XdpRxMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
flag_strings!(
[
(
RxMetadataFlags::Timestamp,
"NETDEV_XDP_RX_METADATA_TIMESTAMP"
),
(RxMetadataFlags::Hash, "NETDEV_XDP_RX_METADATA_HASH"),
(RxMetadataFlags::VlanTag, "NETDEV_XDP_RX_METADATA_VLAN_TAG"),
],
self.0,
f
)
}
}
impl XdpRxMetadata {
#[inline]
pub fn timestamp(self) -> bool {
(self.0 & RxMetadataFlags::Timestamp as u64) != 0
}
#[inline]
pub fn hash(self) -> bool {
(self.0 & RxMetadataFlags::Hash as u64) != 0
}
#[inline]
pub fn vlan_tag(self) -> bool {
(self.0 & RxMetadataFlags::VlanTag as u64) != 0
}
}
#[repr(u64)]
pub enum TxMetadataFlags {
Timestamp = 1 << 0,
Checksum = 1 << 1,
}
#[derive(Copy, Clone)]
pub struct XdpTxMetadata(pub u64);
impl fmt::Debug for XdpTxMetadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
flag_strings!(
[
(TxMetadataFlags::Timestamp, "NETDEV_XSK_FLAGS_TX_TIMESTAMP"),
(TxMetadataFlags::Checksum, "NETDEV_XSK_FLAGS_TX_CHECKSUM"),
],
self.0,
f
)
}
}
impl XdpTxMetadata {
#[inline]
pub fn timestamp(self) -> bool {
(self.0 & TxMetadataFlags::Timestamp as u64) != 0
}
#[inline]
pub fn checksum(self) -> bool {
(self.0 & TxMetadataFlags::Checksum as u64) != 0
}
}
#[derive(Debug)]
pub struct NetdevCapabilities {
pub queue_count: u32,
pub xdp_features: XdpFeatures,
pub zero_copy: XdpZeroCopy,
pub rx_metadata: XdpRxMetadata,
pub tx_metadata: XdpTxMetadata,
}
#[derive(Copy, Clone)]
pub struct Queues {
pub rx_max: u32,
pub rx_current: u32,
pub tx_max: u32,
pub tx_current: u32,
}
#[derive(Copy, Clone)]
pub struct NicIndex(pub u32);
impl NicIndex {
#[inline]
pub fn new(index: u32) -> Self {
Self(index)
}
#[inline]
pub fn lookup_by_name(ifname: &std::ffi::CStr) -> std::io::Result<Option<Self>> {
let res = unsafe { iface::if_nametoindex(ifname.as_ptr().cast()) };
if res == 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(iface::ENODEV) {
Ok(None)
} else {
Err(err)
}
} else {
Ok(Some(Self(res)))
}
}
#[inline]
pub fn name(&self) -> std::io::Result<NicName> {
let mut name = [0; iface::IF_NAMESIZE];
if unsafe { !iface::if_indextoname(self.0, name.as_mut_ptr()).is_null() } {
let len = name
.iter()
.position(|n| *n == 0)
.unwrap_or(iface::IF_NAMESIZE);
Ok(NicName { arr: name, len })
} else {
Err(std::io::Error::last_os_error())
}
}
pub fn addresses(
&self,
) -> std::io::Result<(Option<std::net::Ipv4Addr>, Option<std::net::Ipv6Addr>)> {
unsafe {
let mut ifaddrs = std::mem::MaybeUninit::<*mut iface::ifaddrs>::uninit();
if iface::getifaddrs(ifaddrs.as_mut_ptr()) != 0 {
return Err(std::io::Error::last_os_error());
}
let ifaddrs = ifaddrs.assume_init();
let mut cur = ifaddrs.as_ref();
struct Ifaddrs(*mut iface::ifaddrs);
impl Drop for Ifaddrs {
fn drop(&mut self) {
unsafe { iface::freeifaddrs(self.0) };
}
}
let _ifa = Ifaddrs(ifaddrs);
let name = self.name()?;
let mut ipv4 = None;
let mut ipv6 = None;
while let Some(ifaddr) = cur {
cur = ifaddr.ifa_next.as_ref();
if iface::strncmp(name.arr.as_ptr(), ifaddr.ifa_name, name.len) != 0 {
continue;
}
let Some(addr) = ifaddr.ifa_addr.as_ref() else {
continue;
};
match addr.sa_family as socket::AddressFamily::Enum {
socket::AddressFamily::AF_INET => {
let addr = &*ifaddr.ifa_addr.cast::<socket::sockaddr_in>();
ipv4 = Some(std::net::Ipv4Addr::from_bits(u32::from_be(addr.sin_addr)));
}
socket::AddressFamily::AF_INET6 => {
let addr = &*ifaddr.ifa_addr.cast::<socket::sockaddr_in6>();
ipv6 = Some(addr.sin6_addr.into());
}
_ => {}
}
}
Ok((ipv4, ipv6))
}
}
pub fn queue_count(&self) -> std::io::Result<Queues> {
use std::os::fd::{AsRawFd, FromRawFd};
let socket = unsafe {
let fd = socket::socket(socket::AddressFamily::AF_LOCAL, socket::Kind::SOCK_DGRAM, 0);
if fd < 0 {
return Err(std::io::Error::last_os_error());
}
std::os::fd::OwnedFd::from_raw_fd(fd)
};
#[repr(C)]
struct Channels {
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,
}
const ETHTOOL_GCHANNELS: u32 = 0x0000003c;
let mut channels: Channels = unsafe { std::mem::zeroed() };
channels.cmd = ETHTOOL_GCHANNELS;
let mut ifr: iface::ifreq = unsafe { std::mem::zeroed() };
ifr.ifr_ifru.ifru_data = (&mut channels as *mut Channels).cast();
let name = self.name()?;
ifr.ifr_name[..name.len].copy_from_slice(&name.arr[..name.len]);
if unsafe {
iface::ioctl(
socket.as_raw_fd(),
iface::SIOCETHTOOL,
&mut ifr as *mut iface::ifreq,
)
} != 0
{
const PREFIX: &[u8] = b"/sys/class/net/";
const SUFFIX: &[u8] = b"/queues/";
const MAX: usize = PREFIX.len() + iface::IF_NAMESIZE + SUFFIX.len() + 1;
unsafe {
let mut dir_path = [0; MAX];
let mut start = 0;
dir_path[start..start + PREFIX.len()].copy_from_slice(std::slice::from_raw_parts(
PREFIX.as_ptr().cast(),
PREFIX.len(),
));
start += PREFIX.len();
dir_path[start..start + name.len].copy_from_slice(&name.arr[..name.len]);
start += name.len;
dir_path[start..start + SUFFIX.len()].copy_from_slice(std::slice::from_raw_parts(
SUFFIX.as_ptr().cast(),
SUFFIX.len(),
));
let dir = iface::opendir(dir_path.as_ptr());
if dir.is_null() {
return Err(std::io::Error::last_os_error());
}
struct Dir(*mut iface::DIR);
impl Drop for Dir {
fn drop(&mut self) {
unsafe {
iface::closedir(self.0);
}
}
}
let dir = Dir(dir);
channels = std::mem::zeroed();
while let Some(entry) = iface::readdir(dir.0).as_ref() {
if entry.d_type != iface::DT_DIR {
continue;
}
if entry.d_name[..2] == [b'r', b'x'] {
channels.max_rx += 1;
channels.rx_count += 1;
} else if entry.d_name[..2] == [b't', b'x'] {
channels.max_tx += 1;
channels.tx_count += 1;
}
}
}
}
Ok(Queues {
rx_max: channels.max_rx.max(channels.max_combined),
rx_current: channels.rx_count.max(channels.combined_count),
tx_max: channels.max_tx.max(channels.max_combined),
tx_current: channels.tx_count.max(channels.combined_count),
})
}
pub fn query_capabilities(&self) -> std::io::Result<NetdevCapabilities> {
std::thread::scope(|s| -> std::io::Result<NetdevCapabilities> {
let qc = s.spawn(|| self.queue_count().map_or(1, |queues| queues.rx_current));
let ndc = s.spawn(|| -> std::io::Result<NetdevCapabilities> { self.netdev_caps() });
let queue_count = qc.join().map_err(|_e| {
std::io::Error::new(
std::io::ErrorKind::Deadlock,
"panic occurred querying queue count",
)
})?;
let mut ndc = ndc.join().map_err(|_e| {
std::io::Error::new(
std::io::ErrorKind::TimedOut,
"panic occurred query device caps",
)
})??;
ndc.queue_count = queue_count;
Ok(ndc)
})
}
}
impl From<NicIndex> for u32 {
fn from(value: NicIndex) -> Self {
value.0
}
}
use std::fmt;
impl fmt::Debug for NicIndex {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Ok(name) = self.name() {
write!(f, "{} \"{}\"", self.0, name.as_str().unwrap_or("unknown"))
} else {
write!(f, "{} \"unknown\"", self.0)
}
}
}
impl PartialEq<u32> for NicIndex {
fn eq(&self, other: &u32) -> bool {
self.0 == *other
}
}
impl PartialEq<NicIndex> for NicIndex {
fn eq(&self, other: &NicIndex) -> bool {
self.0 == other.0
}
}
#[derive(Copy, Clone)]
pub struct NicName {
arr: [u8; iface::IF_NAMESIZE],
len: usize,
}
impl NicName {
#[inline]
pub fn as_str(&self) -> Option<&str> {
std::str::from_utf8(&self.arr[..self.len]).ok()
}
}
impl fmt::Debug for NicName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(s) = self.as_str() {
f.write_str(s)
} else {
f.write_str("non utf-8")
}
}
}
impl fmt::Display for NicName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(s) = self.as_str() {
f.write_str(s)
} else {
f.write_str("non utf-8")
}
}
}
pub struct InterfaceIter {
routes: String,
pos: usize,
}
impl InterfaceIter {
#[inline]
pub fn new() -> std::io::Result<Self> {
let routes = std::fs::read_to_string("/proc/net/route")?;
let pos = routes.find('\n').ok_or(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"route table didn't have a newline",
))? + 1;
Ok(Self { routes, pos })
}
}
impl Iterator for InterfaceIter {
type Item = NicIndex;
fn next(&mut self) -> Option<Self::Item> {
const IS_UP: u16 = iface::RTF_UP | iface::RTF_GATEWAY;
loop {
let end = self.routes[self.pos..].find('\n')?;
let line = &self.routes[self.pos..self.pos + end];
self.pos = self.pos + end + 1;
let mut iter = line.split(char::is_whitespace).filter_map(|s| {
let s = s.trim();
(!s.is_empty()).then_some(s)
});
let Some(name) = iter.next() else {
continue;
};
let Some(flags) = iter.nth(2).and_then(|f| u16::from_str_radix(f, 16).ok()) else {
continue;
};
if flags & IS_UP != IS_UP {
continue;
}
let mut ifname = [0u8; iface::IF_NAMESIZE];
ifname[..name.len()].copy_from_slice(name.as_bytes());
ifname[name.len()] = 0;
let Ok(Some(iface)) = NicIndex::lookup_by_name(
unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(&ifname) },
) else {
continue;
};
return Some(iface);
}
}
}
#[cfg(test)]
mod test {
#[test]
#[cfg_attr(miri, ignore)]
fn gets_features() {
let nic = super::InterfaceIter::new().unwrap().next().unwrap();
nic.query_capabilities().unwrap();
}
}