Skip to main content

iperf3_rs/
iperf.rs

1//! Minimal Rust wrapper around upstream libiperf.
2//!
3//! Most users should prefer [`crate::IperfCommand`]. This module keeps the FFI
4//! boundary localized and exposes only small value types at the crate root.
5
6use std::ffi::{CStr, CString};
7use std::os::raw::{c_char, c_double, c_int};
8use std::ptr::NonNull;
9
10use crate::{Error, ErrorKind, Result};
11
12#[allow(non_camel_case_types)]
13mod ffi {
14    use super::{c_char, c_double, c_int};
15
16    // libiperf owns this object; Rust only passes the opaque pointer back to C.
17    #[repr(C)]
18    pub struct iperf_test {
19        _private: [u8; 0],
20    }
21
22    pub type MetricsCallback = unsafe extern "C" fn(
23        *mut iperf_test,
24        c_double,
25        c_double,
26        c_double,
27        c_double,
28        c_double,
29        c_double,
30        c_double,
31        c_double,
32        c_double,
33        c_double,
34        c_double,
35        c_double,
36        c_double,
37        c_double,
38        c_double,
39        c_int,
40        c_int,
41        c_int,
42        c_int,
43        c_int,
44        c_int,
45        c_int,
46        c_int,
47        c_int,
48        c_int,
49        c_int,
50        c_int,
51        c_int,
52        c_int,
53    );
54
55    unsafe extern "C" {
56        pub fn iperf_new_test() -> *mut iperf_test;
57        pub fn iperf_defaults(test: *mut iperf_test) -> c_int;
58        pub fn iperf_free_test(test: *mut iperf_test);
59        pub fn iperf_parse_arguments(
60            test: *mut iperf_test,
61            argc: c_int,
62            argv: *mut *mut c_char,
63        ) -> c_int;
64        pub fn iperf_run_client(test: *mut iperf_test) -> c_int;
65        pub fn iperf_reset_test(test: *mut iperf_test);
66        pub fn iperf_get_test_role(test: *mut iperf_test) -> c_char;
67        pub fn iperf_get_test_one_off(test: *mut iperf_test) -> c_int;
68        pub fn iperf_get_test_json_output_string(test: *mut iperf_test) -> *const c_char;
69        pub fn iperf_get_iperf_version() -> *const c_char;
70
71        pub fn iperf3rs_enable_interval_metrics(
72            test: *mut iperf_test,
73            callback: Option<MetricsCallback>,
74        );
75        pub fn iperf3rs_run_server_once(test: *mut iperf_test) -> c_int;
76        pub fn iperf3rs_suppress_output(test: *mut iperf_test) -> c_int;
77        pub fn iperf3rs_current_errno() -> c_int;
78        pub fn iperf3rs_is_auth_test_error() -> c_int;
79        pub fn iperf3rs_current_error() -> *const c_char;
80        pub fn iperf3rs_ignore_sigpipe();
81        pub fn iperf3rs_usage_long() -> *mut c_char;
82        pub fn iperf3rs_free_string(value: *mut c_char);
83    }
84}
85
86pub(crate) use ffi::iperf_test as RawIperfTest;
87
88/// Role selected by libiperf after parsing iperf arguments.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90#[cfg_attr(feature = "serde", derive(serde::Serialize))]
91#[non_exhaustive]
92pub enum Role {
93    /// Client mode, equivalent to `iperf3 -c`.
94    Client,
95    /// Server mode, equivalent to `iperf3 -s`.
96    Server,
97    /// A role byte libiperf returned that this crate does not recognize.
98    Unknown(i8),
99}
100
101impl Default for Role {
102    fn default() -> Self {
103        Self::Unknown(0)
104    }
105}
106
107pub struct IperfTest {
108    ptr: NonNull<ffi::iperf_test>,
109}
110
111impl IperfTest {
112    pub fn new() -> Result<Self> {
113        let ptr = NonNull::new(unsafe { ffi::iperf_new_test() })
114            .ok_or_else(|| Error::internal("iperf_new_test returned null"))?;
115        let test = Self { ptr };
116        let rc = unsafe { ffi::iperf_defaults(test.as_ptr()) };
117        if rc < 0 {
118            return Err(Error::libiperf(format!(
119                "iperf_defaults failed: {}",
120                current_error()
121            )));
122        }
123        Ok(test)
124    }
125
126    pub(crate) fn as_ptr(&self) -> *mut RawIperfTest {
127        self.ptr.as_ptr()
128    }
129
130    pub fn parse_arguments(&mut self, args: &[String]) -> Result<()> {
131        // libiperf parses synchronously, so the CString backing storage only
132        // needs to stay alive for this call.
133        let cstrings = args
134            .iter()
135            .map(|arg| {
136                CString::new(arg.as_str())
137                    .map_err(|_| Error::invalid_argument(format!("argument contains NUL: {arg:?}")))
138            })
139            .collect::<Result<Vec<_>>>()?;
140        let mut argv = cstrings
141            .iter()
142            .map(|arg| arg.as_ptr() as *mut c_char)
143            .collect::<Vec<_>>();
144
145        let rc = unsafe {
146            ffi::iperf_parse_arguments(self.as_ptr(), argv.len() as c_int, argv.as_mut_ptr())
147        };
148        if rc < 0 {
149            return Err(Error::libiperf(format!(
150                "failed to parse iperf options: {}",
151                current_error()
152            )));
153        }
154        Ok(())
155    }
156
157    pub(crate) fn enable_interval_metrics(&mut self, callback: ffi::MetricsCallback) {
158        unsafe { ffi::iperf3rs_enable_interval_metrics(self.as_ptr(), Some(callback)) };
159    }
160
161    pub(crate) fn suppress_output(&mut self) -> Result<()> {
162        let rc = unsafe { ffi::iperf3rs_suppress_output(self.as_ptr()) };
163        if rc < 0 {
164            return Err(Error::internal("failed to suppress libiperf output"));
165        }
166        Ok(())
167    }
168
169    pub fn role(&self) -> Role {
170        match unsafe { ffi::iperf_get_test_role(self.as_ptr()) } as u8 as char {
171            'c' => Role::Client,
172            's' => Role::Server,
173            other => Role::Unknown(other as i8),
174        }
175    }
176
177    pub(crate) fn one_off(&self) -> bool {
178        (unsafe { ffi::iperf_get_test_one_off(self.as_ptr()) }) != 0
179    }
180
181    /// Return libiperf's retained JSON result, when JSON output was requested.
182    pub fn json_output(&self) -> Option<String> {
183        let ptr = unsafe { ffi::iperf_get_test_json_output_string(self.as_ptr()) };
184        if ptr.is_null() {
185            return None;
186        }
187        Some(
188            unsafe { CStr::from_ptr(ptr) }
189                .to_string_lossy()
190                .into_owned(),
191        )
192    }
193
194    pub fn run(&mut self) -> Result<()> {
195        unsafe { ffi::iperf3rs_ignore_sigpipe() };
196        match self.role() {
197            Role::Client => self.run_client(),
198            Role::Server => self.run_server(),
199            Role::Unknown(role) => Err(Error::invalid_argument(format!(
200                "iperf role was not set by arguments: {role}"
201            ))),
202        }
203    }
204
205    fn run_client(&mut self) -> Result<()> {
206        let rc = unsafe { ffi::iperf_run_client(self.as_ptr()) };
207        if rc < 0 {
208            return Err(Error::libiperf(format!(
209                "iperf client exited with error: {}",
210                current_error()
211            )));
212        }
213        Ok(())
214    }
215
216    fn run_server(&mut self) -> Result<()> {
217        loop {
218            // Upstream server mode handles one accepted test at a time and then
219            // resets the same iperf_test so a long-running server can accept more.
220            let rc = unsafe { ffi::iperf3rs_run_server_once(self.as_ptr()) };
221            if rc < 0 {
222                let error = current_error();
223                if rc < -1 {
224                    return Err(Error::libiperf(format!(
225                        "iperf server exited with error: {error}"
226                    )));
227                }
228                eprintln!("iperf server recovered from error: {error}");
229            }
230
231            unsafe { ffi::iperf_reset_test(self.as_ptr()) };
232
233            let auth_error = unsafe { ffi::iperf3rs_is_auth_test_error() } != 0;
234            if self.one_off() && rc != 2 {
235                // Keep upstream's special-case behavior: authentication failures
236                // in one-off mode should not terminate the server loop.
237                if rc < 0 && auth_error {
238                    continue;
239                }
240                return Ok(());
241            }
242        }
243    }
244}
245
246impl Drop for IperfTest {
247    fn drop(&mut self) {
248        unsafe { ffi::iperf_free_test(self.as_ptr()) };
249    }
250}
251
252pub(crate) fn current_error() -> String {
253    let ptr = unsafe { ffi::iperf3rs_current_error() };
254    if ptr.is_null() {
255        let errno = unsafe { ffi::iperf3rs_current_errno() };
256        return format!("unknown libiperf error ({errno})");
257    }
258    unsafe { CStr::from_ptr(ptr) }
259        .to_string_lossy()
260        .into_owned()
261}
262
263/// Return the upstream libiperf version string.
264pub fn libiperf_version() -> String {
265    let ptr = unsafe { ffi::iperf_get_iperf_version() };
266    if ptr.is_null() {
267        return "unknown".to_owned();
268    }
269    unsafe { CStr::from_ptr(ptr) }
270        .to_string_lossy()
271        .into_owned()
272}
273
274/// Render the upstream iperf3 long help text.
275///
276/// The CLI combines this text with iperf3-rs-specific options before printing
277/// `--help`.
278pub fn usage_long() -> Result<String> {
279    let ptr = unsafe { ffi::iperf3rs_usage_long() };
280    if ptr.is_null() {
281        return Err(Error::new(
282            ErrorKind::Libiperf,
283            "failed to render iperf usage text",
284        ));
285    }
286    let text = unsafe { CStr::from_ptr(ptr) }
287        .to_string_lossy()
288        .into_owned();
289    unsafe { ffi::iperf3rs_free_string(ptr) };
290    Ok(text)
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    use std::sync::Mutex;
297
298    static IPERF_TEST_LOCK: Mutex<()> = Mutex::new(());
299
300    #[test]
301    fn parser_sets_server_role() {
302        let _guard = IPERF_TEST_LOCK.lock().unwrap();
303        let mut test = IperfTest::new().unwrap();
304        test.parse_arguments(&["iperf3-rs".to_owned(), "-s".to_owned(), "-1".to_owned()])
305            .unwrap();
306
307        assert_eq!(test.role(), Role::Server);
308        assert!(test.json_output().is_none());
309    }
310
311    #[test]
312    fn parser_sets_client_role() {
313        let _guard = IPERF_TEST_LOCK.lock().unwrap();
314        let mut test = IperfTest::new().unwrap();
315        test.parse_arguments(&[
316            "iperf3-rs".to_owned(),
317            "-c".to_owned(),
318            "127.0.0.1".to_owned(),
319            "-t".to_owned(),
320            "1".to_owned(),
321        ])
322        .unwrap();
323
324        assert_eq!(test.role(), Role::Client);
325    }
326}