ferranet 0.2.0

A modern, async-first, zero-copy datalink-layer (L2) networking library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
//! The `AF_PACKET` socket: creation, binding, basic send/receive, and option helpers.
//!
//! [`PacketSocket`] owns the file descriptor (via [`OwnedFd`], so it is closed on drop) and is
//! the foundation both the basic and ring backends build on. It concentrates the raw `libc`
//! syscalls behind a small safe surface; every `unsafe` block carries a `SAFETY` note.

use std::ffi::c_int;
use std::io;
use std::mem;
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};

use crate::error::{Error, Result};
use crate::interface::IfIndex;
use crate::sys::Stats;

/// `ETH_P_ALL` in host byte order — capture/inject every protocol.
pub const ETH_P_ALL: u16 = 0x0003;

/// The socket flavour: whether the kernel hands us the link-layer header or strips it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SocketKind {
    /// `SOCK_RAW`: frames include the full Ethernet header on both RX and TX.
    Raw,
    /// `SOCK_DGRAM`: the kernel strips/adds the link-layer header for you.
    Dgram,
}

impl SocketKind {
    const fn as_raw(self) -> c_int {
        match self {
            SocketKind::Raw => libc::SOCK_RAW,
            SocketKind::Dgram => libc::SOCK_DGRAM,
        }
    }
}

/// An open, interface-bound `AF_PACKET` socket.
#[derive(Debug)]
pub struct PacketSocket {
    fd: OwnedFd,
    ifindex: IfIndex,
}

impl PacketSocket {
    /// Opens an `AF_PACKET` socket and binds it to `ifindex`.
    ///
    /// `protocol` is given in host byte order (e.g. [`ETH_P_ALL`]). The socket is created
    /// non-blocking and close-on-exec.
    pub fn open(ifindex: IfIndex, protocol: u16, kind: SocketKind) -> Result<Self> {
        let proto_be = i32::from(protocol.to_be());
        // SAFETY: a plain socket(2) call; arguments are valid constants. The returned fd is
        // immediately adopted by an OwnedFd so it cannot leak.
        let raw = unsafe {
            libc::socket(
                libc::AF_PACKET,
                kind.as_raw() | libc::SOCK_NONBLOCK | libc::SOCK_CLOEXEC,
                proto_be,
            )
        };
        if raw < 0 {
            return Err(Error::Socket(io::Error::last_os_error()));
        }
        // SAFETY: `raw` is a fresh, valid, owned fd returned by socket(2) just above.
        let fd = unsafe { OwnedFd::from_raw_fd(raw) };
        let sock = PacketSocket { fd, ifindex };
        sock.bind(protocol)?;
        Ok(sock)
    }

    fn bind(&self, protocol: u16) -> Result<()> {
        // SAFETY: sockaddr_ll is plain-old-data; an all-zero value is a valid initial state.
        let mut addr: libc::sockaddr_ll = unsafe { mem::zeroed() };
        addr.sll_family = libc::AF_PACKET as u16;
        addr.sll_protocol = protocol.to_be();
        addr.sll_ifindex = self.ifindex.0 as c_int;

        // SAFETY: `addr` is a fully-initialised sockaddr_ll; we pass its real size. The fd is
        // valid for the duration of the call.
        let rc = unsafe {
            libc::bind(
                self.fd.as_raw_fd(),
                (&raw const addr).cast::<libc::sockaddr>(),
                mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
            )
        };
        if rc < 0 {
            return Err(Error::Bind(io::Error::last_os_error()));
        }
        Ok(())
    }

    /// Sends a single frame. Returns `WouldBlock` if the kernel's send buffer is full.
    pub fn send(&self, frame: &[u8]) -> io::Result<usize> {
        // SAFETY: `frame` is a valid readable slice of `frame.len()` bytes; the fd is valid.
        let n = unsafe {
            libc::send(
                self.fd.as_raw_fd(),
                frame.as_ptr().cast(),
                frame.len(),
                libc::MSG_DONTWAIT,
            )
        };
        if n < 0 {
            return Err(io::Error::last_os_error());
        }
        Ok(n as usize)
    }

