# 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`).