Skip to main content

proc_connector/
socket.rs

1//! Core `ProcConnector` type — safe wrapper around a Linux Netlink Proc Connector socket.
2//!
3//! # Lifecycle
4//!
5//! 1. `ProcConnector::new()` — creates the netlink socket, binds to `CN_IDX_PROC`, subscribes.
6//! 2. `recv()` / `recv_timeout()` — receive and parse process events.
7//! 3. `unsubscribe()` / `subscribe()` — toggle subscription (useful after reconnect).
8//! 4. Drop — automatically unsubscribes and closes the socket.
9
10use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};
11use std::time::Duration;
12
13use crate::consts::*;
14use crate::error::{Error, Result};
15use crate::proc_event::ProcEvent;
16
17/// A safe handle to a Linux Netlink Proc Connector socket.
18///
19/// The socket is created, bound, and subscribed in `new()`. On drop, the
20/// subscription is cancelled and the file descriptor is closed automatically.
21///
22/// # Examples
23///
24/// ```no_run
25/// use proc_connector::ProcConnector;
26///
27/// let conn = ProcConnector::new().unwrap();
28/// let mut buf = vec![0u8; 4096];
29///
30/// loop {
31///     match conn.recv(&mut buf) {
32///         Ok(event) => println!("got event: {event:?}"),
33///         Err(e) => eprintln!("error: {e}"),
34///     }
35/// }
36/// ```
37pub struct ProcConnector {
38    fd: OwnedFd,
39}
40
41impl ProcConnector {
42    /// Create a new `ProcConnector`.
43    ///
44    /// This is a convenience constructor that:
45    /// 1. Creates a `PF_NETLINK` / `SOCK_DGRAM` socket of family `NETLINK_CONNECTOR`.
46    /// 2. Binds to the `CN_IDX_PROC` multicast group.
47    /// 3. Sends a `PROC_CN_MCAST_LISTEN` subscription message.
48    ///
49    /// # Errors
50    ///
51    /// Returns `Error::Os` if any system call fails.
52    pub fn new() -> Result<Self> {
53        let fd = create_socket()?;
54        let connector = Self { fd };
55        connector.bind()?;
56        connector.subscribe()?;
57        Ok(connector)
58    }
59
60    /// Bind the socket to the `CN_IDX_PROC` netlink group.
61    fn bind(&self) -> Result<()> {
62        let mut sa: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
63        sa.nl_family = libc::AF_NETLINK as u16;
64        // nl_pad left as zeroed
65        sa.nl_pid = 0; // let kernel pick a port ID
66        sa.nl_groups = CN_IDX_PROC;
67
68        let ret = unsafe {
69            libc::bind(
70                self.fd.as_raw_fd(),
71                &sa as *const libc::sockaddr_nl as *const libc::sockaddr,
72                std::mem::size_of::<libc::sockaddr_nl>() as u32,
73            )
74        };
75
76        if ret < 0 {
77            return Err(Error::Os(std::io::Error::last_os_error()));
78        }
79        Ok(())
80    }
81
82    /// (Re-)subscribe to process events.
83    ///
84    /// Sends a `PROC_CN_MCAST_LISTEN` message via the netlink socket.
85    /// This is safe to call multiple times (e.g. after a reconnection).
86    ///
87    /// # Example
88    ///
89    /// ```no_run
90    /// # use proc_connector::ProcConnector;
91    /// let mut conn = ProcConnector::new().unwrap();
92    /// conn.subscribe().expect("subscribe");
93    /// ```
94    pub fn subscribe(&self) -> Result<()> {
95        self.send_mcast_msg(&PROC_CN_MCAST_LISTEN.to_ne_bytes())?;
96        self.check_subscribe_ack()
97    }
98
99    /// Subscribe to process events, filtering by event type mask.
100    ///
101    /// `mask` is a bitmask of `PROC_EVENT_*` values (use [`EventMask`]
102    /// constants or bitwise OR). On kernels that do not support filtering
103    /// the mask is silently ignored and all events are received.
104    ///
105    /// # Example
106    ///
107    /// ```no_run
108    /// use proc_connector::{ProcConnector, EventMask};
109    ///
110    /// let conn = ProcConnector::new().unwrap();
111    /// conn.subscribe_filtered(EventMask::EXEC | EventMask::EXIT)
112    ///     .expect("subscribe");
113    /// ```
114    pub fn subscribe_filtered(&self, mask: EventMask) -> Result<()> {
115        let mask = mask.0;
116        let mut payload = [0u8; 8];
117        payload[..4].copy_from_slice(&PROC_CN_MCAST_LISTEN.to_ne_bytes());
118        payload[4..].copy_from_slice(&mask.to_ne_bytes());
119        self.send_mcast_msg(&payload)?;
120        self.check_subscribe_ack()
121    }
122
123    /// Unsubscribe from process events.
124    ///
125    /// Sends a `PROC_CN_MCAST_IGNORE` message. Automatically called on drop.
126    ///
127    /// # Example
128    ///
129    /// ```no_run
130    /// # use proc_connector::ProcConnector;
131    /// let conn = ProcConnector::new().unwrap();
132    /// conn.unsubscribe().expect("unsubscribe");
133    /// // Re-subscribe later:
134    /// conn.subscribe().expect("re-subscribe");
135    /// ```
136    pub fn unsubscribe(&self) -> Result<()> {
137        self.send_mcast_msg(&PROC_CN_MCAST_IGNORE.to_ne_bytes())
138    }
139
140    /// Send a netlink message to the kernel connector control channel.
141    fn send_mcast_msg(&self, payload: &[u8]) -> Result<()> {
142        let nlmsg_payload_len = SIZE_CN_MSG + payload.len();
143        let nlmsg_len = nlmsg_length(nlmsg_payload_len);
144
145        let mut buf = vec![0u8; nlmsg_len];
146        let pid = std::process::id();
147
148        let hdr = &mut buf[..SIZE_NLMSGHDR];
149        hdr[0..4].copy_from_slice(&(nlmsg_len as u32).to_ne_bytes());
150        hdr[4..6].copy_from_slice(&NLMSG_MIN_TYPE.to_ne_bytes());
151        hdr[6..8].copy_from_slice(&NLM_F_REQUEST.to_ne_bytes());
152        hdr[8..12].copy_from_slice(&0u32.to_ne_bytes());
153        hdr[12..16].copy_from_slice(&pid.to_ne_bytes());
154
155        let cn_off = nlmsg_hdrlen();
156        let cn = &mut buf[cn_off..cn_off + SIZE_CN_MSG + payload.len()];
157        cn[0..4].copy_from_slice(&CN_IDX_PROC.to_ne_bytes());
158        cn[4..8].copy_from_slice(&CN_VAL_PROC.to_ne_bytes());
159        cn[8..12].copy_from_slice(&0u32.to_ne_bytes());
160        cn[12..16].copy_from_slice(&0u32.to_ne_bytes());
161        cn[16..18].copy_from_slice(&(payload.len() as u16).to_ne_bytes());
162        cn[18..20].copy_from_slice(&0u16.to_ne_bytes());
163        cn[20..20 + payload.len()].copy_from_slice(payload);
164
165        let iov = libc::iovec {
166            iov_base: buf.as_mut_ptr() as *mut libc::c_void,
167            iov_len: nlmsg_len,
168        };
169
170        let msg_hdr = libc::msghdr {
171            msg_name: std::ptr::null_mut(),
172            msg_namelen: 0,
173            msg_iov: &iov as *const libc::iovec as *mut libc::iovec,
174            msg_iovlen: 1,
175            msg_control: std::ptr::null_mut(),
176            msg_controllen: 0,
177            msg_flags: 0,
178        };
179
180        let ret = unsafe { libc::sendmsg(self.fd.as_raw_fd(), &msg_hdr, 0) };
181        if ret < 0 {
182            return Err(Error::Os(std::io::Error::last_os_error()));
183        }
184        Ok(())
185    }
186
187    /// Read and validate the kernel's subscription ACK.
188    fn check_subscribe_ack(&self) -> Result<()> {
189        let mut buf = vec![0u8; 4096];
190        let n = self.recv_raw(&mut buf)?;
191        let msg = match crate::parse::first_event_from_buf(&buf, n) {
192            Ok(Some(msg)) => msg,
193            _ => return Ok(()),
194        };
195        if let ProcEvent::Unknown {
196            what: 0,
197            ref raw_data,
198        } = msg
199            && raw_data.len() >= 4
200        {
201            let err = {
202                let arr: [u8; 4] = raw_data[..4].try_into().unwrap();
203                i32::from_ne_bytes(arr)
204            };
205            if err != 0 {
206                let pos = err.checked_neg().unwrap_or(err);
207                return Err(Error::Os(std::io::Error::from_raw_os_error(pos)));
208            }
209        }
210        Ok(())
211    }
212
213    /// Receive a raw netlink message into `buf`.
214    ///
215    /// On success returns the number of bytes written to `buf`.
216    ///
217    /// This is a thin wrapper around `recv(2)`. The caller is responsible
218    /// for providing a sufficiently large buffer (a page size, 4096 bytes,
219    /// is a safe default).
220    ///
221    /// # Errors
222    ///
223    /// - `Interrupted` if `EINTR` — retry the call.
224    /// - `ConnectionClosed` if recv returns 0.
225    /// - `Os` for other system call errors.
226    ///
227    /// # Example
228    ///
229    /// ```no_run
230    /// use proc_connector::ProcConnector;
231    ///
232    /// let conn = ProcConnector::new().unwrap();
233    /// let mut buf = vec![0u8; 4096];
234    /// let n = conn.recv_raw(&mut buf).unwrap();
235    /// println!("received {n} bytes");
236    /// ```
237    pub fn recv_raw(&self, buf: &mut [u8]) -> Result<usize> {
238        let len = unsafe {
239            libc::recv(
240                self.fd.as_raw_fd(),
241                buf.as_mut_ptr() as *mut libc::c_void,
242                buf.len(),
243                0,
244            )
245        };
246
247        if len < 0 {
248            let err = std::io::Error::last_os_error();
249            return match err.raw_os_error() {
250                Some(libc::EINTR) => Err(Error::Interrupted),
251                Some(libc::EAGAIN) => Err(Error::WouldBlock), // EWOULDBLOCK == EAGAIN on Linux
252                Some(libc::ENOBUFS) => Err(Error::Overrun),
253                _ => Err(Error::Os(err)),
254            };
255        }
256
257        if len == 0 {
258            return Err(Error::ConnectionClosed);
259        }
260
261        Ok(len as usize)
262    }
263
264    /// Receive a raw netlink message with a timeout.
265    ///
266    /// Returns `Ok(None)` if the timeout expires before data is available.
267    /// Otherwise behaves the same as `recv_raw`.
268    ///
269    /// Uses `poll(2)` internally and only calls `recv` when data is ready.
270    ///
271    /// # Example
272    ///
273    /// ```no_run
274    /// # use proc_connector::ProcConnector;
275    /// # use std::time::Duration;
276    /// let conn = ProcConnector::new().unwrap();
277    /// let mut buf = vec![0u8; 4096];
278    ///
279    /// match conn.recv_timeout(&mut buf, Duration::from_secs(5)) {
280    ///     Ok(Some(event)) => println!("{event}"),
281    ///     Ok(None) => eprintln!("timeout, no event in 5s"),
282    ///     Err(e) => eprintln!("error: {e}"),
283    /// }
284    /// ```
285    pub fn recv_raw_timeout(&self, buf: &mut [u8], timeout: Duration) -> Result<Option<usize>> {
286        let mut poll_fd = libc::pollfd {
287            fd: self.fd.as_raw_fd(),
288            events: libc::POLLIN,
289            revents: 0,
290        };
291
292        let timeout_ms = timeout.as_millis().try_into().unwrap_or(libc::c_int::MAX);
293
294        let ret = unsafe { libc::poll(&mut poll_fd, 1, timeout_ms) };
295
296        if ret < 0 {
297            let err = std::io::Error::last_os_error();
298            return match err.raw_os_error() {
299                Some(libc::EINTR) => Err(Error::Interrupted),
300                _ => Err(Error::Os(err)),
301            };
302        }
303
304        if ret == 0 {
305            return Ok(None);
306        }
307
308        // recv_raw may return WouldBlock if the fd was set to non-blocking
309        // between poll() and recv(). Treat as timeout.
310        match self.recv_raw(buf) {
311            Ok(n) => Ok(Some(n)),
312            Err(Error::WouldBlock) => Ok(None),
313            Err(e) => Err(e),
314        }
315    }
316
317    /// Receive and parse the next process event.
318    ///
319    /// `buf` is the receive buffer provided by the caller (allocation control).
320    /// A buffer of at least 4096 bytes (one page) is recommended.
321    ///
322    /// This method handles all netlink control messages internally:
323    /// - `NLMSG_NOOP` → silently skipped, continue reading
324    /// - `NLMSG_DONE` (with no payload) → silently skipped, continue reading
325    ///   (The kernel connector protocol uses `NLMSG_DONE` with a cn_msg payload
326    ///   for data messages, which are parsed as events.)
327    /// - `NLMSG_ERROR` (non-zero) → returned as `Err(Os(...))`
328    /// - `NLMSG_OVERRUN` → returned as `Err(Overrun)`
329    /// - Valid data → parsed into `ProcEvent`
330    ///
331    /// # Errors
332    ///
333    /// See [`Self::recv_raw`] for system-level errors.
334    /// Additionally returns `BufferTooSmall` if the buffer is too small
335    /// to hold even a single netlink header.
336    ///
337    /// # Example
338    ///
339    /// ```no_run
340    /// use proc_connector::ProcConnector;
341    ///
342    /// let conn = ProcConnector::new().unwrap();
343    /// let mut buf = [0u8; 4096];
344    /// loop {
345    ///     match conn.recv(&mut buf) {
346    ///         Ok(event) => println!("{event}"),
347    ///         Err(e) => { eprintln!("{e}"); break; }
348    ///     }
349    /// }
350    /// ```
351    pub fn recv(&self, buf: &mut [u8]) -> Result<ProcEvent> {
352        self.recv_impl(buf)
353    }
354
355    /// Receive and parse the next process event with a timeout.
356    ///
357    /// Returns `Ok(None)` if the timeout expires before an event is available.
358    ///
359    /// Unlike `recv()`, this method returns `Ok(None)` on timeout instead of
360    /// blocking indefinitely. It properly loops past netlink control messages
361    /// (NLMSG_NOOP, NLMSG_DONE, NLMSG_ERROR-ACK) just like `recv()` does.
362    ///
363    /// # Errors
364    ///
365    /// See [`Self::recv_timeout`] for system-level errors.
366    pub fn recv_timeout(
367        &self,
368        buf: &mut [u8],
369        timeout: std::time::Duration,
370    ) -> Result<Option<ProcEvent>> {
371        if buf.len() < SIZE_NLMSGHDR {
372            return Err(Error::BufferTooSmall {
373                needed: SIZE_NLMSGHDR,
374            });
375        }
376
377        let deadline = std::time::Instant::now() + timeout;
378
379        loop {
380            let remaining = deadline.saturating_duration_since(std::time::Instant::now());
381            let n = match self.recv_raw_timeout(buf, remaining) {
382                Ok(Some(n)) => n,
383                Ok(None) => return Ok(None),
384                Err(Error::WouldBlock) => {
385                    return Ok(None);
386                }
387                Err(e) => return Err(e),
388            };
389
390            if let Some(event) = crate::parse::first_event_from_buf(buf, n)? {
391                return Ok(Some(event));
392            }
393        }
394    }
395
396    /// Internal: block until a process event is received.
397    ///
398    /// Loops past netlink control messages (NLMSG_NOOP, NLMSG_DONE, NLMSG_ERROR-ACK)
399    /// until a real ProcEvent is parsed.
400    fn recv_impl(&self, buf: &mut [u8]) -> Result<ProcEvent> {
401        if buf.len() < SIZE_NLMSGHDR {
402            return Err(Error::BufferTooSmall {
403                needed: SIZE_NLMSGHDR,
404            });
405        }
406
407        loop {
408            let n = match self.recv_raw(buf) {
409                Ok(n) => n,
410                Err(Error::WouldBlock) => {
411                    // Non-blocking mode and no data — caller should use
412                    // poll/AsyncFd instead of blocking recv.
413                    // Return a non-recoverable error for blocking API.
414                    return Err(Error::Os(std::io::Error::new(
415                        std::io::ErrorKind::WouldBlock,
416                        "socket is non-blocking, use AsyncFd to wait for readiness",
417                    )));
418                }
419                Err(e) => return Err(e),
420            };
421
422            // Parse all messages in the buffer
423            if let Some(event) = crate::parse::first_event_from_buf(buf, n)? {
424                return Ok(event);
425            }
426
427            // If we got here, all messages were control messages.
428            // Loop back and wait for a real event.
429        }
430    }
431
432    /// Expose the raw file descriptor for integration with async runtimes
433    /// (`tokio::AsyncFd`, `mio`, etc.).
434    ///
435    /// The returned `RawFd` remains valid for the lifetime of this
436    /// `ProcConnector`. Do not close it manually.
437    pub fn as_raw_fd(&self) -> RawFd {
438        self.fd.as_raw_fd()
439    }
440
441    /// Set the socket receive buffer size (`SO_RCVBUF`).
442    ///
443    /// Larger buffers reduce the risk of event loss under load.
444    /// Typical values range from 64 KiB to 1 MiB.
445    ///
446    /// # Errors
447    ///
448    /// Returns `Error::Os` if `setsockopt(2)` fails.
449    pub fn set_recv_buf_size(&self, bytes: usize) -> Result<()> {
450        let size = bytes as libc::c_int;
451        let ret = unsafe {
452            libc::setsockopt(
453                self.fd.as_raw_fd(),
454                libc::SOL_SOCKET,
455                libc::SO_RCVBUF,
456                &size as *const libc::c_int as *const libc::c_void,
457                std::mem::size_of::<libc::c_int>() as u32,
458            )
459        };
460        if ret < 0 {
461            return Err(Error::Os(std::io::Error::last_os_error()));
462        }
463        Ok(())
464    }
465
466    /// Set the socket to non-blocking mode.
467    ///
468    /// After calling this, `recv_raw` will return `Error::WouldBlock`
469    /// when no data is available, instead of blocking.
470    ///
471    /// # Errors
472    ///
473    /// Returns `Error::Os` if `fcntl(2)` fails.
474    ///
475    /// # Example
476    ///
477    /// ```no_run
478    /// use proc_connector::{ProcConnector, Error};
479    ///
480    /// let conn = ProcConnector::new().unwrap();
481    /// conn.set_nonblocking().unwrap();
482    ///
483    /// let mut buf = vec![0u8; 4096];
484    /// match conn.recv_raw(&mut buf) {
485    ///     Ok(n) => println!("received {n} bytes"),
486    ///     Err(Error::WouldBlock) => println!("no data available"),
487    ///     Err(e) => eprintln!("error: {e}"),
488    /// }
489    /// ```
490    pub fn set_nonblocking(&self) -> Result<()> {
491        let flags = unsafe { libc::fcntl(self.fd.as_raw_fd(), libc::F_GETFL) };
492        if flags < 0 {
493            return Err(Error::Os(std::io::Error::last_os_error()));
494        }
495        let ret =
496            unsafe { libc::fcntl(self.fd.as_raw_fd(), libc::F_SETFL, flags | libc::O_NONBLOCK) };
497        if ret < 0 {
498            return Err(Error::Os(std::io::Error::last_os_error()));
499        }
500        Ok(())
501    }
502}
503
504impl AsRawFd for ProcConnector {
505    fn as_raw_fd(&self) -> RawFd {
506        self.fd.as_raw_fd()
507    }
508}
509
510impl AsFd for ProcConnector {
511    fn as_fd(&self) -> BorrowedFd<'_> {
512        self.fd.as_fd()
513    }
514}
515
516impl std::fmt::Debug for ProcConnector {
517    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
518        f.debug_struct("ProcConnector")
519            .field("fd", &self.fd.as_raw_fd())
520            .finish()
521    }
522}
523
524impl Drop for ProcConnector {
525    fn drop(&mut self) {
526        // Best-effort unsubscribe; ignore errors since we're closing anyway.
527        let _ = self.unsubscribe();
528    }
529}
530
531// ---------------------------------------------------------------------------
532// EventMask
533// ---------------------------------------------------------------------------
534
535/// Bitmask of process event types for use with
536/// [`subscribe_filtered`](ProcConnector::subscribe_filtered).
537///
538/// # Example
539///
540/// ```
541/// use proc_connector::EventMask;
542/// let mask = EventMask::EXEC | EventMask::EXIT;
543/// ```
544#[derive(Debug, Clone, Copy, PartialEq, Eq)]
545pub struct EventMask(pub u32);
546
547impl EventMask {
548    /// Fork/clone events.
549    pub const FORK: EventMask = EventMask(PROC_EVENT_FORK);
550    /// Exec events.
551    pub const EXEC: EventMask = EventMask(PROC_EVENT_EXEC);
552    /// UID change events.
553    pub const UID: EventMask = EventMask(PROC_EVENT_UID);
554    /// GID change events.
555    pub const GID: EventMask = EventMask(PROC_EVENT_GID);
556    /// Session ID change events.
557    pub const SID: EventMask = EventMask(PROC_EVENT_SID);
558    /// Ptrace attach/detach events.
559    pub const PTRACE: EventMask = EventMask(PROC_EVENT_PTRACE);
560    /// Comm (process name) change events.
561    pub const COMM: EventMask = EventMask(PROC_EVENT_COMM);
562    /// Coredump events.
563    pub const COREDUMP: EventMask = EventMask(PROC_EVENT_COREDUMP);
564    /// Exit events.
565    pub const EXIT: EventMask = EventMask(PROC_EVENT_EXIT);
566    /// All event types.
567    pub const ALL: EventMask = EventMask(
568        PROC_EVENT_FORK
569            | PROC_EVENT_EXEC
570            | PROC_EVENT_UID
571            | PROC_EVENT_GID
572            | PROC_EVENT_SID
573            | PROC_EVENT_PTRACE
574            | PROC_EVENT_COMM
575            | PROC_EVENT_COREDUMP
576            | PROC_EVENT_EXIT,
577    );
578}
579
580impl std::ops::BitOr for EventMask {
581    type Output = EventMask;
582    fn bitor(self, rhs: EventMask) -> EventMask {
583        EventMask(self.0 | rhs.0)
584    }
585}
586
587// ---------------------------------------------------------------------------
588// Helper: create the netlink socket
589// ---------------------------------------------------------------------------
590
591fn create_socket() -> Result<OwnedFd> {
592    let fd = unsafe {
593        let fd = libc::socket(
594            libc::PF_NETLINK,
595            libc::SOCK_DGRAM | libc::SOCK_CLOEXEC,
596            NETLINK_CONNECTOR,
597        );
598        if fd < 0 {
599            return Err(Error::Os(std::io::Error::last_os_error()));
600        }
601        OwnedFd::from_raw_fd(fd)
602    };
603
604    Ok(fd)
605}