    /// Receives a single frame into `buf`, returning `(wire_len, packet_type)`.
    ///
    /// `MSG_TRUNC` makes the kernel report the frame's true on-wire length, so `wire_len` may
    /// exceed `buf.len()` if the frame was truncated; the caller must clamp its slice to
    /// `buf.len()`. `packet_type` is the raw `sll_pkttype`.
    pub fn recv_into(&self, buf: &mut [u8]) -> io::Result<(usize, u8)> {
        // SAFETY: sockaddr_ll is plain-old-data; an all-zero value is a valid initial state.
        let mut addr: libc::sockaddr_ll = unsafe { mem::zeroed() };
        let mut addr_len = mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t;
        // SAFETY: `buf` is a valid writable slice of `buf.len()` bytes; `addr`/`addr_len` are a
        // valid sockaddr storage and its length. The fd is valid.
        let n = unsafe {
            libc::recvfrom(
                self.fd.as_raw_fd(),
                buf.as_mut_ptr().cast(),
                buf.len(),
                libc::MSG_DONTWAIT | libc::MSG_TRUNC,
                (&raw mut addr).cast::<libc::sockaddr>(),
                &raw mut addr_len,
            )
        };
        if n < 0 {
            return Err(io::Error::last_os_error());
        }
        Ok((n as usize, addr.sll_pkttype))
    }

    /// Enables or disables promiscuous mode on the bound interface.
    pub fn set_promiscuous(&self, on: bool) -> Result<()> {
        // SAFETY: packet_mreq is plain-old-data; an all-zero value is a valid initial state.
        let mut mreq: libc::packet_mreq = unsafe { mem::zeroed() };
        mreq.mr_ifindex = self.ifindex.0 as c_int;
        mreq.mr_type = libc::PACKET_MR_PROMISC as u16;
        let opt = if on { libc::PACKET_ADD_MEMBERSHIP } else { libc::PACKET_DROP_MEMBERSHIP };
        // SAFETY: `mreq` is a fully-initialised packet_mreq passed with its true size.
        let rc = unsafe {
            libc::setsockopt(
                self.fd.as_raw_fd(),
                libc::SOL_PACKET,
                opt,
                (&raw const mreq).cast(),
                mem::size_of::<libc::packet_mreq>() as libc::socklen_t,
            )
        };
        if rc < 0 {
            return Err(Error::SetSockOpt {
                option: "PACKET_MR_PROMISC",
                source: io::Error::last_os_error(),
            });
        }
        Ok(())
    }

    /// Joins this socket to a `PACKET_FANOUT` group, so the kernel load-balances received frames
    /// across all sockets in the group (one per core, for multi-queue RX scaling).
    ///
    /// `mode` is a `PACKET_FANOUT_*` selector (already including any flags). The kernel encodes the
    /// request as `group_id | (mode << 16)`.
    pub fn set_fanout(&self, group_id: u16, mode: u16) -> Result<()> {
        self.set_packet_opt_int(libc::PACKET_FANOUT, "PACKET_FANOUT", fanout_arg(group_id, mode))
    }

    /// Claims a fresh, kernel-allocated `PACKET_FANOUT` group (`PACKET_FANOUT_FLAG_UNIQUEID`) and
    /// returns its id, for subsequent sockets to join with [`PacketSocket::set_fanout`].
    ///
    /// Group ids live in a flat 16-bit space per network namespace, and the kernel happily merges
    /// same-id joins from unrelated processes into one load-balanced group — so a guessed default
    /// id risks silently sharing frames with a stranger. Kernels older than 4.5 reject the flag
    /// with `EINVAL`; there we fall back to `fallback_id`, accepting the (pre-existing) collision
    /// risk.
    pub fn set_fanout_unique(&self, mode: u16, fallback_id: u16) -> Result<u16> {
        let flagged = mode | libc::PACKET_FANOUT_FLAG_UNIQUEID as u16;
        match self.set_packet_opt_int(libc::PACKET_FANOUT, "PACKET_FANOUT", fanout_arg(0, flagged))
        {
            Ok(()) => self.fanout_id(),
            Err(Error::SetSockOpt { source, .. })
                if source.raw_os_error() == Some(libc::EINVAL) =>
            {
                self.set_fanout(fallback_id, mode)?;
                Ok(fallback_id)
            }
            Err(e) => Err(e),
        }
    }

