From b458b919a9ddaf1457b226d791f2d477791d9cc6 Mon Sep 17 00:00:00 2001 From: Ryan Lahfa Date: Fri, 14 Nov 2025 20:10:47 +0100 Subject: [PATCH 1/2] tests/run-in-vm: init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a test harness for virtualizing Bureautix installation and runtime. The quick start guide is as follows: - Setup netboot. - Run the provided command in `run-in-vm.sh` - If everything goes well, you should be booted in the Sécurix installer. From there, you can do your hacking as usual. The system comes with a persistent disk in the cwd. Signed-off-by: Ryan Lahfa --- .gitignore | 1 + tests/.gitignore | 5 + tests/run-in-vm.sh | 360 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 366 insertions(+) create mode 100644 tests/.gitignore create mode 100755 tests/run-in-vm.sh diff --git a/.gitignore b/.gitignore index 72d39f7..3fb3e9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ netboot/boot-response.json netboot/snowboot .pre-commit* +result* diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..34b858d --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2025 Ryan Lahfa +# +# SPDX-License-Identifier: MIT + +*.qcow2 diff --git a/tests/run-in-vm.sh b/tests/run-in-vm.sh new file mode 100755 index 0000000..4d3eb4f --- /dev/null +++ b/tests/run-in-vm.sh @@ -0,0 +1,360 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: 2025 Ryan Lahfa +# +# SPDX-License-Identifier: MIT + +set -euo pipefail + +# This is a Sécurix developer VM harness +# to test various changes on variants. +# It makes a fair amount of assumptions (Nix installed, qemu in the shell). +# It will produce a 30GB QCOW2 image by default and use 192.168.50.1/24 if instructed. +# +# Quick start: +# USB_PASSTHROUGH="auto-yubikey" VM_GRAPHICS=true VM_SERIAL=PC140V35 BRIDGE=vmboot ./run-in-vm.sh +# This will ask for a sudo privilege escalation if the bridge is not precreated. +# It will forward any Yubikey device detected on the host. +# +# Future work: +# - upstream this into Sécurix directly. +# - offer an option to test the USB installer with this. +# - use proper graphical acceleration. +# - use virtual-fido to emulate FIDO2 devices. +# - add TPM2 emulation. + +# If blank, no bridge is used. +BRIDGE="${BRIDGE:-}" +BRIDGE_ADDR="${BRIDGE_ADDR:-192.168.50.1/24}" +ENABLE_NAT="${ENABLE_NAT:-false}" + +VM_RAM="${VM_RAM:-4096}" +VM_CPUS="${VM_CPUS:-2}" + +VM_DISK="${VM_DISK:-./vm-disk.qcow2}" +VM_DISK_SIZE="${VM_DISK_SIZE:-30G}" + +VM_GRAPHICS="${VM_GRAPHICS:-false}" +VM_SERIAL="${VM_SERIAL:-}" + +VM_EXTRA_QEMU_ARGS="${VM_EXTRA_QEMU_ARGS:-}" + +OVMF_NIX_ATTR="${OVMF_NIX_ATTR:-OVMF}" + +USB_PASSTHROUGH="${USB_PASSTHROUGH:-}" + + +_die() { + echo "Error: $*" >&2 + exit 1 +} + +require() { + command -v "$1" >/dev/null 2>&1 \ + || _die "Required binary '$1' not found in PATH" +} + +SUDO="${SUDO:-}" + +need_root() { + # If SUDO is already set, we don't need to ask again + if [[ -n "$SUDO" ]]; then + return + fi + + echo "[!] Root privileges are required to set up networking." + echo " We need this to:" + echo " - Create bridge: $BRIDGE" + echo " - Assign IP: $BRIDGE_ADDR" + echo " - Create TAP interface for the VM" + echo + + read -p "Proceed with sudo? [Y/n] " ans + case "${ans,,}" in + n|no) + echo "Aborting due to user choice." + exit 1 + ;; + *) + echo "[!] Using sudo for privileged operations." + SUDO=sudo + ;; + esac +} + +detect_yubikeys() { + mapfile -t yk < <( + for dev in /sys/bus/usb/devices/*; do + [[ -f "$dev/idVendor" && -f "$dev/idProduct" ]] || continue + vid=$(cat "$dev/idVendor") + pid=$(cat "$dev/idProduct") + + case "$vid:$pid" in + # Common YubiKey IDs + 1050:0407|1050:0408|1050:0120|1050:0116|1050:0200|1050:0402|1050:407*) + echo "$vid:$pid" + ;; + esac + done + ) + printf "%s\n" "${yk[@]}" +} + +build_usb_passthrough_args() { + local entries=() + local devs=() + + case "$USB_PASSTHROUGH" in + auto-yubikey) + echo "[usb] Auto-detecting attached YubiKey devices." >&2 + mapfile -t devs < <(detect_yubikeys) + ;; + all-yubikey) + echo "[usb] Passing all known YubiKey vendor/product IDs." >&2 + devs=("1050:0407" "1050:0408" "1050:0120" "1050:0116" "1050:0200" "1050:0402") + ;; + "") + return 0 + ;; + *) + IFS=',' read -ra devs <<< "$USB_PASSTHROUGH" + ;; + esac + + if [[ ${#devs[@]} -eq 0 ]]; then + echo "[usb] No matching USB devices found." >&2 + return 0 + fi + + # ensure we add a USB controller once + entries+=(-device qemu-xhci) + + for d in "${devs[@]}"; do + vid="0x${d%:*}" + pid="0x${d#*:}" + echo "[usb] Adding passthrough device: $d" >&2 + entries+=(-device "usb-host,vendorid=${vid},productid=${pid}") + done + + echo "${entries[@]}" +} + +enable_nft_nat() { + [[ -z "$BRIDGE" ]] && return 0 # NAT can only be done with a bridge. + [[ "$ENABLE_NAT" != "true" ]] && return 0 + + local subnet="${BRIDGE_ADDR%/*}" + local mask="${BRIDGE_ADDR#*/}" + local tag="qemu-nat-$BRIDGE" + + echo "[nftables] Enabling NAT for $BRIDGE ($BRIDGE_ADDR)" + + $SUDO nft list table ip nat >/dev/null 2>&1 || \ + $SUDO nft add table ip nat + + $SUDO nft list chain ip nat postrouting >/dev/null 2>&1 || \ + $SUDO nft add chain ip nat postrouting '{ type nat hook postrouting priority srcnat; }' + + # Add if not present + if ! $SUDO nft list chain ip nat postrouting | grep -q "$tag"; then + echo "[nftables] Adding NAT rule" + $SUDO nft add rule ip nat postrouting \ + oif != "$BRIDGE" ip saddr "$subnet/$mask" masquerade \ + comment "$tag" + else + echo "[nftables] NAT rule already present" + fi +} + +cleanup_nft_nat() { + [[ -z "$BRIDGE" ]] && return 0 + [[ "$ENABLE_NAT" != "true" ]] && return 0 + + local tag="qemu-nat-$BRIDGE" + + echo "[nftables] Cleaning NAT rule(s) tagged: $tag" + + # If table doesn't exist, skip + if ! $SUDO nft list table ip nat >/dev/null 2>&1; then + echo "[nftables] NAT table missing, nothing to clean" + return 0 + fi + + # If chain missing, skip + if ! $SUDO nft list chain ip nat postrouting >/dev/null 2>&1; then + echo "[nftables] POSTROUTING chain missing, nothing to clean" + return 0 + fi + + # Remove any rules containing our tag + mapfile -t handles < <( + $SUDO nft -a list chain ip nat postrouting \ + | grep "$tag" \ + | awk -F'handle' '{print $2}' + ) + + if [[ ${#handles[@]} -eq 0 ]]; then + echo "[nftables] No NAT rules to delete for tag: $tag" + return 0 + fi + + for h in "${handles[@]}"; do + h="${h//[[:space:]]/}" + echo "[nftables] Removing NAT rule handle: $h" + $SUDO nft delete rule ip nat postrouting handle "$h" || true + done +} + + +ensure_bridge() { + [[ -z "$BRIDGE" ]] && return 0 + + if ! ip link show "$BRIDGE" >/dev/null 2>&1; then + need_root + echo "[vmbridge] Creating missing bridge: $BRIDGE" + $SUDO ip link add name "$BRIDGE" type bridge + $SUDO ip link set "$BRIDGE" up + else + echo "[vmbridge] Using existing bridge: $BRIDGE" + fi + + if ! ip addr show "$BRIDGE" | grep -q "$BRIDGE_ADDR"; then + echo "[vmbridge] Assigning IP $BRIDGE_ADDR" + need_root + $SUDO ip addr add "$BRIDGE_ADDR" dev "$BRIDGE" || true + fi +} + +create_tap() { + [[ -z "$BRIDGE" ]] && return 0 + + TAP="tap-bureautix" + + if ip link show "$TAP" >/dev/null 2>&1; then + echo "[vmbridge] TAP exists: $TAP" + else + need_root + echo "[vmbridge] Creating TAP: $TAP" + $SUDO ip tuntap add dev "$TAP" mode tap user "$USER" + $SUDO ip link set "$TAP" up + $SUDO ip link set "$TAP" master "$BRIDGE" + fi + + cleanup() { + echo "[vmbridge] Cleaning TAP: $TAP" + $SUDO ip link set "$TAP" down || true + $SUDO ip tuntap del dev "$TAP" mode tap || true + + cleanup_nft_nat + } + trap cleanup EXIT +} + +# Pre-flight checks +require qemu-system-x86_64 +require nix + +VM_TEMP_DIR=$(mktemp -d) + +# Resolve OVMF CODE and VARS from nix-build +OVMF_BUILD_DIR=$(nix-build '' -A "$OVMF_NIX_ATTR.fd") || { + echo "Error: Failed to build OVMF from nixpkgs attribute '$OVMF_NIX_ATTR'" >&2 + exit 1 +} + +OVMF_CODE="${OVMF_BUILD_DIR}/FV/OVMF_CODE.fd" +OVMF_TEMPLATE_VARS="${OVMF_BUILD_DIR}/FV/OVMF_VARS.fd" +OVMF_VARS="${OVMF_VARS:-}" + +ensure_bridge +enable_nft_nat +create_tap +TAP_IFACE="tap-bureautix" + +[[ -f "$OVMF_CODE" ]] || _die "OVMF code binary (UEFI firmware) not found: $OVMF_CODE" + +# Ensure writable OVMF_VARS exists +if [[ ! -e "$OVMF_VARS" || ! -w "$OVMF_VARS" ]]; then + OVMF_VARS="$VM_TEMP_DIR/uefi-vars.fd" + echo "Creating writable UEFI variables at $OVMF_VARS" + cp "$OVMF_TEMPLATE_VARS" "$OVMF_VARS" + chmod +w "$OVMF_VARS" +fi + +# Ensure persistent disk exists +if [[ ! -f "$VM_DISK" ]]; then + echo "Creating persistent disk: $VM_DISK ($VM_DISK_SIZE)" + qemu-img create -f qcow2 "$VM_DISK" "$VM_DISK_SIZE" +fi + + +# Build QEMU args +QEMU_ARGS=( + -cpu host + -accel kvm + -machine q35 + -m "$VM_RAM" + -smp "$VM_CPUS" + + # Enable UEFI boot with network option ROMs + -drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" + -drive if=pflash,format=raw,file="$OVMF_VARS" + + # Persistent disk + -drive file="$VM_DISK",if=none,id=nvme0 \ + -device nvme,drive=nvme0,serial=deadbeef +) + +USB_ARGS=($(build_usb_passthrough_args)) +QEMU_ARGS+=("${USB_ARGS[@]}") + +if [[ -n "$VM_SERIAL" ]]; then + QEMU_ARGS+=( + -smbios type=1,serial="$VM_SERIAL" + ) +fi + +# Graphics +if [[ "$VM_GRAPHICS" == "true" ]]; then + QEMU_ARGS+=() +else + QEMU_ARGS+=(-nographic -serial mon:stdio) +fi + +# UEFI PXE boot happens automatically when network device has PXE ROM +# QEMU_ARGS+=(-boot n) + +if [[ -n "$BRIDGE" ]]; then + echo "[vm] Bridge mode active: using TAP interface: $TAP_IFACE" + QEMU_ARGS+=( + -netdev tap,id=net0,ifname="$TAP_IFACE",script=no,downscript=no + -device virtio-net,netdev=net0 + ) +else + echo "[vm] No bridge configured: using user-mode networking" + QEMU_ARGS+=( + -netdev user,id=net0,hostfwd=tcp::2222-:22 + -device virtio-net,netdev=net0 + ) +fi + +# Add extra QEMU args +if [[ -n "$VM_EXTRA_QEMU_ARGS" ]]; then + QEMU_ARGS+=($VM_EXTRA_QEMU_ARGS) +fi + +echo "==================================================" +echo " Starting UEFI PXE test VM" +echo "--------------------------------------------------" +echo " Serial: ${VM_SERIAL:-}" +echo " RAM: $VM_RAM MiB" +echo " CPUs: $VM_CPUS" +echo " Disk: $VM_DISK" +echo " OVMF_CODE: $OVMF_CODE" +echo " OVMF_VARS: $OVMF_VARS" +echo " Bridge: ${BRIDGE:-}" +echo " NAT enabled: $ENABLE_NAT" +echo " SSH: localhost:2222" +echo "==================================================" + +exec qemu-system-x86_64 "${QEMU_ARGS[@]}" "$@" From 3b4e892d308c8d00b7fd5420a157d4598337de15 Mon Sep 17 00:00:00 2001 From: Ryan Lahfa Date: Wed, 3 Dec 2025 16:30:57 +0100 Subject: [PATCH 2/2] netboot: add dnsmasq for automatic DHCP This way, this doesn't relie on the ambient DHCP. It is configured to provide DHCP on the vmboot interface in pair with the test VM script. Signed-off-by: Ryan Lahfa --- netboot/Procfile | 3 ++- netboot/default.nix | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/netboot/Procfile b/netboot/Procfile index fe20130..3792658 100644 --- a/netboot/Procfile +++ b/netboot/Procfile @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: MIT -pixiecore: pixiecore api --ipxe-efi64 $(nix-build -A ipxe)/ipxe--ecm--axge.efi --dhcp-no-bind http://localhost:8000/pixiecore +dhcp: dnsmasq --no-daemon --interface=vmboot --dhcp-range=192.168.50.1,192.168.50.100,12h --port=0 --log-dhcp +pixiecore: pixiecore api --dhcp-no-bind http://localhost:8000/pixiecore http-server: caddy run nix-binary-cache: harmonia diff --git a/netboot/default.nix b/netboot/default.nix index 355a1c6..07f71da 100644 --- a/netboot/default.nix +++ b/netboot/default.nix @@ -51,6 +51,7 @@ in shell = pkgs.mkShell { packages = [ + pkgs.dnsmasq pkgs.harmonia pkgs.caddy pkgs.hivemind