Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# CLAUDE.md

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

## What this is

Userspace re-implementation of Realtek's RTL88xxAU Wi-Fi driver — speaks to the chip
directly via libusb instead of a kernel module. Static library `WiFiDriver` + two
demo binaries (`WiFiDriverDemo` for RX, `WiFiDriverTxDemo` for TX). Used by the
OpenIPC project for long-range video links.

Targets the Realtek "Jaguar" 1st-gen 802.11ac family: **RTL8812AU** (2T2R,
reference), **RTL8811AU** (1T1R cut of 8812 silicon — rides the 8812 code
path with `RFType=RF_TYPE_1T1R` selected via `REG_SYS_CFG` bit 27),
**RTL8814AU** (4T4R RF / 3-SS baseband; RX solid, TX validated on
fresh-chip single-cell runs but unstable after USB passthrough cycles —
issue #36), and **RTL8821AU** (1T1R AC + BT combo; proper 8821-specific
init flow landed in PR #42, Android hotplug confirmed end-to-end).

NOT 8821AU's family confusion: it IS Jaguar wave 1 (CHIP_8821 = 7 in
Realtek's HalVerDef, shares the enum with CHIP_8812), not Jaguar2 as
sometimes claimed. The **Jaguar2 (8812BU, 8822BU/BE, ...) and Kestrel
(11ax) families are out of scope** — they share the "AU"/"BU" branding
but the baseband and HAL differ enough to need their own driver.

## Build

```sh
cmake -S . -B build
cmake --build build -j
```

libusb-1.0 is required: `pkg-config` on Linux/macOS, vcpkg on Windows
(`VCPKG_ROOT` must be set so the toolchain file resolves). CI matrix builds
across GCC/Clang/MSVC on Ubuntu/macOS/Windows
(`.github/workflows/cmake-multi-platform.yml`). `ctest` runs in CI but no CMake
tests are registered — regression testing happens out-of-band via
`tests/regress.py`.

## Regression testing

`tests/regress.py` runs a 2x2 TX/RX matrix (devourer vs. kernel driver) using
two USB Wi-Fi adapters plugged into the host. Run **after** building devourer.

```sh
# Local mode — kernel cells use whatever driver is bound on the host
sudo python3 tests/regress.py --channel 100

# VM mode (recommended for RTL8814AU and other chips whose kernel driver
# doesn't build on bleeding-edge kernels) — kernel cells run inside a
# pinned-kernel (Ubuntu 22.04 / 5.15) libvirt VM with aircrack-ng/rtl8812au
# preloaded. Provision once with tests/setup_vm.sh, then:
sudo python3 tests/regress.py --channel 100 \
--vm-name devourer-testrig --vm-ssh <user>@<VM-IP>
```

Three specialised modes layered on top of the default 4-cell matrix:

- `--full-matrix`: iterates every ordered (TX, RX) pair of plugged DUTs
across all four driver-side combinations (N×(N−1)×4 cells; ~16 min for
N=3 in VM mode). Use for cross-chipset interop regressions.
- `--encoding-matrix --tx-pid 0xNNNN --rx-pid 0xNNNN`: for one ordered
pair, iterates (driver-mode × radiotap encoding combo) — 4 HT
combos (BCC / LDPC / STBC=1 / LDPC+STBC) + 2 VHT combos
(BCC / LDPC), 24 cells total.
- `--sniffer-iface IFACE`: when set, captures on a 3rd monitor-mode
adapter alongside every cell and reports decoded radiotap encoding
distribution. Intended for AR9271 — vanilla radiotap, no driver-side
filtering. NB: `aircrack-ng/88XXau` on the kernel-TX side strips the
radiotap LDPC + STBC bits before air, so the `k/k` and `k/d` rows of
`--encoding-matrix` are NOT authoritative for LDPC/STBC asymmetries
(MCS index + HT/VHT distinction do survive).

DUTs are routed between host and VM per cell via `virsh attach-device`.
Re-run with `--keep-logs` to inspect per-cell logs (and per-cell pcaps
when `--sniffer-iface` is active) at `/tmp/devourer-regress-last/`. See
`tests/README.md` for the full matrix semantics and prerequisites.

## Demo env vars

Both `WiFiDriverDemo` and `WiFiDriverTxDemo` honour:

- `DEVOURER_PID=0xNNNN` — restrict the open loop to a single PID (e.g.
`0x8813` for RTL8814AU); without it, the demo iterates every Realtek PID.
- `DEVOURER_VID=0xNNNN` — override VID (default `0x0bda`); needed for
OEM-rebadged dongles like the TP-Link Archer T2U Plus (`2357:0120`).
- `DEVOURER_CHANNEL=N` — override monitor channel.
- `DEVOURER_SKIP_RESET=1` — skip `libusb_reset_device` before claim; useful
when picking up a chip whose firmware is already running.
- `DEVOURER_FORCE_TXPWR=1` — force the per-rate TX-power loop during channel
switch. Skipped by default in 8814 monitor mode (~300 vendor ctrl
transfers, indices are unused for RX-only).
- `DEVOURER_USB_QUIET=1` — downgrade libusb log level from DEBUG to WARNING
(DEBUG produces ~7 MB per 15 s and has filled `/tmp` mid-capture).

`WiFiDriverTxDemo` additionally honours radiotap-encoding knobs that
patch the beacon's MCS info field (or, with `_VHT=1`, replace it with a
VHT info field) before the bulk-OUT loop:

