Skip to main content

rscamper/
ping.rs

1// rscamper - Rust wrapper for scamper ping results
2//
3// Copyright (C) 2026 Dimitrios Giakatos
4//
5// This program is free software; you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, version 3.
8
9use std::ffi::CStr;
10use std::time::{Duration, UNIX_EPOCH, SystemTime};
11
12use crate::addr::ScamperAddr;
13use crate::list::{ScamperList, ScamperCycle};
14use crate::ffi::scamper_ping::{self, ScamperPingT, ScamperPingProbeT, ScamperPingReplyT,
15    ScamperPingStatsT,
16    SCAMPER_PING_STOP_NONE, SCAMPER_PING_STOP_COMPLETED, SCAMPER_PING_STOP_ERROR,
17    SCAMPER_PING_STOP_HALTED, SCAMPER_PING_STOP_INPROGRESS};
18
19fn timeval_to_duration(tv: *const libc::timeval) -> Option<Duration> {
20    if tv.is_null() {
21        return None;
22    }
23    let tv = unsafe { &*tv };
24    Some(Duration::new(tv.tv_sec as u64, tv.tv_usec as u32 * 1000))
25}
26
27fn timeval_to_systemtime(tv: *const libc::timeval) -> Option<SystemTime> {
28    let d = timeval_to_duration(tv)?;
29    Some(UNIX_EPOCH + d)
30}
31
32/// Stop reasons for a ping.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[repr(u8)]
35pub enum PingStopReason {
36    None       = SCAMPER_PING_STOP_NONE,
37    Completed  = SCAMPER_PING_STOP_COMPLETED,
38    Error      = SCAMPER_PING_STOP_ERROR,
39    Halted     = SCAMPER_PING_STOP_HALTED,
40    InProgress = SCAMPER_PING_STOP_INPROGRESS,
41    Unknown(u8),
42}
43
44impl From<u8> for PingStopReason {
45    fn from(v: u8) -> Self {
46        match v {
47            SCAMPER_PING_STOP_NONE       => Self::None,
48            SCAMPER_PING_STOP_COMPLETED  => Self::Completed,
49            SCAMPER_PING_STOP_ERROR      => Self::Error,
50            SCAMPER_PING_STOP_HALTED     => Self::Halted,
51            SCAMPER_PING_STOP_INPROGRESS => Self::InProgress,
52            other                        => Self::Unknown(other),
53        }
54    }
55}
56
57/// Ping statistics.
58pub struct ScamperPingStats {
59    inner: *mut ScamperPingStatsT,
60}
61
62impl ScamperPingStats {
63    pub fn nreplies(&self) -> u32 {
64        unsafe { scamper_ping::scamper_ping_stats_nreplies_get(self.inner) }
65    }
66
67    pub fn ndups(&self) -> u32 {
68        unsafe { scamper_ping::scamper_ping_stats_ndups_get(self.inner) }
69    }
70
71    pub fn nloss(&self) -> u32 {
72        unsafe { scamper_ping::scamper_ping_stats_nloss_get(self.inner) }
73    }
74
75    pub fn nerrs(&self) -> u32 {
76        unsafe { scamper_ping::scamper_ping_stats_nerrs_get(self.inner) }
77    }
78
79    pub fn min_rtt(&self) -> Option<Duration> {
80        let tv = unsafe { scamper_ping::scamper_ping_stats_min_rtt_get(self.inner) };
81        timeval_to_duration(tv)
82    }
83
84    pub fn max_rtt(&self) -> Option<Duration> {
85        let tv = unsafe { scamper_ping::scamper_ping_stats_max_rtt_get(self.inner) };
86        timeval_to_duration(tv)
87    }
88
89    pub fn avg_rtt(&self) -> Option<Duration> {
90        let tv = unsafe { scamper_ping::scamper_ping_stats_avg_rtt_get(self.inner) };
91        timeval_to_duration(tv)
92    }
93
94    pub fn stddev_rtt(&self) -> Option<Duration> {
95        let tv = unsafe { scamper_ping::scamper_ping_stats_stddev_rtt_get(self.inner) };
96        timeval_to_duration(tv)
97    }
98}
99
100impl Drop for ScamperPingStats {
101    fn drop(&mut self) {
102        unsafe { scamper_ping::scamper_ping_stats_free(self.inner) };
103    }
104}
105
106unsafe impl Send for ScamperPingStats {}
107unsafe impl Sync for ScamperPingStats {}
108
109/// A single ping reply.
110pub struct ScamperPingReply {
111    inner: *mut ScamperPingReplyT,
112}
113
114impl ScamperPingReply {
115    pub(crate) unsafe fn from_ptr(ptr: *mut ScamperPingReplyT) -> Option<Self> {
116        if ptr.is_null() {
117            return None;
118        }
119        let ptr = unsafe { scamper_ping::scamper_ping_reply_use(ptr) };
120        Some(ScamperPingReply { inner: ptr })
121    }
122
123    pub fn addr(&self) -> Option<ScamperAddr> {
124        let ptr = unsafe { scamper_ping::scamper_ping_reply_addr_get(self.inner) };
125        unsafe { ScamperAddr::from_ptr(ptr) }
126    }
127
128    pub fn proto(&self) -> u8 {
129        unsafe { scamper_ping::scamper_ping_reply_proto_get(self.inner) }
130    }
131
132    pub fn ttl(&self) -> u8 {
133        unsafe { scamper_ping::scamper_ping_reply_ttl_get(self.inner) }
134    }
135
136    pub fn size(&self) -> u16 {
137        unsafe { scamper_ping::scamper_ping_reply_size_get(self.inner) }
138    }
139
140    pub fn ipid(&self) -> u16 {
141        unsafe { scamper_ping::scamper_ping_reply_ipid_get(self.inner) }
142    }
143
144    pub fn rtt(&self) -> Option<Duration> {
145        let tv = unsafe { scamper_ping::scamper_ping_reply_rtt_get(self.inner) };
146        timeval_to_duration(tv)
147    }
148
149    pub fn is_icmp(&self) -> bool {
150        unsafe { scamper_ping::scamper_ping_reply_is_icmp(self.inner) != 0 }
151    }
152
153    pub fn is_tcp(&self) -> bool {
154        unsafe { scamper_ping::scamper_ping_reply_is_tcp(self.inner) != 0 }
155    }
156
157    pub fn is_udp(&self) -> bool {
158        unsafe { scamper_ping::scamper_ping_reply_is_udp(self.inner) != 0 }
159    }
160
161    pub fn is_icmp_echo_reply(&self) -> bool {
162        unsafe { scamper_ping::scamper_ping_reply_is_icmp_echo_reply(self.inner) != 0 }
163    }
164
165    pub fn is_icmp_unreach(&self) -> bool {
166        unsafe { scamper_ping::scamper_ping_reply_is_icmp_unreach(self.inner) != 0 }
167    }
168
169    pub fn is_icmp_ttl_exp(&self) -> bool {
170        unsafe { scamper_ping::scamper_ping_reply_is_icmp_ttl_exp(self.inner) != 0 }
171    }
172
173    pub fn is_icmp_ptb(&self) -> bool {
174        unsafe { scamper_ping::scamper_ping_reply_is_icmp_ptb(self.inner) != 0 }
175    }
176
177    pub fn icmp_type(&self) -> u8 {
178        unsafe { scamper_ping::scamper_ping_reply_icmp_type_get(self.inner) }
179    }
180
181    pub fn icmp_code(&self) -> u8 {
182        unsafe { scamper_ping::scamper_ping_reply_icmp_code_get(self.inner) }
183    }
184
185    pub fn tcp_flags(&self) -> u8 {
186        unsafe { scamper_ping::scamper_ping_reply_tcp_flags_get(self.inner) }
187    }
188
189    pub fn flags(&self) -> u32 {
190        unsafe { scamper_ping::scamper_ping_reply_flags_get(self.inner) }
191    }
192
193    pub fn ifname(&self) -> Option<&str> {
194        let ptr = unsafe { scamper_ping::scamper_ping_reply_ifname_get(self.inner) };
195        if ptr.is_null() {
196            None
197        } else {
198            Some(unsafe { CStr::from_ptr(ptr) }.to_str().unwrap_or(""))
199        }
200    }
201}
202
203impl Drop for ScamperPingReply {
204    fn drop(&mut self) {
205        unsafe { scamper_ping::scamper_ping_reply_free(self.inner) };
206    }
207}
208
209unsafe impl Send for ScamperPingReply {}
210unsafe impl Sync for ScamperPingReply {}
211
212/// A single ping probe.
213pub struct ScamperPingProbe {
214    inner: *mut ScamperPingProbeT,
215    ping:  *const ScamperPingT,
216}
217
218impl ScamperPingProbe {
219    pub(crate) unsafe fn from_ptr(
220        ping: *const ScamperPingT,
221        ptr: *mut ScamperPingProbeT,
222    ) -> Option<Self> {
223        if ptr.is_null() {
224            return None;
225        }
226        let ptr = unsafe { scamper_ping::scamper_ping_probe_use(ptr) };
227        Some(ScamperPingProbe { inner: ptr, ping })
228    }
229
230    pub fn id(&self) -> u16 {
231        unsafe { scamper_ping::scamper_ping_probe_id_get(self.inner) }
232    }
233
234    pub fn ipid(&self) -> u16 {
235        unsafe { scamper_ping::scamper_ping_probe_ipid_get(self.inner) }
236    }
237
238    pub fn tx(&self) -> Option<SystemTime> {
239        let tv = unsafe { scamper_ping::scamper_ping_probe_tx_get(self.inner) };
240        timeval_to_systemtime(tv)
241    }
242
243    pub fn flags(&self) -> u32 {
244        unsafe { scamper_ping::scamper_ping_probe_flags_get(self.inner) }
245    }
246
247    /// Return the reply at index `i` for this probe.
248    pub fn reply(&self, i: u16) -> Option<ScamperPingReply> {
249        let ptr =
250            unsafe { scamper_ping::scamper_ping_probe_reply_get(self.inner, i) };
251        unsafe { ScamperPingReply::from_ptr(ptr) }
252    }
253
254    /// Check if the given reply is from the target.
255    pub fn reply_is_from_target(&self, reply: &ScamperPingReply) -> bool {
256        unsafe {
257            scamper_ping::scamper_ping_reply_is_from_target(self.ping, reply.inner) != 0
258        }
259    }
260}
261
262impl Drop for ScamperPingProbe {
263    fn drop(&mut self) {
264        unsafe { scamper_ping::scamper_ping_probe_free(self.inner) };
265    }
266}
267
268unsafe impl Send for ScamperPingProbe {}
269unsafe impl Sync for ScamperPingProbe {}
270
271/// A scamper ping measurement result.
272pub struct ScamperPing {
273    pub(crate) inner: *mut ScamperPingT,
274}
275
276impl ScamperPing {
277    pub(crate) unsafe fn from_ptr(ptr: *mut ScamperPingT) -> Option<Self> {
278        if ptr.is_null() {
279            return None;
280        }
281        Some(ScamperPing { inner: ptr })
282    }
283
284    pub fn dst(&self) -> Option<ScamperAddr> {
285        let ptr = unsafe { scamper_ping::scamper_ping_dst_get(self.inner) };
286        unsafe { ScamperAddr::from_ptr(ptr) }
287    }
288
289    pub fn src(&self) -> Option<ScamperAddr> {
290        let ptr = unsafe { scamper_ping::scamper_ping_src_get(self.inner) };
291        unsafe { ScamperAddr::from_ptr(ptr) }
292    }
293
294    pub fn rtr(&self) -> Option<ScamperAddr> {
295        let ptr = unsafe { scamper_ping::scamper_ping_rtr_get(self.inner) };
296        unsafe { ScamperAddr::from_ptr(ptr) }
297    }
298
299    pub fn userid(&self) -> u32 {
300        unsafe { scamper_ping::scamper_ping_userid_get(self.inner) }
301    }
302
303    pub fn start(&self) -> Option<SystemTime> {
304        let tv = unsafe { scamper_ping::scamper_ping_start_get(self.inner) };
305        timeval_to_systemtime(tv)
306    }
307
308    pub fn stop_reason(&self) -> PingStopReason {
309        let v = unsafe { scamper_ping::scamper_ping_stop_reason_get(self.inner) };
310        PingStopReason::from(v)
311    }
312
313    pub fn stop_data(&self) -> u8 {
314        unsafe { scamper_ping::scamper_ping_stop_data_get(self.inner) }
315    }
316
317    pub fn errmsg(&self) -> Option<&str> {
318        let ptr = unsafe { scamper_ping::scamper_ping_errmsg_get(self.inner) };
319        if ptr.is_null() {
320            None
321        } else {
322            Some(unsafe { CStr::from_ptr(ptr) }.to_str().unwrap_or(""))
323        }
324    }
325
326    pub fn attempts(&self) -> u16 {
327        unsafe { scamper_ping::scamper_ping_attempts_get(self.inner) }
328    }
329
330    pub fn pktsize(&self) -> u16 {
331        unsafe { scamper_ping::scamper_ping_pktsize_get(self.inner) }
332    }
333
334    pub fn method(&self) -> u8 {
335        unsafe { scamper_ping::scamper_ping_method_get(self.inner) }
336    }
337
338    pub fn ttl(&self) -> u8 {
339        unsafe { scamper_ping::scamper_ping_ttl_get(self.inner) }
340    }
341
342    pub fn tos(&self) -> u8 {
343        unsafe { scamper_ping::scamper_ping_tos_get(self.inner) }
344    }
345
346    pub fn sport(&self) -> u16 {
347        unsafe { scamper_ping::scamper_ping_sport_get(self.inner) }
348    }
349
350    pub fn dport(&self) -> u16 {
351        unsafe { scamper_ping::scamper_ping_dport_get(self.inner) }
352    }
353
354    pub fn icmpsum(&self) -> u16 {
355        unsafe { scamper_ping::scamper_ping_icmpsum_get(self.inner) }
356    }
357
358    pub fn tcpseq(&self) -> u32 {
359        unsafe { scamper_ping::scamper_ping_tcpseq_get(self.inner) }
360    }
361
362    pub fn tcpack(&self) -> u32 {
363        unsafe { scamper_ping::scamper_ping_tcpack_get(self.inner) }
364    }
365
366    pub fn flags(&self) -> u32 {
367        unsafe { scamper_ping::scamper_ping_flags_get(self.inner) }
368    }
369
370    pub fn stop_count(&self) -> u16 {
371        unsafe { scamper_ping::scamper_ping_stop_count_get(self.inner) }
372    }
373
374    pub fn pmtu(&self) -> u16 {
375        unsafe { scamper_ping::scamper_ping_pmtu_get(self.inner) }
376    }
377
378    pub fn sent(&self) -> u16 {
379        unsafe { scamper_ping::scamper_ping_sent_get(self.inner) }
380    }
381
382    pub fn datalen(&self) -> u16 {
383        unsafe { scamper_ping::scamper_ping_datalen_get(self.inner) }
384    }
385
386    pub fn data(&self) -> &[u8] {
387        let len = unsafe { scamper_ping::scamper_ping_datalen_get(self.inner) };
388        let ptr = unsafe { scamper_ping::scamper_ping_data_get(self.inner) };
389        if ptr.is_null() || len == 0 {
390            return &[];
391        }
392        unsafe { std::slice::from_raw_parts(ptr, len as usize) }
393    }
394
395    pub fn wait_probe(&self) -> Option<Duration> {
396        let tv = unsafe { scamper_ping::scamper_ping_wait_probe_get(self.inner) };
397        timeval_to_duration(tv)
398    }
399
400    pub fn wait_timeout(&self) -> Option<Duration> {
401        let tv = unsafe { scamper_ping::scamper_ping_wait_timeout_get(self.inner) };
402        timeval_to_duration(tv)
403    }
404
405    pub fn is_method_icmp(&self) -> bool {
406        unsafe { scamper_ping::scamper_ping_method_is_icmp(self.inner) != 0 }
407    }
408
409    pub fn is_method_tcp(&self) -> bool {
410        unsafe { scamper_ping::scamper_ping_method_is_tcp(self.inner) != 0 }
411    }
412
413    pub fn is_method_udp(&self) -> bool {
414        unsafe { scamper_ping::scamper_ping_method_is_udp(self.inner) != 0 }
415    }
416
417    pub fn is_method_vary_sport(&self) -> bool {
418        unsafe { scamper_ping::scamper_ping_method_is_vary_sport(self.inner) != 0 }
419    }
420
421    pub fn is_method_vary_dport(&self) -> bool {
422        unsafe { scamper_ping::scamper_ping_method_is_vary_dport(self.inner) != 0 }
423    }
424
425    pub fn list(&self) -> Option<ScamperList> {
426        let ptr = unsafe { scamper_ping::scamper_ping_list_get(self.inner) };
427        unsafe { ScamperList::from_ptr(ptr) }
428    }
429
430    pub fn cycle(&self) -> Option<ScamperCycle> {
431        let ptr = unsafe { scamper_ping::scamper_ping_cycle_get(self.inner) };
432        unsafe { ScamperCycle::from_ptr(ptr) }
433    }
434
435    /// Return probe at index `i`.
436    pub fn probe(&self, i: u16) -> Option<ScamperPingProbe> {
437        let ptr = unsafe { scamper_ping::scamper_ping_probe_get(self.inner, i) };
438        unsafe { ScamperPingProbe::from_ptr(self.inner, ptr) }
439    }
440
441    /// Compute and return ping statistics.
442    pub fn stats(&self) -> Option<ScamperPingStats> {
443        let ptr = unsafe { scamper_ping::scamper_ping_stats_alloc(self.inner) };
444        if ptr.is_null() {
445            None
446        } else {
447            Some(ScamperPingStats { inner: ptr })
448        }
449    }
450
451    /// Convert to JSON string.
452    pub fn to_json(&self) -> Option<String> {
453        let mut len = 0usize;
454        let ptr = unsafe { scamper_ping::scamper_ping_tojson(self.inner, &mut len) };
455        if ptr.is_null() {
456            return None;
457        }
458        let s = unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned();
459        unsafe { libc::free(ptr as *mut libc::c_void) };
460        Some(s)
461    }
462}
463
464impl Drop for ScamperPing {
465    fn drop(&mut self) {
466        unsafe { scamper_ping::scamper_ping_free(self.inner) };
467    }
468}
469
470unsafe impl Send for ScamperPing {}
471unsafe impl Sync for ScamperPing {}