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}