Files
linux-image-manager/tests/e2e/qemu/README.md
Kevin Veen-Birkenbach 2c3b29ee98 test(e2e): full virtualized build+boot+unlock QEMU harness
Opt-in via LIM_E2E_QEMU=1. Models the whole process on a virtio VM (the
Pi's USB-gadget net can't be emulated, so it models the software stack,
not the board): builds a LUKS image carrying the real lim initcpio Tor
artifacts, boots it in QEMU rootless, lets the real netconf/tor/dropbear/
encryptssh chain publish the onion, delivers the passphrase over Tor, and
asserts the boot-ok marker on the serial console. Supports a private
offline Tor network via chutney.

The pure command builders (config.qemu_argv/kernel_cmdline/ssh_argv,
qemu_binary), the env parser, and drift guards that keep build_image.sh
aligned with the harness run in the normal suite — no QEMU/root/network.

The build stage needs root; harness.py primes sudo up front, keeps the
credential warm, and reclaims work-dir ownership on every exit path so
the pytest tmp cleanup never trips on root-owned files. QEMU stderr is
captured so an early exit is debuggable, and each teardown step is
fault-isolated so none masks the real error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 19:12:35 +02:00

3.9 KiB

Virtualized end-to-end Tor unlock harness

This harness models the entire encrypted-image process in a virtual machine: it builds a LUKS image carrying the real lim Tor-in-initramfs artifacts, boots it in QEMU, lets the real netconf → tor → dropbear → encryptssh chain run in early userspace, then unlocks the root over Tor and proves the real system booted.

It complements the lightweight, always-rootless test_tor_unlock_e2e.py: that one round-trips a passphrase through Tor against a dropbear stand-in; this one runs the real dropbear, real cryptsetup, real mkinitcpio initramfs, and a real tor daemon in a booted machine.

Why a virtio machine and not an emulated Raspberry Pi

QEMU's raspi3b/raspi4b machines do not emulate the Pi's USB-gadget Ethernet — exactly the network path the Pi images use in the initramfs (g_ether/smsc95xx/lan78xx). Without early networking there is no Tor and no unlock, so emulating the literal board is pointless here. Instead we reproduce the same software stack (same HOOKS chain, same tor hook, same torrc and onion keys) on a QEMU-friendly virt/q35 machine with virtio-net. The only deltas from a real Pi image are the NIC driver and the CPU architecture.

Stages

Stage File Privilege
Build encrypted image build_image.sh root (loop, cryptsetup, chroot)
Tor network tor_net.py rootless
Boot + unlock boot_unlock.py rootless (QEMU user-mode net)
Orchestration harness.py mixed (uses sudo for the build only)
Pure command builders config.py none — unit-tested offline

Keeping the host rootless

Only build_image.sh needs root (there is no rootless dm-crypt). To keep your host unprivileged, run the whole harness inside a throwaway VM that has /dev/kvm, and drive it from there. QEMU itself, the tor client and the unlock are rootless: user-mode networking (-netdev user) needs no tap device, and a Tor onion service needs no inbound port forward (its rendezvous is outbound from both ends).

When run directly on the host, harness.py invokes the build via sudo -E and then chowns the artifacts back so rootless QEMU can read them.

Requirements

  • qemu-system-x86_64 (or -aarch64 for LIM_E2E_ARCH=aarch64)
  • arch-install-scripts (pacstrap, arch-chroot)
  • cryptsetup, mkinitcpio, tor, openssh, ncat (SOCKS5 ProxyCommand)
  • Network access to the public Tor network, or chutney for a private one
  • /dev/kvm recommended (TCG works but is slow; aarch64-on-x86 is always TCG)

Running

# Public Tor network (simplest; non-deterministic, needs internet):
LIM_E2E_QEMU=1 pytest tests/e2e/test_qemu_unlock_e2e.py -v -s

# Private, offline, deterministic Tor network via chutney:
git clone https://gitlab.torproject.org/tpo/core/chutney
CHUTNEY_PATH=$PWD/chutney LIM_E2E_QEMU=1 \
  pytest tests/e2e/test_qemu_unlock_e2e.py -v -s

Environment knobs: LIM_E2E_ARCH (x86_64 default, or aarch64), CHUTNEY_PATH (enables the private network), CHUTNEY_PATH unset → public.

Determinism note (chutney)

For a fully private loop the guest image must trust the same authorities as the host client. ChutneyNetwork.test_network_conf() distils the TestingTorNetwork/DirAuthority lines (rewriting 127.0.0.1 to the QEMU user-net host alias 10.0.2.2) into a file, which build_image.sh appends to the baked-in torrc via TOR_TEST_NETWORK_CONF. The offline onion key generation is unaffected — it never touches the network either way.

What runs in CI without any of this

The pure logic (config.py, the env parser, the chutney torrc parsing) and the drift guards that keep build_image.sh aligned with what the harness expects are covered by test_qemu_harness_unit.py, which runs in the normal pytest suite — no QEMU, root, or network.