below_ethtool/
reader.rs

1// Copyright (c) Facebook, Inc. and its affiliates.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::alloc;
16use std::ffi::CStr;
17use std::mem;
18use std::os::fd::AsRawFd;
19use std::os::fd::OwnedFd;
20use std::ptr;
21use std::str;
22
23use nix::errno::Errno;
24use nix::libc;
25use nix::sys::socket::socket;
26use nix::sys::socket::AddressFamily;
27use nix::sys::socket::SockFlag;
28use nix::sys::socket::SockType;
29
30use crate::errors::EthtoolError;
31use crate::ethtool_sys;
32const ETH_GSTATS_LEN: usize = 8;
33
34fn if_name_bytes(if_name: &str) -> [libc::c_char; libc::IF_NAMESIZE] {
35    let mut it = if_name.as_bytes().iter().copied();
36    [0; libc::IF_NAMESIZE].map(|_| it.next().unwrap_or(0) as libc::c_char)
37}
38
39fn ioctl(
40    fd: &OwnedFd,
41    if_name: [libc::c_char; libc::IF_NAMESIZE],
42    data: *mut libc::c_char,
43) -> Result<(), Errno> {
44    let ifr = libc::ifreq {
45        ifr_name: if_name,
46        ifr_ifru: libc::__c_anonymous_ifr_ifru { ifru_data: data },
47    };
48
49    // The SIOCETHTOOL conversion is necessary because POSIX (and thus libcs like musl) defines the
50    // `ioctl` request argument as `c_int`, while glibc and nix::libc use `c_ulong`. In C, this
51    // discrepancy is hidden behind typeless #defines, but in Rust, it becomes a type mismatch.
52    //
53    // By converting `libc::SIOCETHTOOL` (likely defined as `c_ulong`) to the libc's native type,
54    // we ensure portability across different libc implementations. On glibc this does nothing, but
55    // this conversion prevents type errors on libcs that use `c_int` for the request argument,
56    // such as musl.
57    #[allow(clippy::useless_conversion)]
58    let exit_code = unsafe { libc::ioctl(fd.as_raw_fd(), libc::SIOCETHTOOL as _, &ifr) };
59    if exit_code != 0 {
60        return Err(Errno::from_i32(exit_code));
61    }
62    Ok(())
63}
64
65/// Parses the byte array returned by ioctl for ETHTOOL_GSTRINGS command.
66/// In case of error during parsing any stat name,
67/// the function returns a `ParseError`.
68fn parse_names(data: &[u8]) -> Result<Vec<String>, EthtoolError> {
69    let names = data
70        .chunks(ethtool_sys::ETH_GSTRING_LEN as usize)
71        .map(|chunk| {
72            // Find the position of the null terminator for specific stat name
73            let c_str = CStr::from_bytes_until_nul(chunk);
74            match c_str {
75                Ok(c_str) => Ok(c_str.to_string_lossy().into_owned()),
76                Err(err) => Err(EthtoolError::ParseError(err.to_string())),
77            }
78        })
79        .collect::<Result<Vec<String>, EthtoolError>>()?;
80
81    Ok(names)
82}
83
84/// Parses the byte array returned by ioctl for ETHTOOL_GSTATS command.
85/// In case of error during parsing any feature,
86/// the function returns a `ParseError`.
87fn parse_values(data: &[u64], length: usize) -> Result<Vec<u64>, EthtoolError> {
88    let values = data.iter().take(length).copied().collect::<Vec<u64>>();
89
90    Ok(values)
91}
92
93struct StringSetInfo {
94    layout: alloc::Layout,
95    ptr: *mut ethtool_sys::ethtool_sset_info,
96}
97
98impl StringSetInfo {
99    /// Allocates memory for ethtool_sset_info struct and initializes it.
100    fn new() -> Result<Self, EthtoolError> {
101        // Calculate the layout with proper alignment
102        let layout = alloc::Layout::from_size_align(
103            mem::size_of::<ethtool_sys::ethtool_sset_info>(),
104            mem::align_of::<ethtool_sys::ethtool_sset_info>(),
105        )
106        .map_err(EthtoolError::CStructInitError)?;
107
108        // Allocate memory for the struct
109        let sset_info_ptr = unsafe { alloc::alloc(layout) } as *mut ethtool_sys::ethtool_sset_info;
110
111        // Initialize the fields of the struct
112        unsafe {
113            let cmd_ptr = ptr::addr_of_mut!((*sset_info_ptr).cmd);
114            let reserved_ptr = ptr::addr_of_mut!((*sset_info_ptr).reserved);
115            let sset_mask_ptr = ptr::addr_of_mut!((*sset_info_ptr).sset_mask);
116            let data_ptr = ptr::addr_of_mut!((*sset_info_ptr).data);
117
118            cmd_ptr.write(ethtool_sys::ETHTOOL_GSSET_INFO);
119            reserved_ptr.write(1u32);
120            sset_mask_ptr.write((1 << ethtool_sys::ethtool_stringset_ETH_SS_STATS) as u64);
121
122            data_ptr.write_bytes(0u8, mem::size_of::<u32>());
123        }
124
125        Ok(StringSetInfo {
126            layout,
127            ptr: sset_info_ptr,
128        })
129    }
130
131    fn data(&self) -> Result<usize, EthtoolError> {
132        unsafe {
133            let value = self.ptr.as_ref().ok_or(EthtoolError::CStructReadError())?;
134            Ok(value.data.as_ptr().read() as usize)
135        }
136    }
137}
138
139impl Drop for StringSetInfo {
140    fn drop(&mut self) {
141        unsafe {
142            alloc::dealloc(self.ptr as *mut u8, self.layout);
143        }
144    }
145}
146
147struct GStrings {
148    length: usize,
149    layout: alloc::Layout,
150    ptr: *mut ethtool_sys::ethtool_gstrings,
151}
152
153impl GStrings {
154    fn new(length: usize) -> Result<Self, EthtoolError> {
155        let data_length = length * ethtool_sys::ETH_GSTRING_LEN as usize;
156
157        // Calculate the layout with proper alignment based on the struct itself
158        let layout = alloc::Layout::from_size_align(
159            mem::size_of::<ethtool_sys::ethtool_gstrings>() + data_length * mem::size_of::<u8>(),
160            mem::align_of::<ethtool_sys::ethtool_gstrings>(),
161        )
162        .map_err(EthtoolError::CStructInitError)?;
163
164        // Allocate memory for the struct
165        let gstrings_ptr = unsafe { alloc::alloc(layout) } as *mut ethtool_sys::ethtool_gstrings;
166        if gstrings_ptr.is_null() {
167            return Err(EthtoolError::AllocationFailure());
168        }
169
170        // Initialize the fields of the struct using raw pointers
171        unsafe {
172            let cmd_ptr = ptr::addr_of_mut!((*gstrings_ptr).cmd);
173            let ss_ptr = ptr::addr_of_mut!((*gstrings_ptr).string_set);
174            let data_ptr = ptr::addr_of_mut!((*gstrings_ptr).data);
175
176            cmd_ptr.write(ethtool_sys::ETHTOOL_GSTRINGS);
177            ss_ptr.write(ethtool_sys::ethtool_stringset_ETH_SS_STATS);
178
179            // Initialize the data field with zeros
180            data_ptr.write_bytes(0u8, data_length);
181        }
182
183        Ok(GStrings {
184            length,
185            layout,
186            ptr: gstrings_ptr,
187        })
188    }
189
190    fn data(&self) -> Result<&[u8], EthtoolError> {
191        unsafe {
192            Ok(std::slice::from_raw_parts(
193                (*self.ptr).data.as_ptr(),
194                self.length * ethtool_sys::ETH_GSTRING_LEN as usize,
195            ))
196        }
197    }
198}
199
200impl Drop for GStrings {
201    fn drop(&mut self) {
202        unsafe {
203            alloc::dealloc(self.ptr as *mut u8, self.layout);
204        }
205    }
206}
207
208struct GStats {
209    length: usize,
210    layout: alloc::Layout,
211    ptr: *mut ethtool_sys::ethtool_stats,
212}
213
214impl GStats {
215    /// Allocates memory for ethtool_stats struct and initializes it.
216    fn new(length: usize) -> Result<Self, EthtoolError> {
217        let data_length = length * ETH_GSTATS_LEN;
218
219        // Calculate the layout with proper alignment
220        let layout = alloc::Layout::from_size_align(
221            mem::size_of::<ethtool_sys::ethtool_stats>() + data_length * mem::size_of::<u64>(),
222            mem::align_of::<ethtool_sys::ethtool_stats>(),
223        )
224        .map_err(EthtoolError::CStructInitError)?;
225
226        // Allocate memory for the struct
227        let stats_ptr = unsafe { alloc::alloc(layout) } as *mut ethtool_sys::ethtool_stats;
228        if stats_ptr.is_null() {
229            return Err(EthtoolError::AllocationFailure());
230        }
231
232        // Initialize the fields of the struct
233        unsafe {
234            let cmd_ptr = ptr::addr_of_mut!((*stats_ptr).cmd);
235            let data_ptr = ptr::addr_of_mut!((*stats_ptr).data);
236
237            cmd_ptr.write(ethtool_sys::ETHTOOL_GSTATS);
238
239            // Initialize the data field with zeros
240            let data_bytes = data_length * mem::size_of::<u64>();
241            data_ptr.write_bytes(0u8, data_bytes);
242        }
243
244        Ok(GStats {
245            length,
246            layout,
247            ptr: stats_ptr,
248        })
249    }
250
251    fn data(&self) -> Result<&[u64], EthtoolError> {
252        unsafe {
253            Ok(std::slice::from_raw_parts(
254                (*self.ptr).data.as_ptr(),
255                self.length * ETH_GSTATS_LEN,
256            ))
257        }
258    }
259}
260
261impl Drop for GStats {
262    fn drop(&mut self) {
263        unsafe {
264            alloc::dealloc(self.ptr as *mut u8, self.layout);
265        }
266    }
267}
268
269/// A trait for reading stats using ethtool.
270///
271/// This trait allows mocking the ethtool calls for unit testing.
272pub trait EthtoolReadable {
273    fn new(if_name: &str) -> Result<Self, EthtoolError>
274    where
275        Self: Sized;
276    fn stats(&self) -> Result<Vec<(String, u64)>, EthtoolError>;
277}
278
279pub struct Ethtool {
280    sock_fd: OwnedFd,
281    if_name: [libc::c_char; libc::IF_NAMESIZE],
282}
283
284impl Ethtool {
285    /// Get the number of stats using ETHTOOL_GSSET_INFO command
286    fn gsset_info(&self) -> Result<usize, EthtoolError> {
287        let sset_info = StringSetInfo::new()?;
288        let data = sset_info.ptr as *mut libc::c_char;
289
290        match ioctl(&self.sock_fd, self.if_name, data) {
291            Ok(_) => Ok(sset_info.data()?),
292            Err(errno) => Err(EthtoolError::SocketError(errno)),
293        }
294    }
295
296    /// Get the feature names using ETHTOOL_GSTRINGS command
297    fn gstrings(&self, length: usize) -> Result<Vec<String>, EthtoolError> {
298        let gstrings = GStrings::new(length)?;
299        let data = gstrings.ptr as *mut libc::c_char;
300
301        match ioctl(&self.sock_fd, self.if_name, data) {
302            Ok(_) => parse_names(gstrings.data()?),
303            Err(errno) => Err(EthtoolError::GStringsReadError(errno)),
304        }
305    }
306
307    /// Get the statistics for the features using ETHTOOL_GSTATS command
308    fn gstats(&self, features: &[String]) -> Result<Vec<u64>, EthtoolError> {
309        let length = features.len();
310        let gstats = GStats::new(length)?;
311        let data = gstats.ptr as *mut libc::c_char;
312
313        match ioctl(&self.sock_fd, self.if_name, data) {
314            Ok(_) => parse_values(gstats.data()?, length),
315            Err(errno) => Err(EthtoolError::GStatsReadError(errno)),
316        }
317    }
318}
319
320impl EthtoolReadable for Ethtool {
321    fn new(if_name: &str) -> Result<Self, EthtoolError> {
322        match socket(
323            AddressFamily::Inet,
324            SockType::Datagram,
325            SockFlag::empty(),
326            None,
327        ) {
328            Ok(sock_fd) => Ok(Ethtool {
329                sock_fd,
330                if_name: if_name_bytes(if_name),
331            }),
332            Err(errno) => Err(EthtoolError::SocketError(errno)),
333        }
334    }
335
336    /// Get statistics using ethtool
337    /// Equivalent to `ethtool -S <ifname>` command
338    fn stats(&self) -> Result<Vec<(String, u64)>, EthtoolError> {
339        let length = self.gsset_info()?;
340
341        let features = self.gstrings(length)?;
342        let values = self.gstats(&features)?;
343
344        let final_stats = features.into_iter().zip(values).collect();
345        Ok(final_stats)
346    }
347}