#[derive(Debug, Clone)]
pub struct TimestampCapabilities {
pub software_tx: bool,
pub software_rx: bool,
pub hardware_tx: bool,
pub hardware_rx: bool,
}
impl TimestampCapabilities {
#[cfg(target_os = "linux")]
pub fn query(interface_name: &str) -> std::io::Result<Self> {
use std::path::Path;
if interface_name.contains('/') || interface_name.contains('\0') {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"invalid interface name",
));
}
let sys_path = format!("/sys/class/net/{interface_name}");
if !Path::new(&sys_path).exists() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("interface {interface_name} not found"),
));
}
match query_ethtool_ts_info(interface_name) {
Ok(caps) => return Ok(caps),
Err(_) => {
}
}
let _has_device = Path::new(&format!("{sys_path}/device")).exists();
Ok(Self::software_only())
}
#[cfg(target_os = "freebsd")]
pub fn query(interface_name: &str) -> std::io::Result<Self> {
if interface_name.contains('/') || interface_name.contains('\0') {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"invalid interface name",
));
}
use std::ffi::CString;
let c_name = CString::new(interface_name).map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid interface name")
})?;
let idx = unsafe { libc::if_nametoindex(c_name.as_ptr()) };
if idx == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("interface not found: {interface_name}"),
));
}
Ok(Self::software_only())
}
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
pub fn query(_interface_name: &str) -> std::io::Result<Self> {
Ok(Self::software_only())
}
pub fn software_only() -> Self {
Self {
software_tx: true,
software_rx: true,
hardware_tx: false,
hardware_rx: false,
}
}
pub fn hardware() -> Self {
Self {
software_tx: true,
software_rx: true,
hardware_tx: true,
hardware_rx: true,
}
}
pub fn has_hardware(&self) -> bool {
self.hardware_tx || self.hardware_rx
}
pub fn has_software(&self) -> bool {
self.software_tx || self.software_rx
}
}
#[cfg(target_os = "linux")]
mod ethtool {
pub const ETHTOOL_GET_TS_INFO: u32 = 0x00000041;
pub const SIOCETHTOOL: libc::c_ulong = 0x8946;
pub const SOF_TIMESTAMPING_TX_HARDWARE: u32 = 1 << 0;
pub const SOF_TIMESTAMPING_TX_SOFTWARE: u32 = 1 << 1;
pub const SOF_TIMESTAMPING_RX_HARDWARE: u32 = 1 << 2;
pub const SOF_TIMESTAMPING_RX_SOFTWARE: u32 = 1 << 3;
#[repr(C)]
pub struct EthtoolTsInfo {
pub cmd: u32,
pub so_timestamping: u32,
pub phc_index: i32,
pub tx_types: u32,
pub tx_reserved: [u32; 3],
pub rx_filters: u32,
pub rx_reserved: [u32; 3],
}
#[repr(C)]
pub struct Ifreq {
pub ifr_name: [u8; libc::IFNAMSIZ],
pub ifr_data: *mut libc::c_void,
}
}
#[cfg(target_os = "linux")]
fn query_ethtool_ts_info(interface_name: &str) -> std::io::Result<TimestampCapabilities> {
use ethtool::*;
let mut ts_info: EthtoolTsInfo = unsafe { std::mem::zeroed() };
ts_info.cmd = ETHTOOL_GET_TS_INFO;
let mut ifr: Ifreq = unsafe { std::mem::zeroed() };
let name_bytes = interface_name.as_bytes();
if name_bytes.len() >= libc::IFNAMSIZ {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"interface name too long",
));
}
ifr.ifr_name[..name_bytes.len()].copy_from_slice(name_bytes);
ifr.ifr_data = &mut ts_info as *mut EthtoolTsInfo as *mut libc::c_void;
let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
if fd < 0 {
return Err(std::io::Error::last_os_error());
}
let ret = unsafe { libc::ioctl(fd, SIOCETHTOOL, &mut ifr) };
let errno = std::io::Error::last_os_error();
unsafe {
libc::close(fd);
}
if ret < 0 {
return Err(errno);
}
let so_ts = ts_info.so_timestamping;
Ok(TimestampCapabilities {
software_tx: so_ts & SOF_TIMESTAMPING_TX_SOFTWARE != 0,
software_rx: so_ts & SOF_TIMESTAMPING_RX_SOFTWARE != 0,
hardware_tx: so_ts & SOF_TIMESTAMPING_TX_HARDWARE != 0,
hardware_rx: so_ts & SOF_TIMESTAMPING_RX_HARDWARE != 0,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn software_only_defaults() {
let caps = TimestampCapabilities::software_only();
assert!(caps.software_tx);
assert!(caps.software_rx);
assert!(!caps.hardware_tx);
assert!(!caps.hardware_rx);
assert!(!caps.has_hardware());
assert!(caps.has_software());
}
#[test]
fn hardware_defaults() {
let caps = TimestampCapabilities::hardware();
assert!(caps.software_tx);
assert!(caps.software_rx);
assert!(caps.hardware_tx);
assert!(caps.hardware_rx);
assert!(caps.has_hardware());
assert!(caps.has_software());
}
#[test]
fn query_nonexistent_interface() {
let result = TimestampCapabilities::query("nonexistent_iface_xyz");
assert!(result.is_err());
}
#[test]
fn query_invalid_name_slash() {
let result = TimestampCapabilities::query("../etc/passwd");
assert!(result.is_err());
}
#[test]
fn query_loopback() {
match TimestampCapabilities::query("lo") {
Ok(caps) => {
assert!(caps.software_tx || caps.software_rx || !caps.has_hardware());
}
Err(e) => {
eprintln!("query(\"lo\") failed (may be expected in CI): {e}");
}
}
}
}