    /// Reads back the id of the fanout group this socket belongs to.
    fn fanout_id(&self) -> Result<u16> {
        let mut val: c_int = 0;
        let mut len = mem::size_of::<c_int>() as libc::socklen_t;
        // SAFETY: `val`/`len` are valid storage and its length for getsockopt to fill.
        let rc = unsafe {
            libc::getsockopt(
                self.fd.as_raw_fd(),
                libc::SOL_PACKET,
                libc::PACKET_FANOUT,
                (&raw mut val).cast(),
                &raw mut len,
            )
        };
        if rc < 0 {
            return Err(Error::SetSockOpt {
                option: "PACKET_FANOUT",
                source: io::Error::last_os_error(),
            });
        }
        Ok((val & 0xffff) as u16)
    }

    /// Sets an integer-valued `SOL_PACKET` option.
    pub fn set_packet_opt_int(&self, opt: c_int, name: &'static str, val: c_int) -> Result<()> {
        // SAFETY: `val` is a valid c_int passed with size_of::<c_int>(); fd is valid.
        let rc = unsafe {
            libc::setsockopt(
                self.fd.as_raw_fd(),
                libc::SOL_PACKET,
                opt,
                (&raw const val).cast(),
                mem::size_of::<c_int>() as libc::socklen_t,
            )
        };
        if rc < 0 {
            return Err(Error::SetSockOpt { option: name, source: io::Error::last_os_error() });
        }
        Ok(())
    }

    /// Sets a `SOL_PACKET` option from an arbitrary POD request struct (e.g. a ring request).
    ///
    /// # Safety
    ///
    /// `T` must be a plain-old-data type the kernel expects for `opt`; `req` is copied by value
    /// into the kernel, so it must be fully initialised with no padding requirements violated.
    pub unsafe fn set_packet_opt<T>(&self, opt: c_int, name: &'static str, req: &T) -> Result<()> {
        // SAFETY: by this function's contract `req` is a valid `T` of the size the kernel expects
        // for `opt`; we pass its real size. The fd is valid.
        let rc = unsafe {
            libc::setsockopt(
                self.fd.as_raw_fd(),
                libc::SOL_PACKET,
                opt,
                (req as *const T).cast(),
                mem::size_of::<T>() as libc::socklen_t,
            )
        };
        if rc < 0 {
            return Err(Error::SetSockOpt { option: name, source: io::Error::last_os_error() });
        }
        Ok(())
    }

    /// Reads and resets the kernel's `tpacket_stats_v3` counters.
    pub fn statistics(&self) -> io::Result<Stats> {
        // SAFETY: tpacket_stats_v3 is plain-old-data; an all-zero value is a valid initial state.
        let mut stats: libc::tpacket_stats_v3 = unsafe { mem::zeroed() };
        let mut len = mem::size_of::<libc::tpacket_stats_v3>() as libc::socklen_t;
        // SAFETY: `stats`/`len` are valid storage and its length; getsockopt fills `stats` and
        // updates `len`. The fd is valid.
        let rc = unsafe {
            libc::getsockopt(
                self.fd.as_raw_fd(),
                libc::SOL_PACKET,
                libc::PACKET_STATISTICS,
                (&raw mut stats).cast(),
                &raw mut len,
            )
        };
        if rc < 0 {
            return Err(io::Error::last_os_error());
        }
        Ok(Stats {
            received: u64::from(stats.tp_packets),
            dropped: u64::from(stats.tp_drops),
            freezes: u64::from(stats.tp_freeze_q_cnt),
        })
    }
}