- `DEVOURER_TX_MCS=N` — HT MCS index (0..31). Default 1.
- `DEVOURER_TX_LDPC=1` — set FEC type LDPC (vs default BCC).
- `DEVOURER_TX_STBC=N` — STBC stream count (0..3). Default 0.
- `DEVOURER_TX_BW=20|40|80|160` — HT honours 20/40; VHT honours 20-160.
- `DEVOURER_TX_VHT=1` — switch from HT MCS field (13-byte radiotap) to
VHT info field (22-byte radiotap). Exposes:
- `DEVOURER_TX_VHT_MCS=N` — VHT MCS index (0..9 typical).
- `DEVOURER_TX_VHT_NSS=N` — VHT spatial streams.

`_LDPC` / `_STBC` / `_BW` apply to whichever (HT/VHT) mode is active.

## Architecture

**The caller owns libusb.** `WiFiDriver::CreateRtlDevice` is intentionally
thin — `libusb_init`, device open, kernel driver detach, and
`libusb_claim_interface(handle, 0)` must happen **before** handing the handle
to the factory. `demo/main.cpp` is the canonical boilerplate.

**Chip identity is resolved at construction** from SYS_CFG bits + USB PID;
`RtlJaguarDevice` (the orchestrator) then drives bring-up, RX, and TX
through chip-family branches. The class name was previously `Rtl8812aDevice`
— a deprecated alias still exists for one release cycle.

Module layout in `src/`:

- `RtlJaguarDevice` — top-level orchestrator (Init / InitWrite / send_packet).
- `HalModule` — chip bring-up, power sequencing, BB/AGC/RF table application,
USB EP priority, FIFO/page-boundary init. Most of the chip-family-specific
divergence lives here (`_8812A`, `_8814A`, `_8821A` suffixed methods).
- `RadioManagementModule` — channel, bandwidth, TX power; supports up to 4
RF paths for 8814.
- `EepromManager` — EFUSE / EEPROM read + autoload state; supplies the
`cut_version` / `rfe_type` used by `PhyTableLoader::CheckPositive`.
- `FirmwareManager` — chip-specific FW download (`FirmwareDownload_8814A`
uses the 3081-DDMA path; 8812/8821 use the page-write path).
- `PhyTableLoader` — runtime walker for Realtek's phydm-format register
tables (opcode-encoded conditional blocks). Replicates upstream phydm's
`check_positive` + state machine **without pulling in phydm itself**.
- `RtlUsbAdapter` — libusb wrapper (vendor control + bulk transfers).
- `FrameParser` — RX parsing and TX descriptor layout (`SET_TX_DESC_*_8812`
macros are shared across the Jaguar family).
- `Radiotap.c` — radiotap header iterator. TX buffers passed to
`send_packet` **must** begin with a radiotap header; rate / MCS / VHT /
STBC / LDPC / SGI / bandwidth are read from it.

`hal/` holds vendor headers and tables ported from Realtek's tree. The
8814-specific BB/AGC/RF tables under `hal/phydm/rtl8814a/Hal8814_PhyTables.{c,h}`
are **generated** from the upstream aircrack-ng/rtl8814au source by
`tools/extract_8814a_phy_tables.py` — edit the generator, not the output.
The runtime parser for these tables is `src/PhyTableLoader`, not the upstream
phydm parser.

## Hardware gotchas

