tests: VM mode for kernel cells (aircrack-ng/rtl8812au on pinned kernel)#33
Merged
Conversation
Adds a libvirt-VM execution mode so the kernel-side cells of the
regression matrix can run against the aircrack-ng/rtl8812au out-of-tree
driver on a pinned kernel — sidestepping the OOT-driver-vs-modern-kernel
breakage that kept the matrix from validating chipsets like RTL8814AU
on hosts running recent kernels.
Why VM: aircrack-ng/rtl8812au lags kernel API changes by 6-12 months
(timer_*, cfg80211 callback signatures with MLO link_id, etc.). On
kernel 6.15+ the OOT driver needs hand-patching to build, and
morrownr/USB-WiFi flags that mainline rtw88 is the recommended path
from 6.14 onwards. But mainline rtw88_8814au currently fails to probe
the RTL8814AU on this lab's adapter (`failed to download firmware`,
`error -22`) — so for 8814 specifically, the OOT driver is the only
working kernel path. Pinning a VM to Ubuntu 22.04 LTS (kernel 5.15)
gives us a stable platform where aircrack-ng's driver builds and loads
cleanly without ongoing patching, regardless of how the host kernel
evolves.
Three pieces:
1. `tests/setup_vm.sh` — one-shot VM provisioner. Clones the local
`jammy-base.qcow2` cloud image, generates a cloud-init seed
(creates `dima` user with the caller's SSH key, NOPASSWD sudo,
installs build-essential / dkms / linux-headers / iw / tcpdump /
python3-scapy / aircrack-ng), `virt-install`s with a qemu-xhci USB
controller for hot-plug, then runs `make dkms_install` of
aircrack-ng/rtl8812au inside via cloud-init runcmd. ~5-10 min end
to end. `--teardown` and `--status` subcommands included.
2. `tests/regress.py` refactor: introduces a `KernelHost` abstraction
that owns every kernel-side operation (modprobe / sysfs reads / iw
/ tcpdump / scapy inject). Local mode = `subprocess.run`. VM mode =
`ssh ... sudo` wrappers + `virsh attach-device` / `detach-device`
for per-cell USB passthrough. New CLI flags `--vm-name` /
`--vm-ssh` (env: DEVOURER_VM_NAME, DEVOURER_VM_SSH). When invoked
under `sudo`, the script picks up SUDO_USER's SSH key so it can
reach the VM without root having its own key provisioned.
3. Per-cell DUT routing: each cell calls `_ensure_dut_location` for
each DUT, which (in VM mode) transitions the DUT between host and
VM via virsh as needed. State always restored to "both DUTs on
host" between cells via try/finally so a crashed cell doesn't
poison the next one. Script start also has a `release_all_known_duts`
pass to clean up any leftover-attached DUT from a previous aborted run.
Validation on trainer-arch (Arch host kernel 6.18, VM Ubuntu 22.04
kernel 5.15, 8812AU + 8814AU on USB hub):
| | TX = devourer | TX = kernel |
|---|---|---|
| RX = devourer | 0 hits / 4500 TX ✗ | 0 hits / 258 TX ✗ |
| RX = kernel | 4172 hits / 4500 TX ✓ | 229 hits / 259 TX ✓ |
- baseline ✓ (kernel-TX 8812 → kernel-RX 8814 inside VM, ~88%
delivery)
- devourer-TX validation ✓ (devourer-TX 8812 on host → kernel-RX
8814 in VM, ~93% delivery — confirms devourer's 8812AU TX really
emits valid frames at the wire level)
- the two failing cells are pre-existing devourer 8814 RX TODO,
not regressions
For comparison: the same hardware in local mode (PR #32 first run)
got 1 hit on the devourer-TX→kernel-RX cell because mainline
rtw88_8814au couldn't probe the chip. The VM with aircrack-ng gives
~4000x the signal.
A few smaller fixes folded in:
- TX-count parser surfaces "Failed to send packet" failure count
separately from the rate-limited `<devourer-tx>` print count
(previously a misleading low number when sends were failing)
- `--no-baseline-abort` flag for partial-rig diagnostics
- `wait_for_wlan_iface` timeout bumped to 20s (kernel rebinds + VM
passthrough enumeration can take 10s+)
- Kernel-TX cells `wait()` for inject_beacon to self-terminate instead
of killing the ssh wrapper — captures the final "sent N frames" line
(previously TX count showed 0 even though RX side received the frames)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 tasks
josephnef
added a commit
that referenced
this pull request
May 23, 2026
Extends tests/regress.py with a --full-matrix mode that iterates every
ordered (TX, RX) pair of plugged-in DUTs across all four driver-side
combinations (kernel-only, devourer-TX/kernel-RX, kernel-TX/devourer-RX,
devourer-only) and emits one NxN table per mode instead of one 4-cell
table for a single pair. Useful for catching cross-chipset interop
regressions in PRs that touch shared HAL code.
Usage:
sudo python3 tests/regress.py --full-matrix --channel 100 \\
--vm-name devourer-testrig --vm-ssh <user>@<VM-IP>
For N adapters, runs N*(N-1)*4 cells total — at ~30-40s per cell in VM
mode that's ~16 min for N=3, manageable. Diagonal is blanked (same
physical adapter can't simultaneously TX and RX with one driver). The
script reuses run_cell as-is; the addition is just the outer pair loop,
result dict keyed by (tx_side, rx_side, tx_vidpid, rx_vidpid), and a
new emit_full_markdown that renders four NxN tables.
Also scrubs personal identifiers from earlier docs/scripts (PR #33):
- tests/setup_vm.sh now reads VM_USER from $SUDO_USER / $USER instead
of hardcoding a specific username
- tests/README.md + regress.py docstrings switch to <user>@<VM-IP>
placeholders in example commands
Validation on a 3-adapter rig (Ubuntu 22.04 VM with aircrack-ng/88XXau,
0bda:8812 + 0bda:8813 + 2357:0120, channel 100, 10s/cell):
## Kernel-only (rig sanity)
All 6 cross-chipset cells pass — 88XXau handles all three chipsets
cleanly in the pinned-kernel VM (88-271 hits per cell).
## devourer-TX → kernel-RX
devourer-TX confirmed for 8812 (4114, 4693 hits) AND 8821 (4341 hits
reaching 8814 kernel RX). 8814 TX flaky after passthrough cycles
(chip-state degradation across cell sequencing — known sensitive).
## kernel-TX → devourer-RX
Surprise — devourer-RX 8821 caught 200 frames from kernel-TX 8814,
contradicting PR #30's "RX silent" finding. devourer-RX 8812
confirmed (100 hits from each of 8814, 8821 TX). devourer-RX 8814
confirmed broken (0 hits all directions — known TODO).
## devourer ↔ devourer
All 0 — every cell hits at least one broken side (8814 RX or 8814 TX
degraded mid-run).
Net new product signal from the full matrix:
- devourer-TX 8821 actually works (was unvalidated since PR #30 had no
peer sniffer in that session — VM mode is the peer)
- devourer-RX 8821 works under at least one TX condition — reopen PR
#30's "RX silent" conclusion
- 8814 chip state degrades through repeated host↔VM passthrough — needs
investigation, may want a chip reset between cells
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
josephnef
added a commit
that referenced
this pull request
May 23, 2026
…el) (#33) ## What this is Adds a libvirt-VM execution mode to `tests/regress.py` so the kernel-side cells of the regression matrix can run against the `aircrack-ng/rtl8812au` out-of-tree driver on a **pinned kernel**, instead of fighting the host kernel. ## Why a VM The OOT `aircrack-ng/rtl8812au` driver lags kernel API changes by 6-12 months (timer_*, cfg80211 callback signatures with MLO link_id, etc.). On kernel 6.15+ it needs hand-patching to build. morrownr's README flags that mainline `rtw88_*` is now the recommended path from kernel 6.14 onwards — but **mainline `rtw88_8814au` currently fails to probe** RTL8814AU on this lab's adapter (`failed to download firmware`, `error -22`). So for 8814 specifically, OOT aircrack-ng is the only working kernel-side path. Pinning a VM to Ubuntu 22.04 LTS (kernel 5.15) gives a stable platform where aircrack-ng's driver builds and loads cleanly. The host can upgrade freely without breaking the test rig. ## Pieces **`tests/setup_vm.sh`** — one-shot VM provisioner. Clones an Ubuntu 22.04 cloud image (`jammy-base.qcow2`), generates a cloud-init seed (creates a user with caller's SSH key, NOPASSWD sudo, installs build-essential / dkms / linux-headers / iw / tcpdump / python3-scapy / aircrack-ng), `virt-install`s with `qemu-xhci` USB controller for hot-plug, runs `make dkms_install` of `aircrack-ng/rtl8812au` inside via `runcmd`. ~5-10 min end to end. `--teardown` and `--status` subcommands included. **`tests/regress.py` refactor** — introduces a `KernelHost` abstraction owning every kernel-side operation (`modprobe`, sysfs reads, `iw`, `tcpdump`, scapy). Local mode = `subprocess.run`. VM mode = `ssh ... sudo` + `virsh attach-device`/`detach-device` for per-cell USB passthrough. New CLI flags `--vm-name` / `--vm-ssh` (env: `DEVOURER_VM_NAME`, `DEVOURER_VM_SSH`). When invoked under `sudo`, picks up `SUDO_USER`'s SSH key — root usually doesn't have keys provisioned on the VM. **Per-cell DUT routing** — each cell calls `_ensure_dut_location` for each DUT, which (in VM mode) moves the DUT between host and VM via virsh as needed. State always restored to \"both DUTs on host\" between cells via try/finally so a crashed cell doesn't poison the next one. Script start has a `release_all_known_duts` pass for leftover-attached DUTs from previous aborted runs. ## Validation on trainer-arch Arch Linux host kernel 6.18, VM Ubuntu 22.04 LTS kernel 5.15, two USB DUTs in a hub (0bda:8812 RTL8812AU + 0bda:8813 RTL8814AU): ``` ## Regression matrix — channel 100, 2026-05-23 13:22:14 - TX adapter: 0bda:8812 (RTL8812AU) - RX adapter: 0bda:8813 (RTL8814AU) - Kernel host: VM devourer-testrig via <user>@<VM-IP> - Cell duration: 10s - Pass threshold: ≥ 3 hits | | TX = devourer | TX = kernel | |---|---|---| | RX = devourer | 0 hits / 4500 TX ✗ | 0 hits / 258 TX ✗ | | RX = kernel | 4172 hits / 4500 TX ✓ | 229 hits / 259 TX ✓ | ``` - **Baseline ✓** kernel-TX 8812 → kernel-RX 8814 inside VM, **~88% delivery** - **devourer-TX validation ✓** devourer-TX 8812 on host → kernel-RX 8814 in VM, **~93% delivery** — confirms devourer's RTL8812AU TX really emits valid frames at the wire level - The two failing cells are the pre-existing devourer 8814 RX TODO, not regressions; cell 3's new \"0 hits / 258 TX\" output correctly fingers the RX side (TX side really did emit 258 frames; devourer-RX 8814 silent) For comparison: the same hardware in local mode from #32's first run got **1 hit** on the devourer-TX→kernel-RX cell because mainline `rtw88_8814au` couldn't probe the chip. The VM with aircrack-ng gives **~4000× the signal**. ## Smaller fixes folded in - TX-count parser surfaces \"Failed to send packet\" failure count separately from the rate-limited `<devourer-tx>` print count (previously misleadingly low when sends were failing) - `--no-baseline-abort` flag for partial-rig diagnostics - `wait_for_wlan_iface` timeout bumped to 20s (kernel rebinds + VM passthrough enumeration take 10s+) - Kernel-TX cells `wait()` for `inject_beacon` to self-terminate instead of killing the ssh wrapper — captures the final \"sent N frames\" line (previously TX count showed 0 even though RX side received frames) ## Usage ```bash sudo tests/setup_vm.sh # ~5-10 min, one-time sudo tests/setup_vm.sh --status sudo python3 tests/regress.py --channel 100 \ --vm-name devourer-testrig \ --vm-ssh <user>@<VM-IP> ``` See [`tests/README.md`](tests/README.md) for full options, prereqs, architecture notes. ## Known limitations (documented in README) - VM mode assumes a single libvirt host running both `virsh` (locally) and the VM. Pulling the VM onto a different host needs your own `virsh` wrapper. - Per matrix run: ~3-4 min in VM mode (USB hot-plug adds ~5s per cell transition vs ~100s for local mode). - Two-adapter scope today. >2 needs a pairing loop in `main()`. - Cell 4 (`devourer-TX → devourer-RX`) needs both DUTs devourer-claimable simultaneously — if one chipset has broken devourer RX (current RTL8814AU TODO), that cell shows 0 regardless of TX. ## Test plan - [x] VM provisioning succeeds end-to-end (`setup_vm.sh` clean run on trainer-arch) - [x] aircrack-ng/rtl8812au DKMS install works inside VM (kernel 5.15) - [x] USB hot-plug of 8814AU into VM works (mainline rtw88 couldn't probe; aircrack-ng claims cleanly) - [x] Full 4-cell matrix runs end-to-end in VM mode - [x] Baseline cell passes (rig sanity) - [x] devourer-TX → kernel-RX cell passes (cross-driver validation) - [x] Failing cells produce diagnostic output (TX count vs RX hits) - [ ] Validate on a different distro / different VM base image - [ ] Validate with a 2× same-chip DUT setup (both cells with both-devourer pass) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
josephnef
added a commit
that referenced
this pull request
May 23, 2026
Extends tests/regress.py with a --full-matrix mode that iterates every
ordered (TX, RX) pair of plugged-in DUTs across all four driver-side
combinations (kernel-only, devourer-TX/kernel-RX, kernel-TX/devourer-RX,
devourer-only) and emits one NxN table per mode instead of one 4-cell
table for a single pair. Useful for catching cross-chipset interop
regressions in PRs that touch shared HAL code.
Usage:
sudo python3 tests/regress.py --full-matrix --channel 100 \\
--vm-name devourer-testrig --vm-ssh <user>@<VM-IP>
For N adapters, runs N*(N-1)*4 cells total — at ~30-40s per cell in VM
mode that's ~16 min for N=3, manageable. Diagonal is blanked (same
physical adapter can't simultaneously TX and RX with one driver). The
script reuses run_cell as-is; the addition is just the outer pair loop,
result dict keyed by (tx_side, rx_side, tx_vidpid, rx_vidpid), and a
new emit_full_markdown that renders four NxN tables.
Also scrubs personal identifiers from earlier docs/scripts (PR #33):
- tests/setup_vm.sh now reads VM_USER from $SUDO_USER / $USER instead
of hardcoding a specific username
- tests/README.md + regress.py docstrings switch to <user>@<VM-IP>
placeholders in example commands
Validation on a 3-adapter rig (Ubuntu 22.04 VM with aircrack-ng/88XXau,
0bda:8812 + 0bda:8813 + 2357:0120, channel 100, 10s/cell):
## Kernel-only (rig sanity)
All 6 cross-chipset cells pass — 88XXau handles all three chipsets
cleanly in the pinned-kernel VM (88-271 hits per cell).
## devourer-TX → kernel-RX
devourer-TX confirmed for 8812 (4114, 4693 hits) AND 8821 (4341 hits
reaching 8814 kernel RX). 8814 TX flaky after passthrough cycles
(chip-state degradation across cell sequencing — known sensitive).
## kernel-TX → devourer-RX
Surprise — devourer-RX 8821 caught 200 frames from kernel-TX 8814,
contradicting PR #30's "RX silent" finding. devourer-RX 8812
confirmed (100 hits from each of 8814, 8821 TX). devourer-RX 8814
confirmed broken (0 hits all directions — known TODO).
## devourer ↔ devourer
All 0 — every cell hits at least one broken side (8814 RX or 8814 TX
degraded mid-run).
Net new product signal from the full matrix:
- devourer-TX 8821 actually works (was unvalidated since PR #30 had no
peer sniffer in that session — VM mode is the peer)
- devourer-RX 8821 works under at least one TX condition — reopen PR
#30's "RX silent" conclusion
- 8814 chip state degrades through repeated host↔VM passthrough — needs
investigation, may want a chip reset between cells
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
josephnef
added a commit
that referenced
this pull request
May 23, 2026
## What this is
Extends `tests/regress.py` with `--full-matrix`: iterates every ordered
(TX, RX) pair of plugged-in DUTs across all 4 driver-side combinations
(kernel-only / dvr-TX×kernel-RX / kernel-TX×dvr-RX / devourer-only) and
emits one NxN table per mode. Useful for catching cross-chipset interop
regressions in PRs that touch shared HAL code.
```bash
sudo python3 tests/regress.py --full-matrix --channel 100 \
--vm-name devourer-testrig --vm-ssh <user>@<VM-IP>
```
For N adapters → N×(N-1)×4 cells. ~16 min for N=3 in VM mode. Diagonal
blanked (same physical adapter can't simultaneously TX and RX with one
driver).
Also scrubs placeholder usernames from earlier docs/scripts
(`tests/setup_vm.sh` now reads `VM_USER` from `$SUDO_USER`/`$USER`
instead of hardcoding; READMEs + docstrings use `<user>@<VM-IP>`).
## Validation on a 3-adapter rig
Ubuntu 22.04 VM with aircrack-ng/88XXau, DUTs `0bda:8812 + 0bda:8813 +
2357:0120`, channel 100, 10s/cell:
### Kernel-only (rig sanity / cross-chipset kernel interop)
| TX \ RX | RTL8814AU | RTL8812AU | RTL8821AU |
|---|---|---|---|
| RTL8814AU | — | 99 hits ✓ | 271 hits ✓ |
| RTL8812AU | 243 hits ✓ | — | 270 hits ✓ |
| RTL8821AU | 260 hits ✓ | 88 hits ✓ | — |
**6/6 cells pass.** `88XXau` in the pinned-kernel VM gives full
cross-chipset kernel-side interop. Rig is sound.
### devourer-TX → kernel-RX (does devourer emit valid frames?)
| TX \ RX | RTL8814AU | RTL8812AU | RTL8821AU |
|---|---|---|---|
| RTL8814AU | — | 0 ✗ (2272 fail) | 0 ✗ (2322 fail) |
| RTL8812AU | **4114 ✓** | — | **4693 ✓** |
| RTL8821AU | **4341 ✓** | 0 ✗ | — |
- **devourer-TX 8812 validated** end-to-end at the wire — 4000+ hits to
each kernel RX peer
- **devourer-TX 8821 validated end-to-end** — 4341 hits reaching
kernel-RX 8814. New result: PR #30 had no peer sniffer to confirm; the
VM gives us one. The TX path that #30 wired works.
- 8814 TX flaky after the chip cycles through host↔VM passthroughs
(chip-state degradation across cell sequencing — single-cell tests in PR
#33 worked, this is a multi-cell wear issue)
### kernel-TX → devourer-RX (does devourer RX a known-good frame?)
| TX \ RX | RTL8814AU | RTL8812AU | RTL8821AU |
|---|---|---|---|
| RTL8814AU | — | **100 ✓** | **200 ✓** |
| RTL8812AU | 0 ✗ | — | 0 ✗ |
| RTL8821AU | 0 ✗ | **100 ✓** | — |
- **Surprise: devourer-RX 8821 caught 200 frames** from kernel-TX 8814 —
contradicts PR #30's "RX silent" finding. Reopen-worthy: 8821 RX may
actually be working under certain conditions and #30's session-local
conclusion was wrong.
- devourer-RX 8812 confirmed working (100 hits from each of 8814 and
8821 TX)
- devourer-RX 8814 confirmed broken — pre-existing TODO
### devourer ↔ devourer (end-to-end devourer)
All 0. Every cell hits at least one broken side (8814 RX broken or 8814
TX degraded mid-run).
## Net new product signal
1. **devourer-TX 8821 is working** — closes the gap PR #30 documented as
"wired but unvalidated"
2. **devourer-RX 8821 may actually be working** — at least one cell got
200 hits. PR #30's negative conclusion needs reopening.
3. **Chip state degrades through repeated passthrough** — devourer-TX
8814 is reliable in single-cell tests but flakes after several virsh
attach/detach cycles. Suggests we should consider a chip-reset /
power-cycle between cells in the orchestrator, or accept this as a known
limitation of the VM rig.
## What this PR doesn't touch
- The `--full-matrix` extension doesn't change any existing code path;
the single-pair mode (`--tx-pid`/`--rx-pid`) still works exactly as
before. Pure addition.
- Doesn't address the 8814 chip-state degradation (that's separate work
— possibly a `usbreset` between cells)
- Doesn't open the PR #30 reopen (that's a follow-up: rerun the 8821 RX
investigation with this rig)
## Test plan
- [x] `--full-matrix` runs end-to-end against 3 adapters in VM mode
- [x] All 4 NxN tables render correctly
- [x] Cell counts and per-pair timing match expected (~30-40s per cell)
- [x] Kernel-only baseline (6 cells) all pass
- [x] At least one cell in each non-trivial mode passes (validates
harness logic)
- [ ] Run on a different rig with different chipset combinations
- [ ] Validate on 4+ adapters (script supports it; not exercised here)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Status: WIP — chip init succeeds, RX data flow still silent
Brings up the RTL8821AU end-to-end except for RX bulk-IN data flow. Posting so others can continue the investigation from a clean checkpoint that builds against current master instead of resurrecting #22 (which deletes the 8814AU work and has a parallel dispatch enum that doesn't compose with the
ICType-based convention #23-#29 established).What this is
Adds a libvirt-VM execution mode to
tests/regress.pyso the kernel-side cells of the regression matrix can run against theaircrack-ng/rtl8812auout-of-tree driver on a pinned kernel, instead of fighting the host kernel.Why a VM
The OOT
aircrack-ng/rtl8812audriver lags kernel API changes by 6-12 months (timer_*, cfg80211 callback signatures with MLO link_id, etc.). On kernel 6.15+ it needs hand-patching to build. morrownr's README flags that mainlinertw88_*is now the recommended path from kernel 6.14 onwards — but mainlinertw88_8814aucurrently fails to probe RTL8814AU on this lab's adapter (failed to download firmware,error -22). So for 8814 specifically, OOT aircrack-ng is the only working kernel-side path.Pinning a VM to Ubuntu 22.04 LTS (kernel 5.15) gives a stable platform where aircrack-ng's driver builds and loads cleanly. The host can upgrade freely without breaking the test rig.
Pieces
tests/setup_vm.sh— one-shot VM provisioner. Clones an Ubuntu 22.04 cloud image (jammy-base.qcow2), generates a cloud-init seed (creates a user with caller's SSH key, NOPASSWD sudo, installs build-essential / dkms / linux-headers / iw / tcpdump / python3-scapy / aircrack-ng),virt-installs withqemu-xhciUSB controller for hot-plug, runsmake dkms_installofaircrack-ng/rtl8812auinside viaruncmd. ~5-10 min end to end.--teardownand--statussubcommands included.tests/regress.pyrefactor — introduces aKernelHostabstraction owning every kernel-side operation (modprobe, sysfs reads,iw,tcpdump, scapy). Local mode =subprocess.run. VM mode =ssh ... sudo+virsh attach-device/detach-devicefor per-cell USB passthrough. New CLI flags--vm-name/--vm-ssh(env:DEVOURER_VM_NAME,DEVOURER_VM_SSH). When invoked undersudo, picks upSUDO_USER's SSH key — root usually doesn't have keys provisioned on the VM.Per-cell DUT routing — each cell calls
_ensure_dut_locationfor each DUT, which (in VM mode) moves the DUT between host and VM via virsh as needed. State always restored to "both DUTs on host" between cells via try/finally so a crashed cell doesn't poison the next one. Script start has arelease_all_known_dutspass for leftover-attached DUTs from previous aborted runs.Validation
Arch Linux host kernel 6.18, VM Ubuntu 22.04 LTS kernel 5.15, two USB DUTs in a hub (0bda:8812 RTL8812AU + 0bda:8813 RTL8814AU):
For comparison: the same hardware in local mode from #32's first run got 1 hit on the devourer-TX→kernel-RX cell because mainline
rtw88_8814aucouldn't probe the chip. The VM with aircrack-ng gives ~4000× the signal.🤖 Generated with Claude Code