/// Encodes a `PACKET_FANOUT` request: the group id in the low 16 bits, the mode (plus any flags)
/// in the high 16 bits.
pub fn fanout_arg(group_id: u16, mode: u16) -> c_int {
    c_int::from(group_id) | (c_int::from(mode) << 16)
}

/// Blocks until `fd` reports one of `events` (or `timeout_ms` elapses).
///
/// Interrupted waits (`EINTR`) are retried rather than surfaced, so a stray signal (a profiler's
/// `SIGPROF`, `SIGCHLD`, ...) never aborts a blocking wait. The retry reuses the original
/// timeout, over-waiting in the worst case; every caller today passes `-1` or `0`, where that is
/// moot. Error conditions (`POLLERR`/`POLLHUP`/`POLLNVAL`) count as ready: the caller's next
/// syscall then surfaces the real errno, instead of the caller looping straight back into an
/// instantly-returning poll (a silent busy-spin on a socket stuck at `POLLERR`).
fn poll_events(fd: BorrowedFd<'_>, events: i16, timeout_ms: c_int) -> io::Result<bool> {
    let mut pfd = libc::pollfd { fd: fd.as_raw_fd(), events, revents: 0 };
    loop {
        // SAFETY: `pfd` is a single valid pollfd; we pass a count of 1.
        let rc = unsafe { libc::poll(&raw mut pfd, 1, timeout_ms) };
        if rc < 0 {
            let err = io::Error::last_os_error();
            if err.kind() == io::ErrorKind::Interrupted {
                continue;
            }
            return Err(err);
        }
        let ready = events | libc::POLLERR | libc::POLLHUP | libc::POLLNVAL;
        return Ok(rc > 0 && pfd.revents & ready != 0);
    }
}

/// Blocks until `fd` is readable (or `timeout_ms` elapses), returning whether it became readable.
///
/// `timeout_ms` of `-1` waits indefinitely. Used by the synchronous receive path. Signal- and
/// `POLLERR`-robust; see [`poll_events`].
pub fn poll_readable(fd: BorrowedFd<'_>, timeout_ms: c_int) -> io::Result<bool> {
    poll_events(fd, libc::POLLIN, timeout_ms)
}

/// Blocks until `fd` is writable (or `timeout_ms` elapses), returning whether it became writable.
///
/// `timeout_ms` of `-1` waits indefinitely. Used by the synchronous transmit path to wait out a
/// full TX ring or send buffer. Signal- and `POLLERR`-robust; see [`poll_events`].
pub fn poll_writable(fd: BorrowedFd<'_>, timeout_ms: c_int) -> io::Result<bool> {
    poll_events(fd, libc::POLLOUT, timeout_ms)
}

impl AsFd for PacketSocket {
    fn as_fd(&self) -> BorrowedFd<'_> {
        self.fd.as_fd()
    }
}