- **ZeroCD trap**: some Realtek dongles enumerate first as a USB
mass-storage device (`0bda:1a2b` is the canonical offender) exposing the
Windows installer, then re-enumerate as the NIC after a mode switch. If
`libusb_open_device_with_vid_pid` returns NULL, check `lsusb` — may need
`usb_modeswitch` first.
- **RTL8814AU TX is flaky after USB passthrough cycles** (issue #36):
fresh-chip single-cell runs send ~4000 frames with 0 submit failures,
but after one or more virsh attach/detach cycles `libusb_bulk_transfer`
starts returning `LIBUSB_ERROR_IO` on 90%+ of submits. Every full-
matrix run since #34 has reproduced this. `RX = devourer` 8814 cells
are also still 0 (RX path itself is separately broken — not in scope
for #36).
- **rmmod/sysfs-unbind actively de-inits the chip** (RF off, MAC DMA off).
After detaching a kernel driver, expect to re-init from cold, not warm.
`DEVOURER_SKIP_RESET=1` only helps when firmware state is still intact.

## TX path

```cpp
auto logger = std::make_shared<Logger>();
WiFiDriver driver(logger);
auto dev = driver.CreateRtlDevice(handle); // handle is already claimed
dev->InitWrite(SelectedChannel{ .Channel = 36, .ChannelOffset = 0,
.ChannelWidth = CHANNEL_WIDTH_20 });
dev->send_packet(buffer, len); // buffer[0..] = radiotap header, then 802.11
```

The canonical test beacon (`txdemo/main.cpp`) uses SA `57:42:75:05:d6:00` —
the same constant is hardcoded into `demo/main.cpp` as the `<devourer-tx-hit>`
matcher and into `tests/regress.py` (`CANONICAL_SA`). Change all three
together if it ever moves.
77 changes: 58 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,37 @@
The Realtek 11ac driver that simply devours its competitors.

Devourer is a userspace re-implementation of Realtek's RTL88xxAU Wi-Fi
driver (Jaguar family: RTL8812AU shipping, RTL8814AU RX-only, RTL8811AU
WIP), speaking to the chip directly through libusb. No kernel module, no
`rtl8812au` DKMS tree — just a C++20 static library (`WiFiDriver`) plus two
demo executables for RX and TX. It is the OpenIPC project's driver of
choice for long-range video links built on top of cheap Realtek 11ac USB
radios.
driver (Jaguar family: RTL8812AU + RTL8821AU shipping, RTL8811AU
supported via the 8812 code path, RTL8814AU RX solid + TX validated on
fresh-chip runs), speaking to the chip directly through libusb. No
kernel module, no `rtl8812au` DKMS tree — just a C++20 static library
(`WiFiDriver`) plus two demo executables for RX and TX. It is the
OpenIPC project's driver of choice for long-range video links built on
top of cheap Realtek 11ac USB radios.

## Hardware landscape

Devourer targets **RTL8812AU**, **RTL8811AU**, and **RTL8814AU** — all
members of Realtek's first-generation 802.11ac silicon family, internally
codenamed **"Jaguar"**. The HAL, register-table layout, firmware-download
plumbing, and `SET_TX_DESC_*_8812` macros in `src/FrameParser.h` are shared
across the family; chip-specific EEPROM handling, firmware blobs, and RF
tables are layered on top.
Devourer targets **RTL8812AU**, **RTL8811AU**, **RTL8814AU**, and
**RTL8821AU** — all members of Realtek's first-generation 802.11ac
silicon family, internally codenamed **"Jaguar"**. The HAL,
register-table layout, firmware-download plumbing, and
`SET_TX_DESC_*_8812` macros in `src/FrameParser.h` are shared across the
family; chip-specific EEPROM handling, firmware blobs, and RF tables are
layered on top.

| Part | RF / streams | Status | Notes |
| -------------- | --------------- | ------------- | ------------------------------------------- |
| **RTL8812AU** | 2T2R | Supported | VID/PID `0bda:8812`; reference part |
| **RTL8811AU** | 1T1R | WIP | Pin/feature-reduced cut of 8812AU |
| **RTL8814AU** | 4T4R, 3-SS max | RX supported | VID/PID `0bda:8813`; 2-SS effective on USB-2; TX path not yet validated |
| **RTL8821AU** | 1T1R + BT | Not supported | Same Jaguar PHY combined with Bluetooth |
| **RTL8811AU** | 1T1R | Supported | 1T1R cut of 8812 silicon; rides 8812 code path with `RFType=RF_TYPE_1T1R` selected from `REG_SYS_CFG` bit 27 |
| **RTL8814AU** | 4T4R, 3-SS max | RX + flaky TX | VID/PID `0bda:8813`; 2-SS effective on USB-2; TX works on fresh-chip single-cell runs but degrades to `LIBUSB_ERROR_IO` after virsh USB passthrough cycles |
| **RTL8821AU** | 1T1R AC + BT | Supported | OEM-rebadged as TP-Link Archer T2U Plus (`2357:0120`) etc; Android hotplug works end-to-end |

Successor families (`Jaguar2` / `Jaguar+` — 8812BU, 8822BU/BE, etc., and the
later `Kestrel` 11ax generation) are **out of scope**: they share the Realtek
"AU" branding but the baseband and HAL differ enough that they would need
their own driver.
Successor families (`Jaguar2` / `Jaguar+` — 8812BU, 8822BU/BE, etc., and
the later `Kestrel` 11ax generation) are **out of scope**: they share
the Realtek "AU" / "BU" branding but the baseband and HAL differ enough
that they would need their own driver. NB: RTL8821AU itself is Jaguar
wave 1 (CHIP_8821 = 7 in Realtek's HalVerDef, shares the enum with
CHIP_8812), not Jaguar2 — the naming is a known trap.

> Heads up — some Realtek USB sticks ship in "ZeroCD" mode and enumerate first
> as a USB mass-storage device exposing the Windows driver installer
Expand Down Expand Up @@ -85,8 +89,12 @@ root.

### Demo env vars

Common to both demos:

- `DEVOURER_PID=0xNNNN` — restrict the device-open loop to a single PID
(e.g. `0x8813` for RTL8814AU).
- `DEVOURER_VID=0xNNNN` — override VID (default `0x0bda`). Needed for
OEM-rebadged dongles like the TP-Link Archer T2U Plus (`2357:0120`).
- `DEVOURER_CHANNEL=N` — override the demo's monitor channel (e.g. `6`
for 2.4 GHz, `36` for 5 GHz).
- `DEVOURER_SKIP_RESET=1` — skip `libusb_reset_device` before claim. Useful
Expand All @@ -96,6 +104,20 @@ root.
channel switch. Skipped by default in 8814 monitor mode: the loop issues
~300 vendor control transfers and the resulting per-rate indices are
unused for RX-only operation.
- `DEVOURER_USB_QUIET=1` — downgrade libusb log level from DEBUG to
WARNING (DEBUG produces ~7 MB per 15 s and can fill `/tmp` mid-capture).

`WiFiDriverTxDemo`-only knobs patch the canonical beacon's radiotap
header before the TX loop:

- `DEVOURER_TX_MCS=N` — HT MCS index. Default 1.
- `DEVOURER_TX_LDPC=1` — FEC type LDPC (vs default BCC).
- `DEVOURER_TX_STBC=N` — STBC stream count (0..3). Default 0.
- `DEVOURER_TX_BW=20|40|80|160` — bandwidth.
- `DEVOURER_TX_VHT=1` — switch from HT MCS field (radiotap bit 19) to
VHT info field (bit 21). Exposes `DEVOURER_TX_VHT_MCS=N` (VHT MCS
index, 0..9 typical) and `DEVOURER_TX_VHT_NSS=N` (spatial streams).
`_LDPC` / `_STBC` / `_BW` apply to whichever (HT/VHT) mode is active.

## Using the library

Expand All @@ -121,6 +143,23 @@ on a channel followed by `send_packet(buffer, len)` where the buffer begins
with a radiotap header (the iterator in `src/Radiotap.c` extracts
rate/MCS/VHT/STBC/LDPC/SGI/bandwidth from it).

## Testing

Out-of-band regression rig in `tests/regress.py` — runs a cross-driver
TX/RX matrix between devourer and the kernel driver across plugged-in
USB Wi-Fi adapters. Default mode is a 4-cell matrix on one ordered
pair; `--full-matrix` extends to all ordered pairs; `--encoding-matrix`
adds radiotap encoding sweeps (HT BCC/LDPC/STBC + VHT BCC/LDPC); and
`--sniffer-iface IFACE` adds a 3rd-adapter monitor capture for
verifying what actually flies on-air.

See [`tests/README.md`](tests/README.md) for setup (host + libvirt VM
modes), per-mode CLI knobs, and the regression matrix's known
limitations (notably: `aircrack-ng/88XXau` strips radiotap LDPC + STBC
on the kernel-TX path, so the `kernel`-TX rows of `--encoding-matrix`
are not authoritative for LDPC/STBC asymmetries — devourer-TX rows
ARE).

## Project layout

```
Expand Down
Loading