Skip to main content

rtime_net/
interface.rs

1/// Timestamping capabilities of a network interface.
2#[derive(Debug, Clone)]
3pub struct TimestampCapabilities {
4    pub software_tx: bool,
5    pub software_rx: bool,
6    pub hardware_tx: bool,
7    pub hardware_rx: bool,
8}
9
10impl TimestampCapabilities {
11    /// Query the timestamping capabilities of a network interface (Linux).
12    ///
13    /// Uses a best-effort approach:
14    /// 1. Checks if the interface exists via `/sys/class/net/{iface}`.
15    /// 2. Checks if it has a backing hardware device via `/sys/class/net/{iface}/device/`.
16    ///    Hardware devices with PCI backing often support hardware timestamping.
17    /// 3. Attempts `SIOCETHTOOL` with `ETHTOOL_GET_TS_INFO` ioctl for precise capability detection.
18    /// 4. Falls back to `software_only()` if detection fails.
19    ///
20    /// Software timestamping is always assumed available on Linux (kernel provides it).
21    #[cfg(target_os = "linux")]
22    pub fn query(interface_name: &str) -> std::io::Result<Self> {
23        use std::path::Path;
24
25        // Validate interface name (prevent path traversal).
26        if interface_name.contains('/') || interface_name.contains('\0') {
27            return Err(std::io::Error::new(
28                std::io::ErrorKind::InvalidInput,
29                "invalid interface name",
30            ));
31        }
32
33        let sys_path = format!("/sys/class/net/{interface_name}");
34        if !Path::new(&sys_path).exists() {
35            return Err(std::io::Error::new(
36                std::io::ErrorKind::NotFound,
37                format!("interface {interface_name} not found"),
38            ));
39        }
40
41        // Try the ioctl-based approach first for accurate detection.
42        match query_ethtool_ts_info(interface_name) {
43            Ok(caps) => return Ok(caps),
44            Err(_) => {
45                // ioctl failed -- fall back to sysfs heuristic.
46            }
47        }
48
49        // Heuristic: if the interface has a backing PCI/platform device,
50        // it *might* support hardware timestamping. But without ioctl
51        // confirmation we cannot be sure, so we only report software.
52        let _has_device = Path::new(&format!("{sys_path}/device")).exists();
53
54        // Software timestamping is always available on Linux.
55        Ok(Self::software_only())
56    }
57
58    /// Query the timestamping capabilities of a network interface (FreeBSD).
59    ///
60    /// Verifies the interface exists using `if_nametoindex()`. FreeBSD does not
61    /// expose hardware timestamp capabilities through ethtool, so software-only
62    /// timestamps are assumed.
63    #[cfg(target_os = "freebsd")]
64    pub fn query(interface_name: &str) -> std::io::Result<Self> {
65        // Validate interface name.
66        if interface_name.contains('/') || interface_name.contains('\0') {
67            return Err(std::io::Error::new(
68                std::io::ErrorKind::InvalidInput,
69                "invalid interface name",
70            ));
71        }
72
73        // Verify interface exists using if_nametoindex.
74        use std::ffi::CString;
75        let c_name = CString::new(interface_name).map_err(|_| {
76            std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid interface name")
77        })?;
78        let idx = unsafe { libc::if_nametoindex(c_name.as_ptr()) };
79        if idx == 0 {
80            return Err(std::io::Error::new(
81                std::io::ErrorKind::NotFound,
82                format!("interface not found: {interface_name}"),
83            ));
84        }
85
86        // FreeBSD: assume software-only timestamps.
87        Ok(Self::software_only())
88    }
89
90    /// Query the timestamping capabilities of a network interface (unsupported platform).
91    ///
92    /// Returns software-only capabilities as a safe fallback.
93    #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
94    pub fn query(_interface_name: &str) -> std::io::Result<Self> {
95        Ok(Self::software_only())
96    }
97
98    /// Return default software-only capabilities (fallback).
99    pub fn software_only() -> Self {
100        Self {
101            software_tx: true,
102            software_rx: true,
103            hardware_tx: false,
104            hardware_rx: false,
105        }
106    }
107
108    /// Return capabilities indicating full hardware timestamping support.
109    pub fn hardware() -> Self {
110        Self {
111            software_tx: true,
112            software_rx: true,
113            hardware_tx: true,
114            hardware_rx: true,
115        }
116    }
117
118    /// Whether any form of hardware timestamping is available.
119    pub fn has_hardware(&self) -> bool {
120        self.hardware_tx || self.hardware_rx
121    }
122
123    /// Whether software timestamping is available (always true on Linux/FreeBSD).
124    pub fn has_software(&self) -> bool {
125        self.software_tx || self.software_rx
126    }
127}
128
129// ---------------------------------------------------------------------------
130// Linux-specific ETHTOOL ioctl-based timestamp capability query
131// ---------------------------------------------------------------------------
132#[cfg(target_os = "linux")]
133mod ethtool {
134    /// ETHTOOL command number for `ETHTOOL_GET_TS_INFO`.
135    pub const ETHTOOL_GET_TS_INFO: u32 = 0x00000041;
136
137    /// SIOCETHTOOL ioctl request number.
138    pub const SIOCETHTOOL: libc::c_ulong = 0x8946;
139
140    /// SOF_TIMESTAMPING flag bits we care about.
141    pub const SOF_TIMESTAMPING_TX_HARDWARE: u32 = 1 << 0;
142    pub const SOF_TIMESTAMPING_TX_SOFTWARE: u32 = 1 << 1;
143    pub const SOF_TIMESTAMPING_RX_HARDWARE: u32 = 1 << 2;
144    pub const SOF_TIMESTAMPING_RX_SOFTWARE: u32 = 1 << 3;
145
146    /// Kernel struct `ethtool_ts_info` (simplified -- we only read the first fields).
147    /// See linux/ethtool.h.
148    #[repr(C)]
149    pub struct EthtoolTsInfo {
150        pub cmd: u32,
151        pub so_timestamping: u32,
152        pub phc_index: i32,
153        pub tx_types: u32,
154        pub tx_reserved: [u32; 3],
155        pub rx_filters: u32,
156        pub rx_reserved: [u32; 3],
157    }
158
159    /// Kernel struct `ifreq` -- 40 bytes on x86_64.
160    /// We only use `ifr_name` (first 16 bytes) and `ifr_data` (pointer at offset 16).
161    #[repr(C)]
162    pub struct Ifreq {
163        pub ifr_name: [u8; libc::IFNAMSIZ],
164        pub ifr_data: *mut libc::c_void,
165    }
166}
167
168/// Attempt to query timestamping capabilities via SIOCETHTOOL ioctl (Linux only).
169#[cfg(target_os = "linux")]
170fn query_ethtool_ts_info(interface_name: &str) -> std::io::Result<TimestampCapabilities> {
171    use ethtool::*;
172
173    let mut ts_info: EthtoolTsInfo = unsafe { std::mem::zeroed() };
174    ts_info.cmd = ETHTOOL_GET_TS_INFO;
175
176    let mut ifr: Ifreq = unsafe { std::mem::zeroed() };
177
178    // Copy interface name into the fixed buffer.
179    let name_bytes = interface_name.as_bytes();
180    if name_bytes.len() >= libc::IFNAMSIZ {
181        return Err(std::io::Error::new(
182            std::io::ErrorKind::InvalidInput,
183            "interface name too long",
184        ));
185    }
186    ifr.ifr_name[..name_bytes.len()].copy_from_slice(name_bytes);
187    // NUL terminator is already there because we zeroed the struct.
188
189    ifr.ifr_data = &mut ts_info as *mut EthtoolTsInfo as *mut libc::c_void;
190
191    // Open a temporary socket for the ioctl.
192    let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
193    if fd < 0 {
194        return Err(std::io::Error::last_os_error());
195    }
196
197    let ret = unsafe { libc::ioctl(fd, SIOCETHTOOL, &mut ifr) };
198    let errno = std::io::Error::last_os_error();
199
200    unsafe {
201        libc::close(fd);
202    }
203
204    if ret < 0 {
205        return Err(errno);
206    }
207
208    let so_ts = ts_info.so_timestamping;
209
210    Ok(TimestampCapabilities {
211        software_tx: so_ts & SOF_TIMESTAMPING_TX_SOFTWARE != 0,
212        software_rx: so_ts & SOF_TIMESTAMPING_RX_SOFTWARE != 0,
213        hardware_tx: so_ts & SOF_TIMESTAMPING_TX_HARDWARE != 0,
214        hardware_rx: so_ts & SOF_TIMESTAMPING_RX_HARDWARE != 0,
215    })
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn software_only_defaults() {
224        let caps = TimestampCapabilities::software_only();
225        assert!(caps.software_tx);
226        assert!(caps.software_rx);
227        assert!(!caps.hardware_tx);
228        assert!(!caps.hardware_rx);
229        assert!(!caps.has_hardware());
230        assert!(caps.has_software());
231    }
232
233    #[test]
234    fn hardware_defaults() {
235        let caps = TimestampCapabilities::hardware();
236        assert!(caps.software_tx);
237        assert!(caps.software_rx);
238        assert!(caps.hardware_tx);
239        assert!(caps.hardware_rx);
240        assert!(caps.has_hardware());
241        assert!(caps.has_software());
242    }
243
244    #[test]
245    fn query_nonexistent_interface() {
246        let result = TimestampCapabilities::query("nonexistent_iface_xyz");
247        assert!(result.is_err());
248    }
249
250    #[test]
251    fn query_invalid_name_slash() {
252        let result = TimestampCapabilities::query("../etc/passwd");
253        assert!(result.is_err());
254    }
255
256    #[test]
257    fn query_loopback() {
258        // `lo` exists on virtually all Linux systems.
259        match TimestampCapabilities::query("lo") {
260            Ok(caps) => {
261                // Loopback should report at least software capabilities.
262                // The ioctl may succeed or fail depending on kernel -- either way
263                // we fall back to software_only().
264                assert!(caps.software_tx || caps.software_rx || !caps.has_hardware());
265            }
266            Err(e) => {
267                // In very restricted environments lo might not be visible.
268                eprintln!("query(\"lo\") failed (may be expected in CI): {e}");
269            }
270        }
271    }
272}