impl AsRawFd for PacketSocket {
    fn as_raw_fd(&self) -> RawFd {
        self.fd.as_raw_fd()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn fanout_arg_encoding() {
        // group id in low 16 bits, mode in high 16 bits.
        assert_eq!(fanout_arg(0x1234, libc::PACKET_FANOUT_LB as u16), (1 << 16) | 0x1234);
        assert_eq!(fanout_arg(0, libc::PACKET_FANOUT_HASH as u16), 0);
        assert_eq!(fanout_arg(0xffff, libc::PACKET_FANOUT_CPU as u16), (2 << 16) | 0xffff);
    }

    extern "C" fn noop_signal_handler(_sig: c_int) {}

    #[test]
    #[cfg_attr(miri, ignore = "uses poll on pipe fds; not interpretable by Miri")]
    fn poll_writable_waits_for_capacity() {
        let (read, write) = crate::dummy::make_pipe().expect("pipe");

        // Fill the (non-blocking) pipe so its write end is not writable.
        let chunk = [0u8; 4096];
        loop {
            // SAFETY: writing from a valid local buffer to a valid pipe fd we own.
            let n = unsafe { libc::write(write.as_raw_fd(), chunk.as_ptr().cast(), chunk.len()) };
            if n < 0 {
                break; // EAGAIN: full
            }
        }
        assert!(!poll_writable(write.as_fd(), 0).expect("poll full pipe"), "pipe must be full");

        let poller = std::thread::spawn(move || {
            let ok = poll_writable(write.as_fd(), -1);
            drop(write);
            ok
        });
        std::thread::sleep(std::time::Duration::from_millis(50));
        // Drain the pipe; the blocked poll must now report writability.
        let mut buf = [0u8; 65536];
        loop {
            // SAFETY: reading into a valid local buffer from a valid pipe fd we own.
            let n = unsafe { libc::read(read.as_raw_fd(), buf.as_mut_ptr().cast(), buf.len()) };
            if n <= 0 {
                break;
            }
        }
        let res = poller.join().expect("poller thread");
        assert!(matches!(res, Ok(true)), "poll_writable must report capacity, got {res:?}");
    }

    #[test]
    #[cfg_attr(miri, ignore = "uses poll/sigaction/pthread_kill; not interpretable by Miri")]
    fn poll_readable_survives_signals() {
        // Install a no-op SIGUSR1 handler *without* SA_RESTART, so poll(2) returns EINTR.
        // SAFETY: installing a valid handler function for SIGUSR1; sigaction storage is zeroed
        // then fully initialised before use.
        unsafe {
            let mut sa: libc::sigaction = mem::zeroed();
            sa.sa_sigaction = noop_signal_handler as *const () as usize;
            libc::sigemptyset(&raw mut sa.sa_mask);
            sa.sa_flags = 0;
            assert_eq!(libc::sigaction(libc::SIGUSR1, &raw const sa, std::ptr::null_mut()), 0);
        }

        let (read, write) = crate::dummy::make_pipe().expect("pipe");
        let (tid_tx, tid_rx) = std::sync::mpsc::channel();
        let poller = std::thread::spawn(move || {
            // SAFETY: pthread_self has no preconditions.
            tid_tx.send(unsafe { libc::pthread_self() }).expect("send tid");
            poll_readable(read.as_fd(), -1)
        });

        let tid = tid_rx.recv().expect("tid");
        std::thread::sleep(std::time::Duration::from_millis(50));
        // Interrupt the blocked poll; a robust wait must resume rather than surface EINTR.
        // SAFETY: `tid` is the live poller thread; SIGUSR1 has the no-op handler installed.
        unsafe { libc::pthread_kill(tid, libc::SIGUSR1) };
        std::thread::sleep(std::time::Duration::from_millis(50));
        // Only now make the fd readable.
        // SAFETY: writing one byte from a valid local buffer to a valid pipe fd we own.
        unsafe { libc::write(write.as_raw_fd(), [1u8].as_ptr().cast(), 1) };

        let res = poller.join().expect("poller thread");
        assert!(matches!(res, Ok(true)), "poll_readable must retry EINTR, got {res:?}");
    }

    #[test]
    #[cfg_attr(miri, ignore = "opens an AF_PACKET socket; not interpretable by Miri")]
    fn open_without_privileges_reports_socket_error() {
        // Opening an AF_PACKET socket needs CAP_NET_RAW. In an unprivileged environment this must
        // fail with a clear Error::Socket rather than panicking; with privileges it succeeds.
        match PacketSocket::open(IfIndex(1), ETH_P_ALL, SocketKind::Raw) {
            Ok(sock) => {
                // We have CAP_NET_RAW: the socket should hold a valid descriptor.
                assert!(sock.as_raw_fd() >= 0);
            }
            Err(Error::Socket(_)) | Err(Error::Bind(_)) => {}
            Err(other) => panic!("unexpected error opening packet socket: {other}"),
        }
    }
}