From 13f3458e914a4c89d03d554605494333274c57ec Mon Sep 17 00:00:00 2001 From: Antoine PLASKOWSKI Date: Tue, 4 Nov 2025 15:09:22 +0100 Subject: [PATCH 1/6] Add a binding for pcap_breakloop --- src/capture/activated/mod.rs | 68 +++++++++++++++++++++++++++++++----- src/capture/mod.rs | 30 +++++++++++----- src/lib.rs | 4 ++- 3 files changed, 85 insertions(+), 17 deletions(-) diff --git a/src/capture/activated/mod.rs b/src/capture/activated/mod.rs index b32af64e..08d95f94 100644 --- a/src/capture/activated/mod.rs +++ b/src/capture/activated/mod.rs @@ -12,13 +12,14 @@ use std::{ path::Path, ptr::{self, NonNull}, slice, + sync::{Arc, Weak}, }; #[cfg(not(windows))] use std::os::unix::io::RawFd; use crate::{ - capture::{Activated, Capture}, + capture::{Activated, Capture, Handle}, codec::PacketCodec, linktype::Linktype, packet::{Packet, PacketHeader}, @@ -216,17 +217,17 @@ impl Capture { None => -1, }; - let mut handler = Handler { + let mut handler = HandlerFn { func: AssertUnwindSafe(handler), panic_payload: None, - handle: self.handle, + handle: self.handle.clone(), }; let return_code = unsafe { raw::pcap_loop( self.handle.as_ptr(), cnt, - Handler::::callback, - &mut handler as *mut Handler> as *mut u8, + HandlerFn::::callback, + &mut handler as *mut HandlerFn> as *mut u8, ) }; if let Some(e) = handler.panic_payload { @@ -235,6 +236,38 @@ impl Capture { self.check_err(return_code == 0) } + /// Returns a thread-safe `BreakLoop` handle for calling pcap_breakloop() on an active capture. + /// + /// # Example + /// + /// ```no_run + /// // Using an active capture + /// use pcap::Device; + /// + /// let mut cap = Device::lookup().unwrap().unwrap().open().unwrap(); + /// + /// let break_handle = cap.breakloop_handle(); + /// + /// let capture_thread = std::thread::spawn(move || { + /// while let Ok(packet) = cap.next_packet() { + /// println!("received packet! {:?}", packet); + /// } + /// }); + /// + /// // Send break_handle to a separate thread (e.g. user input, signal handler, etc.) + /// std::thread::spawn(move || { + /// std::thread::sleep(std::time::Duration::from_secs(1)); + /// break_handle.breakloop(); + /// }); + /// + /// capture_thread.join().unwrap(); + /// ``` + pub fn breakloop_handle(&mut self) -> BreakLoop { + BreakLoop { + handle: Arc::::downgrade(&self.handle), + } + } + /// Compiles the string into a filter program using `pcap_compile`. pub fn compile(&self, program: &str, optimize: bool) -> Result { let program = CString::new(program)?; @@ -281,13 +314,13 @@ impl Capture { // Rust FnMut, which may be a closure with a captured environment. The *only* purpose of this // generic parameter is to ensure that in Capture::pcap_loop that we pass the right function // pointer and the right data pointer to pcap_loop. -struct Handler { +struct HandlerFn { func: F, panic_payload: Option>, - handle: NonNull, + handle: Arc, } -impl Handler +impl HandlerFn where F: FnMut(Packet), { @@ -322,6 +355,25 @@ impl From> for Capture { } } +/// BreakLoop can safely be sent to other threads such as signal handlers to abort +/// blocking capture loops such as `Capture::next_packet` and `Capture::for_each`. +/// +/// See for per-platform caveats about +/// how breakloop can wake up blocked threads. +pub struct BreakLoop { + handle: Weak, +} + +impl BreakLoop { + /// Calls `pcap_breakloop` to make the blocking loop of a pcap capture return. + /// The call is a no-op if the handle is invalid. + pub fn breakloop(&self) { + if let Some(handle) = self.handle.upgrade() { + unsafe { raw::pcap_breakloop(handle.as_ptr()) }; + } + } +} + /// Abstraction for writing pcap savefiles, which can be read afterwards via `Capture::from_file()`. pub struct Savefile { handle: NonNull, diff --git a/src/capture/mod.rs b/src/capture/mod.rs index 83f50566..01aa18bc 100644 --- a/src/capture/mod.rs +++ b/src/capture/mod.rs @@ -8,6 +8,7 @@ use std::{ ffi::CString, marker::PhantomData, ptr::{self, NonNull}, + sync::Arc, }; #[cfg(windows)] @@ -90,10 +91,29 @@ impl State for Dead {} /// ``` pub struct Capture { nonblock: bool, - handle: NonNull, + handle: Arc, _marker: PhantomData, } +struct Handle { + handle: NonNull, +} + +impl Handle { + fn as_ptr(&self) -> *mut raw::pcap_t { + self.handle.as_ptr() + } +} + +unsafe impl Send for Handle {} +unsafe impl Sync for Handle {} + +impl Drop for Handle { + fn drop(&mut self) { + unsafe { raw::pcap_close(self.handle.as_ptr()) } + } +} + // A Capture is safe to Send as it encapsulates the entire lifetime of `raw::pcap_t *`, but it is // not safe to Sync as libpcap does not promise thread-safe access to the same `raw::pcap_t *` from // multiple threads. @@ -103,7 +123,7 @@ impl From> for Capture { fn from(handle: NonNull) -> Self { Capture { nonblock: false, - handle, + handle: Arc::new(Handle { handle }), _marker: PhantomData, } } @@ -178,12 +198,6 @@ impl Capture { } } -impl Drop for Capture { - fn drop(&mut self) { - unsafe { raw::pcap_close(self.handle.as_ptr()) } - } -} - #[repr(u32)] #[derive(Debug, PartialEq, Eq, Clone, Copy)] /// Timestamp resolution types diff --git a/src/lib.rs b/src/lib.rs index be4cf1ec..f562cd2f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,7 +74,9 @@ mod packet; #[cfg(not(windows))] pub use capture::activated::open_raw_fd; pub use capture::{ - activated::{iterator::PacketIter, BpfInstruction, BpfProgram, Direction, Savefile, Stat}, + activated::{ + iterator::PacketIter, BpfInstruction, BpfProgram, BreakLoop, Direction, Savefile, Stat, + }, inactive::TimestampType, {Activated, Active, Capture, Dead, Inactive, Offline, Precision, State}, }; From 5559e39d14f7d1d445f7d8f24f7f900d8966f661 Mon Sep 17 00:00:00 2001 From: cai-william Date: Mon, 3 Nov 2025 13:36:27 +1000 Subject: [PATCH 2/6] Add tests and example for BreakLoop handle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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(). --- examples/breakloop.rs | 36 ++++++++++++++++++++++++++++++++++++ src/capture/activated/mod.rs | 25 +++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 examples/breakloop.rs diff --git a/examples/breakloop.rs b/examples/breakloop.rs new file mode 100644 index 00000000..f37ecaf0 --- /dev/null +++ b/examples/breakloop.rs @@ -0,0 +1,36 @@ +use std::{thread, time::Duration}; + +fn main() { + // Get the default device + let device = pcap::Device::lookup() + .expect("device lookup failed") + .expect("no device available"); + println!("Using device {}", device.name); + + // Setup capture + let mut cap = pcap::Capture::from_device(device) + .unwrap() + .immediate_mode(true) + .open() + .unwrap(); + println!("Using device"); + + let break_handle = cap.breakloop_handle(); + + // Start capture in a separate thread + let capture_thread = thread::spawn(move || { + while cap.next_packet().is_ok() { + println!("got packet!"); + } + println!("capture loop exited"); + }); + + // Send break_handle to a separate thread (e.g. user input, signal handler, etc.) + thread::spawn(move || { + thread::sleep(Duration::from_secs(1)); + println!("break loop called!"); + break_handle.breakloop(); + }); + + capture_thread.join().unwrap(); +} diff --git a/src/capture/activated/mod.rs b/src/capture/activated/mod.rs index 08d95f94..3a1aaaf7 100644 --- a/src/capture/activated/mod.rs +++ b/src/capture/activated/mod.rs @@ -665,6 +665,31 @@ mod tests { } } + #[test] + fn test_breakloop_capture_dropped() { + let _m = RAWMTX.lock(); + + let mut value: isize = 1234; + let pcap = as_pcap_t(&mut value); + + let test_capture = test_capture::(pcap); + let mut capture: Capture = test_capture.capture.into(); + + let ctx = raw::pcap_breakloop_context(); + ctx.expect() + .withf_st(move |h| *h == pcap) + .return_const(()) + .times(1); + + let break_handle = capture.breakloop_handle(); + + break_handle.breakloop(); + + drop(capture); + + break_handle.breakloop(); // this call does not trigger mock after drop + } + #[test] fn test_savefile() { let _m = RAWMTX.lock(); From 31dfb04af6fa12e14e2760a624e96a48fff843b4 Mon Sep 17 00:00:00 2001 From: cai-william Date: Mon, 3 Nov 2025 13:37:08 +1000 Subject: [PATCH 3/6] Update change log for pcap_breakloop --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44232a63..5875ece2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changed - MSRV is now `1.64`. +- Binding for `pcap_breakloop` added. It can be accessed via the `breakloop_handle` call on Activated captures. ## [2.3.0] - 2025-07-19 From 20da2c1d69ee32e90560dde59e4dda4ea230784a Mon Sep 17 00:00:00 2001 From: cai-william Date: Wed, 5 Nov 2025 19:43:23 +1000 Subject: [PATCH 4/6] Add safety docs for BreakLoop::breakloop --- src/capture/activated/mod.rs | 12 ++++++++++++ src/capture/mod.rs | 7 +++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/capture/activated/mod.rs b/src/capture/activated/mod.rs index 3a1aaaf7..59357ead 100644 --- a/src/capture/activated/mod.rs +++ b/src/capture/activated/mod.rs @@ -364,9 +364,21 @@ pub struct BreakLoop { handle: Weak, } +unsafe impl Send for BreakLoop {} +unsafe impl Sync for BreakLoop {} + impl BreakLoop { /// Calls `pcap_breakloop` to make the blocking loop of a pcap capture return. /// The call is a no-op if the handle is invalid. + /// + /// # Safety + /// + /// Can be called from any thread, but **must not** be used inside a + /// signal handler unless the owning `Capture` is guaranteed to still + /// be alive. + /// + /// The signal handler should defer the execution of `BreakLoop::breakloop()` + /// to a thread instead for safety. pub fn breakloop(&self) { if let Some(handle) = self.handle.upgrade() { unsafe { raw::pcap_breakloop(handle.as_ptr()) }; diff --git a/src/capture/mod.rs b/src/capture/mod.rs index 01aa18bc..ea2e80c7 100644 --- a/src/capture/mod.rs +++ b/src/capture/mod.rs @@ -105,8 +105,10 @@ impl Handle { } } +// `PcapHandle` is safe to Send as it encapsulates the entire lifetime of `raw::pcap_t *`, but it is +// not safe to Sync as libpcap does not promise thread-safe access to the same `raw::pcap_t *` from +// multiple threads. unsafe impl Send for Handle {} -unsafe impl Sync for Handle {} impl Drop for Handle { fn drop(&mut self) { @@ -114,9 +116,6 @@ impl Drop for Handle { } } -// A Capture is safe to Send as it encapsulates the entire lifetime of `raw::pcap_t *`, but it is -// not safe to Sync as libpcap does not promise thread-safe access to the same `raw::pcap_t *` from -// multiple threads. unsafe impl Send for Capture {} impl From> for Capture { From f8968361235ea658fbad3776b469b0128733ea4c Mon Sep 17 00:00:00 2001 From: cai-william Date: Wed, 5 Nov 2025 19:44:45 +1000 Subject: [PATCH 5/6] Rename `Handle` to `PcapHandle` --- src/capture/activated/mod.rs | 8 ++++---- src/capture/mod.rs | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/capture/activated/mod.rs b/src/capture/activated/mod.rs index 59357ead..46405689 100644 --- a/src/capture/activated/mod.rs +++ b/src/capture/activated/mod.rs @@ -19,7 +19,7 @@ use std::{ use std::os::unix::io::RawFd; use crate::{ - capture::{Activated, Capture, Handle}, + capture::{Activated, Capture, PcapHandle}, codec::PacketCodec, linktype::Linktype, packet::{Packet, PacketHeader}, @@ -264,7 +264,7 @@ impl Capture { /// ``` pub fn breakloop_handle(&mut self) -> BreakLoop { BreakLoop { - handle: Arc::::downgrade(&self.handle), + handle: Arc::::downgrade(&self.handle), } } @@ -317,7 +317,7 @@ impl Capture { struct HandlerFn { func: F, panic_payload: Option>, - handle: Arc, + handle: Arc, } impl HandlerFn @@ -361,7 +361,7 @@ impl From> for Capture { /// See for per-platform caveats about /// how breakloop can wake up blocked threads. pub struct BreakLoop { - handle: Weak, + handle: Weak, } unsafe impl Send for BreakLoop {} diff --git a/src/capture/mod.rs b/src/capture/mod.rs index ea2e80c7..08cea2c1 100644 --- a/src/capture/mod.rs +++ b/src/capture/mod.rs @@ -91,15 +91,15 @@ impl State for Dead {} /// ``` pub struct Capture { nonblock: bool, - handle: Arc, + handle: Arc, _marker: PhantomData, } -struct Handle { +struct PcapHandle { handle: NonNull, } -impl Handle { +impl PcapHandle { fn as_ptr(&self) -> *mut raw::pcap_t { self.handle.as_ptr() } @@ -108,9 +108,9 @@ impl Handle { // `PcapHandle` is safe to Send as it encapsulates the entire lifetime of `raw::pcap_t *`, but it is // not safe to Sync as libpcap does not promise thread-safe access to the same `raw::pcap_t *` from // multiple threads. -unsafe impl Send for Handle {} +unsafe impl Send for PcapHandle {} -impl Drop for Handle { +impl Drop for PcapHandle { fn drop(&mut self) { unsafe { raw::pcap_close(self.handle.as_ptr()) } } @@ -122,7 +122,7 @@ impl From> for Capture { fn from(handle: NonNull) -> Self { Capture { nonblock: false, - handle: Arc::new(Handle { handle }), + handle: Arc::new(PcapHandle { handle }), _marker: PhantomData, } } From 2bf4796db4c67b70b01de3590638859d184ae2b8 Mon Sep 17 00:00:00 2001 From: cai-william Date: Thu, 6 Nov 2025 09:57:01 +1000 Subject: [PATCH 6/6] Add clippy allow `arc_with_non_send_sync` for Capture --- src/capture/mod.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/capture/mod.rs b/src/capture/mod.rs index 08cea2c1..330dd3d7 100644 --- a/src/capture/mod.rs +++ b/src/capture/mod.rs @@ -105,9 +105,9 @@ impl PcapHandle { } } -// `PcapHandle` is safe to Send as it encapsulates the entire lifetime of `raw::pcap_t *`, but it is -// not safe to Sync as libpcap does not promise thread-safe access to the same `raw::pcap_t *` from -// multiple threads. +// `PcapHandle` is safe to Send as it encapsulates the entire lifetime of `raw::pcap_t *` +// `PcapHandle` is only Sync under special circumstances when used in thread-safe functions such as +// the `pcap_breakloop` function. The Sync correctness is left to the wrapping structure to provide. unsafe impl Send for PcapHandle {} impl Drop for PcapHandle { @@ -118,6 +118,9 @@ impl Drop for PcapHandle { unsafe impl Send for Capture {} +// `Capture` is not safe to implement Sync as the libpcap functions it uses are not promised to have +// thread-safe access to the same `raw::pcap_t *` from multiple threads. +#[allow(clippy::arc_with_non_send_sync)] impl From> for Capture { fn from(handle: NonNull) -> Self { Capture {