1#[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 #[cfg(target_os = "linux")]
22 pub fn query(interface_name: &str) -> std::io::Result<Self> {
23 use std::path::Path;
24
25 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 match query_ethtool_ts_info(interface_name) {
43 Ok(caps) => return Ok(caps),
44 Err(_) => {
45 }
47 }
48
49 let _has_device = Path::new(&format!("{sys_path}/device")).exists();
53
54 Ok(Self::software_only())
56 }
57
58 #[cfg(target_os = "freebsd")]
64 pub fn query(interface_name: &str) -> std::io::Result<Self> {
65 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 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 Ok(Self::software_only())
88 }
89
90 #[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 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 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 pub fn has_hardware(&self) -> bool {
120 self.hardware_tx || self.hardware_rx
121 }
122
123 pub fn has_software(&self) -> bool {
125 self.software_tx || self.software_rx
126 }
127}
128
129#[cfg(target_os = "linux")]
133mod ethtool {
134 pub const ETHTOOL_GET_TS_INFO: u32 = 0x00000041;
136
137 pub const SIOCETHTOOL: libc::c_ulong = 0x8946;
139
140 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 #[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 #[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#[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 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 ifr.ifr_data = &mut ts_info as *mut EthtoolTsInfo as *mut libc::c_void;
190
191 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 match TimestampCapabilities::query("lo") {
260 Ok(caps) => {
261 assert!(caps.software_tx || caps.software_rx || !caps.has_hardware());
265 }
266 Err(e) => {
267 eprintln!("query(\"lo\") failed (may be expected in CI): {e}");
269 }
270 }
271 }
272}