Skip to content

Commit f138c4b

Browse files
Add a binding for pcap_breakloop (#390)
* Add a binding for pcap_breakloop * Add tests and example for BreakLoop handle * Added unit tests to verify handle validity and behavior after capture drop. * Introduced examples/break_loop.rs demonstrating thread‑safe capture abort using breakloop_handle(). * Update change log for pcap_breakloop * Add safety docs for BreakLoop::breakloop * Rename `Handle` to `PcapHandle` * Add clippy allow `arc_with_non_send_sync` for Capture --------- Co-authored-by: Antoine PLASKOWSKI <plaskowski.stanislas@gmail.com>
1 parent 538cd95 commit f138c4b

File tree

5 files changed

+164
-20
lines changed

5 files changed

+164
-20
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Changed
66

77
- MSRV is now `1.64`.
8+
- Binding for `pcap_breakloop` added. It can be accessed via the `breakloop_handle` call on Activated captures.
89

910
## [2.3.0] - 2025-07-19
1011

examples/breakloop.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use std::{thread, time::Duration};
2+
3+
fn main() {
4+
// Get the default device
5+
let device = pcap::Device::lookup()
6+
.expect("device lookup failed")
7+
.expect("no device available");
8+
println!("Using device {}", device.name);
9+
10+
// Setup capture
11+
let mut cap = pcap::Capture::from_device(device)
12+
.unwrap()
13+
.immediate_mode(true)
14+
.open()
15+
.unwrap();
16+
println!("Using device");
17+
18+
let break_handle = cap.breakloop_handle();
19+
20+
// Start capture in a separate thread
21+
let capture_thread = thread::spawn(move || {
22+
while cap.next_packet().is_ok() {
23+
println!("got packet!");
24+
}
25+
println!("capture loop exited");
26+
});
27+
28+
// Send break_handle to a separate thread (e.g. user input, signal handler, etc.)
29+
thread::spawn(move || {
30+
thread::sleep(Duration::from_secs(1));
31+
println!("break loop called!");
32+
break_handle.breakloop();
33+
});
34+
35+
capture_thread.join().unwrap();
36+
}

src/capture/activated/mod.rs

Lines changed: 97 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ use std::{
1212
path::Path,
1313
ptr::{self, NonNull},
1414
slice,
15+
sync::{Arc, Weak},
1516
};
1617

1718
#[cfg(not(windows))]
1819
use std::os::unix::io::RawFd;
1920

2021
use crate::{
21-
capture::{Activated, Capture},
22+
capture::{Activated, Capture, PcapHandle},
2223
codec::PacketCodec,
2324
linktype::Linktype,
2425
packet::{Packet, PacketHeader},
@@ -216,17 +217,17 @@ impl<T: Activated + ?Sized> Capture<T> {
216217
None => -1,
217218
};
218219

219-
let mut handler = Handler {
220+
let mut handler = HandlerFn {
220221
func: AssertUnwindSafe(handler),
221222
panic_payload: None,
222-
handle: self.handle,
223+
handle: self.handle.clone(),
223224
};
224225
let return_code = unsafe {
225226
raw::pcap_loop(
226227
self.handle.as_ptr(),
227228
cnt,
228-
Handler::<F>::callback,
229-
&mut handler as *mut Handler<AssertUnwindSafe<F>> as *mut u8,
229+
HandlerFn::<F>::callback,
230+
&mut handler as *mut HandlerFn<AssertUnwindSafe<F>> as *mut u8,
230231
)
231232
};
232233
if let Some(e) = handler.panic_payload {
@@ -235,6 +236,38 @@ impl<T: Activated + ?Sized> Capture<T> {
235236
self.check_err(return_code == 0)
236237
}
237238

239+
/// Returns a thread-safe `BreakLoop` handle for calling pcap_breakloop() on an active capture.
240+
///
241+
/// # Example
242+
///
243+
/// ```no_run
244+
/// // Using an active capture
245+
/// use pcap::Device;
246+
///
247+
/// let mut cap = Device::lookup().unwrap().unwrap().open().unwrap();
248+
///
249+
/// let break_handle = cap.breakloop_handle();
250+
///
251+
/// let capture_thread = std::thread::spawn(move || {
252+
/// while let Ok(packet) = cap.next_packet() {
253+
/// println!("received packet! {:?}", packet);
254+
/// }
255+
/// });
256+
///
257+
/// // Send break_handle to a separate thread (e.g. user input, signal handler, etc.)
258+
/// std::thread::spawn(move || {
259+
/// std::thread::sleep(std::time::Duration::from_secs(1));
260+
/// break_handle.breakloop();
261+
/// });
262+
///
263+
/// capture_thread.join().unwrap();
264+
/// ```
265+
pub fn breakloop_handle(&mut self) -> BreakLoop {
266+
BreakLoop {
267+
handle: Arc::<PcapHandle>::downgrade(&self.handle),
268+
}
269+
}
270+
238271
/// Compiles the string into a filter program using `pcap_compile`.
239272
pub fn compile(&self, program: &str, optimize: bool) -> Result<BpfProgram, Error> {
240273
let program = CString::new(program)?;
@@ -281,13 +314,13 @@ impl<T: Activated + ?Sized> Capture<T> {
281314
// Rust FnMut, which may be a closure with a captured environment. The *only* purpose of this
282315
// generic parameter is to ensure that in Capture::pcap_loop that we pass the right function
283316
// pointer and the right data pointer to pcap_loop.
284-
struct Handler<F> {
317+
struct HandlerFn<F> {
285318
func: F,
286319
panic_payload: Option<Box<dyn Any + Send>>,
287-
handle: NonNull<raw::pcap_t>,
320+
handle: Arc<PcapHandle>,
288321
}
289322

290-
impl<F> Handler<F>
323+
impl<F> HandlerFn<F>
291324
where
292325
F: FnMut(Packet),
293326
{
@@ -322,6 +355,37 @@ impl<T: Activated> From<Capture<T>> for Capture<dyn Activated> {
322355
}
323356
}
324357

358+
/// BreakLoop can safely be sent to other threads such as signal handlers to abort
359+
/// blocking capture loops such as `Capture::next_packet` and `Capture::for_each`.
360+
///
361+
/// See <https://www.tcpdump.org/manpages/pcap_breakloop.3pcap.html> for per-platform caveats about
362+
/// how breakloop can wake up blocked threads.
363+
pub struct BreakLoop {
364+
handle: Weak<PcapHandle>,
365+
}
366+
367+
unsafe impl Send for BreakLoop {}
368+
unsafe impl Sync for BreakLoop {}
369+
370+
impl BreakLoop {
371+
/// Calls `pcap_breakloop` to make the blocking loop of a pcap capture return.
372+
/// The call is a no-op if the handle is invalid.
373+
///
374+
/// # Safety
375+
///
376+
/// Can be called from any thread, but **must not** be used inside a
377+
/// signal handler unless the owning `Capture` is guaranteed to still
378+
/// be alive.
379+
///
380+
/// The signal handler should defer the execution of `BreakLoop::breakloop()`
381+
/// to a thread instead for safety.
382+
pub fn breakloop(&self) {
383+
if let Some(handle) = self.handle.upgrade() {
384+
unsafe { raw::pcap_breakloop(handle.as_ptr()) };
385+
}
386+
}
387+
}
388+
325389
/// Abstraction for writing pcap savefiles, which can be read afterwards via `Capture::from_file()`.
326390
pub struct Savefile {
327391
handle: NonNull<raw::pcap_dumper_t>,
@@ -613,6 +677,31 @@ mod tests {
613677
}
614678
}
615679

680+
#[test]
681+
fn test_breakloop_capture_dropped() {
682+
let _m = RAWMTX.lock();
683+
684+
let mut value: isize = 1234;
685+
let pcap = as_pcap_t(&mut value);
686+
687+
let test_capture = test_capture::<Active>(pcap);
688+
let mut capture: Capture<dyn Activated> = test_capture.capture.into();
689+
690+
let ctx = raw::pcap_breakloop_context();
691+
ctx.expect()
692+
.withf_st(move |h| *h == pcap)
693+
.return_const(())
694+
.times(1);
695+
696+
let break_handle = capture.breakloop_handle();
697+
698+
break_handle.breakloop();
699+
700+
drop(capture);
701+
702+
break_handle.breakloop(); // this call does not trigger mock after drop
703+
}
704+
616705
#[test]
617706
fn test_savefile() {
618707
let _m = RAWMTX.lock();

src/capture/mod.rs

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::{
88
ffi::CString,
99
marker::PhantomData,
1010
ptr::{self, NonNull},
11+
sync::Arc,
1112
};
1213

1314
#[cfg(windows)]
@@ -90,20 +91,41 @@ impl State for Dead {}
9091
/// ```
9192
pub struct Capture<T: State + ?Sized> {
9293
nonblock: bool,
93-
handle: NonNull<raw::pcap_t>,
94+
handle: Arc<PcapHandle>,
9495
_marker: PhantomData<T>,
9596
}
9697

97-
// A Capture is safe to Send as it encapsulates the entire lifetime of `raw::pcap_t *`, but it is
98-
// not safe to Sync as libpcap does not promise thread-safe access to the same `raw::pcap_t *` from
99-
// multiple threads.
98+
struct PcapHandle {
99+
handle: NonNull<raw::pcap_t>,
100+
}
101+
102+
impl PcapHandle {
103+
fn as_ptr(&self) -> *mut raw::pcap_t {
104+
self.handle.as_ptr()
105+
}
106+
}
107+
108+
// `PcapHandle` is safe to Send as it encapsulates the entire lifetime of `raw::pcap_t *`
109+
// `PcapHandle` is only Sync under special circumstances when used in thread-safe functions such as
110+
// the `pcap_breakloop` function. The Sync correctness is left to the wrapping structure to provide.
111+
unsafe impl Send for PcapHandle {}
112+
113+
impl Drop for PcapHandle {
114+
fn drop(&mut self) {
115+
unsafe { raw::pcap_close(self.handle.as_ptr()) }
116+
}
117+
}
118+
100119
unsafe impl<T: State + ?Sized> Send for Capture<T> {}
101120

121+
// `Capture` is not safe to implement Sync as the libpcap functions it uses are not promised to have
122+
// thread-safe access to the same `raw::pcap_t *` from multiple threads.
123+
#[allow(clippy::arc_with_non_send_sync)]
102124
impl<T: State + ?Sized> From<NonNull<raw::pcap_t>> for Capture<T> {
103125
fn from(handle: NonNull<raw::pcap_t>) -> Self {
104126
Capture {
105127
nonblock: false,
106-
handle,
128+
handle: Arc::new(PcapHandle { handle }),
107129
_marker: PhantomData,
108130
}
109131
}
@@ -178,12 +200,6 @@ impl<T: State + ?Sized> Capture<T> {
178200
}
179201
}
180202

181-
impl<T: State + ?Sized> Drop for Capture<T> {
182-
fn drop(&mut self) {
183-
unsafe { raw::pcap_close(self.handle.as_ptr()) }
184-
}
185-
}
186-
187203
#[repr(u32)]
188204
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
189205
/// Timestamp resolution types

src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ mod packet;
7474
#[cfg(not(windows))]
7575
pub use capture::activated::open_raw_fd;
7676
pub use capture::{
77-
activated::{iterator::PacketIter, BpfInstruction, BpfProgram, Direction, Savefile, Stat},
77+
activated::{
78+
iterator::PacketIter, BpfInstruction, BpfProgram, BreakLoop, Direction, Savefile, Stat,
79+
},
7880
inactive::TimestampType,
7981
{Activated, Active, Capture, Dead, Inactive, Offline, Precision, State},
8082
};

0 commit comments

Comments
 (0)