ferranet 0.2.0

A modern, async-first, zero-copy datalink-layer (L2) networking library
Documentation
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

ferranet is an async-first, zero-copy **datalink-layer (L2)** networking library for Rust — open an
interface and send/receive raw Ethernet frames with batching and zero-copy receive. Linux-only
(v0.2), edition 2024, MSRV 1.85. Packet **parsing/construction is deliberately out of scope** (pair
with `etherparse`). Beta: the `AF_PACKET` backends are validated on real hardware (Raspberry Pi
Zero 2 W via `pibench`); AF_XDP is veth-validated only, throughput numbers in `BENCHMARKS.md` are
`veth`-only, and the API may still break before 1.0.

## Commands

The CI pipeline (`.tangled/workflows/ci.yml`) is the source of truth — these four gates must pass:

```sh
cargo clippy --all-targets --all-features -- -D warnings   # warnings are hard errors
cargo test --all-features
cargo build --no-default-features                          # sync-only build must keep compiling
cargo doc --no-deps --all-features
```

CI additionally attempts the veth suite inside `unshare --user --map-root-user --net`, skipping
with a log line if the runner forbids user namespaces — run it locally the same way (see below).

`cargo test` needs no privileges — it runs the lib, `dummy`, and `proptest` suites; the syscall
paths are `#[ignore]`d. Run one test with `cargo test <name>`, or a file with `cargo test --test
<file>` (e.g. `--test property`, `--test dummy`).

Deeper checks:
- **Miri** (UB in the pure-memory unsafe, mainly the block parser): `cargo +nightly miri test
  --lib`. Syscall/FFI paths are `#[ignore]`d under Miri (it can't interpret syscalls).
- **Fuzz** the block parser: `cargo +nightly fuzz run parse_block` (see `fuzz/`).
- **veth integration** (`tests/veth.rs`, `#[ignore]`d, needs `CAP_NET_RAW`): build *outside* the
  namespace, run *inside* it — the namespace has no network so it can't compile there:
  ```sh
  cargo test --test veth --no-run
  unshare --user --map-root-user --net env RUSTC_WRAPPER= cargo test --test veth -- --ignored --test-threads=1
  ```
- **XDP end-to-end** (`tests/xdp_veth.rs`, `#[ignore]`d, needs **real root**`bpf(2)` checks
  capabilities in the initial namespace, so `unshare` is not enough; it skips cleanly there):
  loads the checked-in XSK redirect program (`tests/data/xsk_redirect.{c,o}`) with `aya` and
  round-trips frames both ways between `AF_PACKET` and an `XdpSocket` over veth:
  ```sh
  cargo test --features xdp --test xdp_veth --no-run
  sudo -E unshare --net env RUSTC_WRAPPER= cargo test --features xdp --test xdp_veth -- --ignored
  ```
- **Real-hardware** (`tests/hw.rs`): set `FERRANET_HW_IFACE_A`/`_B` to two cabled ports; skips
  cleanly when unset, so plain `cargo test` stays green.
- **Benches** (Criterion): `cargo bench --bench parse` is privilege-free; `--bench throughput` needs
  the namespace like veth. `scripts/bench.sh` runs everything.

## Architecture

The public API is platform-agnostic; everything OS-specific sits behind the `RawChannel` trait in
`src/sys/` — the seam for adding BSD/Windows later without API churn.

- **`src/channel.rs`** — synchronous API: `Channel::builder(iface)``(Sender, Receiver)` via
  `build_sync()`, plus `RingConfig` (ring geometry / snaplen / `retire_blk_tov_ms`) and
  `FanoutMode`. `build_fanout_rx(n)` opens a `PACKET_FANOUT` group for multi-core RX.
- **`src/async_channel.rs`** — the Tokio surface (`AsyncSender`/`AsyncReceiver` via `build_async()`),
  gated behind the default `tokio` feature. `default-features = false` drops it for a smaller
  sync-only build — which is why the no-default-features build is a CI gate.
- **`src/block.rs`** — the zero-copy receive model: `recv_block()` returns a `Block` borrowing a
  kernel ring block; `block.frames()` yields `Frame`s valid *until the block is dropped* (no copy).
  Frame metadata (timestamp/vlan/packet_type) is decoded lazily on access. **This is the main
  untrusted-input surface** — the target of the fuzzer, Miri, and the proptests.
- **`src/sys/linux/`** — all `unsafe` and FFI lives here: `ring.rs` (TPACKET_V3 RX block ring +
  TPACKET_V2 TX ring), `socket.rs` (AF_PACKET socket, `PACKET_*` sockopts/stats), `tpacket.rs` (ring
  layout math), `xdp.rs` (the `xdp`-feature AF_XDP backend). Each `unsafe` block carries a `SAFETY`
  comment resting on the kernel's `TP_STATUS_*` ownership protocol.
- **`src/dummy.rs`** — in-memory `Sender`/`Receiver` backed by FIFO queues + a pipe for readiness:
  privilege-free, deterministic packet-handling tests that work with sync/poll/async alike.
- **`src/interface.rs`**`interfaces()` enumeration (`Interface { name, index, mac, ips, mtu, .. }`)
  and `MacAddr`.

Two backends ship: **AF_PACKET/PACKET_MMAP** (default, the zero-copy ring) and **AF_XDP**
(`--features xdp`, kernel-bypass). AF_XDP receives only what an external XDP redirect program steers
in — ferranet drives the socket/rings but does not load the program.

Drop detection is first-class: `Block::is_losing()` reads `TP_STATUS_LOSING` with no syscall;
`Receiver::stats()` gives cumulative `received`/`dropped`/`freezes` (ferranet accumulates the
kernel's reset-on-read counter).

## Conventions

- **Unsafe discipline**: keep all `unsafe`/FFI inside `src/sys/linux/`. Lints enforce the rest —
  `undocumented_unsafe_blocks` and `missing_docs` are warn, `unsafe_op_in_unsafe_fn` is forbid, and
  clippy runs `-D warnings` in CI; so every `unsafe` block needs a `SAFETY:` comment and every
  public item needs a doc comment.
- **CAP_NET_RAW** is required to open AF_PACKET sockets — grant with `setcap cap_net_raw+ep
  <binary>` or run under `unshare --user --map-root-user --net`.
- **Version control: this repo uses jujutsu (`jj`), not plain git** — commit and branch with `jj`.

## Low-power / Pi benchmarking

`examples/pibench.rs` + `scripts/pibench*.sh` compare ferranet's capture backends
(`ring`/`basic`/`pnet`/`pcap`) on constrained ARM hardware, measuring CPU-per-frame rather than peak
pps. See `docs/pi-bench.md`. Build for the Pi with `cross`:

```sh
CARGO_TARGET_DIR=target-cross cross build --release --no-default-features \
  --example pibench --target aarch64-unknown-linux-gnu
```

A separate `CARGO_TARGET_DIR` is needed because a prior native `cargo build` leaves x86-64
build-script artifacts in `target/` that the older cross container can't run (glibc mismatch). The
`pcap` backend additionally needs `--features pcap-bench` and arm64 libpcap in the cross image (see
`scripts/Cross.pcap.toml`).