Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/mock-upcloud/kvm-guest/src/lib.rs

//! **`DraupnirGuest` — the real KVM behind the mock's `GuestEngine` seam.**
//!
//! [`mock_upcloud::kvm`] says what a faithful guest must be and why: a BIOS box
//! with a screen and no serial console, two hours ahead, whose DNS queries are
//! answered by nobody and whose CD stays first until someone ejects it. This
//! crate is the engine that really boots it, through draupnir — the law's door
//! to a hypervisor. Nothing here starts a qemu behind draupnir's back.
//!
//! # The refusals are the point
//!
//! A `GuestEngine` that cannot honour a clause must REFUSE IT BY NAME rather
//! than quietly boot without it, because a guest that silently got a serial
//! console would make behaviour 13 untestable *while reporting that it had been
//! tested* — the always-yes defect this estate has found four times.
//!
//! So [`DraupnirGuest::boot`] does not assume draupnir did what it was asked.
//! It renders the argv, **reads it back**, and checks every clause against it
//! ([`preflight`]). A missing clause is a [`GuestOutcome::Refused`] carrying that
//! clause's name — `no-serial-console`, `seabios`, `framebuffer`, `rtc-skew`,
//! `no-inbound-udp`, `cd-first`, `machine-pc` — and no QEMU is started. The two the brief
//! names, `no-serial-console` and `seabios`, are the ones a tunnr-shaped guest
//! could never honour at all, and they are checked as NEGATIVES (`mon:stdio` and
//! `pflash` absent) because those are the spellings that would mean the machine
//! silently reverted.
//!
//! # What a verdict is made of, given there is no log
//!
//! There is no serial console, so there is nothing to grep. The evidence is:
//!
//! * **the qcow2's size on disk** — an install writes, and a guest that wrote
//!   did something;
//! * **two screendumps hashed** — different means the guest progressed, equal
//!   means it stopped;
//! * **whether the QEMU is still alive** — `-no-reboot` means a guest that
//!   powered itself off is gone.
//!
//! Each [`GuestOutcome`] below says which of those three produced it. Nothing
//! here returns the guest's output, because the guest has none.

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::{Duration, Instant};

use draupnir::qemu::{self, QemuGuest};
use draupnir::{BootSpec, Rtc};
use mock_upcloud::digest::sha256_hex;
use mock_upcloud::kvm::{GuestEngine, GuestOutcome, GuestSpec, Machine};
use serde_json::{json, Value};

/// **The longest path a Unix socket can have on Linux**: `sun_path` is 108
/// bytes and one of them is the NUL. QEMU given a longer `-qmp unix:` path
/// still starts the guest, but nothing can connect to its QMP — so every
/// screendump fails, the frames stay empty, and a working guest reads exactly
/// like a hung one. That is why a root that is too long is REFUSED, by name.
pub const UNIX_SOCKET_PATH_MAX: usize = 107;

/// The QMP socket path under `root` for one server's boot `n`.
fn qmp_path(root: &Path, server: &str, n: usize) -> PathBuf {
    root.join("servers").join(server).join(format!("boot-{n}")).join("qmp.sock")
}

/// The longest QMP socket path a root can produce: a 36-character server uuid
/// and a four-digit boot number.
pub fn longest_qmp_path(root: &Path) -> usize {
    qmp_path(root, &"x".repeat(36), 9999).as_os_str().len()
}

/// A socket path that is too long, refused by name with the limit and the
/// length — never discovered later as an empty screen.
fn refuse_long_socket(path: &Path) -> Option<String> {
    let n = path.as_os_str().len();
    (n > UNIX_SOCKET_PATH_MAX).then(|| {
        format!(
            "qmp-socket-path: {} is {n} bytes and a Unix socket path is at most \
             {UNIX_SOCKET_PATH_MAX}; QEMU would start with a QMP nobody can reach, and the \
             guest's screen would read as a hang",
            path.display()
        )
    })
}

/// `progressed`, from what the sampler saw. `None` — "unknown", never `false` —
/// when no frame was ever read and QMP failed: an unreadable screen is not a
/// frozen one.
pub fn progressed_verdict(frames: usize, qmp_failures: u64) -> Option<bool> {
    if frames == 0 && qmp_failures > 0 {
        None
    } else {
        Some(frames >= PROGRESS_FRAMES)
    }
}

/// How many never-seen-before frames it takes to call a guest "progressing".
/// A blinking cursor on a frozen screen is two, so two proves nothing.
pub const PROGRESS_FRAMES: usize = 3;

/// The clauses a faithful guest must honour, by the names a refusal uses.
pub const CLAUSES: &[&str] = &[
    "no-serial-console",
    "seabios",
    "framebuffer",
    "rtc-skew",
    "no-inbound-udp",
    "cd-first",
    "machine-pc",
];

/// A real QEMU, through draupnir, on the machine shape UpCloud hands out.
pub struct DraupnirGuest {
    /// Where each guest's workdir, screendumps and captured streams go.
    pub root: PathBuf,
    /// How long an install is given before the guest is judged on what it did.
    pub install_budget: Duration,
    /// How often the framebuffer is photographed, from the moment the guest
    /// starts. Every frame that differs from the one before it is kept, so
    /// "progressed" means at least two DISTINCT frames were seen. It is a
    /// sampling interval rather than one fixed gap because under KVM the whole
    /// install (SeaBIOS, kernel, write, power-off) took 2.2 s on t14s: a first
    /// frame at 3 s saw nothing at all.
    pub frame_gap: Duration,
    /// The `-vnc` display this engine serves its guests on, e.g. `127.0.0.1:91`.
    /// Loopback only, and never the estate's: a rig that could be reached from
    /// the network is a rig that can be reached by something real.
    pub vnc: Option<String>,
    /// **The machines behind the mock's servers**, one per server uuid. Empty
    /// for the one-shot [`DraupnirGuest::run`]; filled by the lifecycle calls
    /// the estate makes through [`GuestEngine`]. See [`DraupnirGuest::for_estate`].
    pub fleet: Mutex<Fleet>,
    /// **The chipset.** `Pc` (i440fx, the cdrom on PIIX IDE) is what UpCloud
    /// hands out (ledger L46, L49), and the default. `Q35` only when a test
    /// asks for it by name (`--machine q35`); the clause `machine-pc` then
    /// stands down, because the caller chose the other machine on purpose.
    pub machine: draupnir::MachineType,
    /// **The mTLS door** (`--mtls`): also forward the guest's eth1 `:50052` — the primary's
    /// mutual-TLS-1.3-only listener (gunnar `agent/wg-mtls` 3521a39) — on 127.0.0.1, next to
    /// 50051. OFF by default: every guest keeps exactly the argv it had unless a caller asks.
    pub mtls: bool,
    /// **Hot-plug (`--hotplug`)**: the data disks ride explicit `-blockdev`
    /// nodes behind their own `virtio-blk-pci` devices, so the estate's
    /// `storage/detach` and `storage/attach` on a RUNNING server really unplug
    /// (`device_del`, waited for the guest's ack) and re-plug (`device_add`)
    /// the card, and a resize of a detached member is QMP `block_resize` on
    /// its surviving node — the shape UpCloud has and a data-set growth needs
    /// (T21r, 2026-09-22). OFF by default: without it the disks are the
    /// implicit `-drive if=virtio` they always were, attach/detach are records
    /// and a disk under a running machine is never grown — the guest that did
    /// not ask for hot-plug keeps exactly the machine it had.
    pub hotplug: bool,
    /// **The estate's ONE utility L2** (`--utility-fabric`): every guest's eth1 on one
    /// `dgram` multicast group, with the MAC the API reports, and
    /// [`mock_upcloud::fabric::Fabric`] answering its DHCP with the API's address. As on
    /// UpCloud, the door sees each box's own utility address and the boxes dial each other
    /// directly. `None` (the default): each guest's own slirp, exactly as before.
    pub utility: Option<(std::sync::Arc<Mutex<mock_upcloud::fabric::Fabric>>, std::net::SocketAddrV4)>,
    /// **Template boots** (`--template-image`): a boot disk cloned from a
    /// public template (the front's Ubuntu) is booted as that image, with the
    /// create's keys and hostname. `None`: a template clone has no image and is
    /// refused by name at power-on, never booted as a blank disk.
    pub templates: Option<Templates>,
    /// **No way out** (`--offline`, 2026-09-26): every user-mode NIC is `restrict=on` — the
    /// guest reaches nothing but what is forwarded to it and the relays in [`Offline`]. `None`
    /// (the default): each guest's slirp NATs to the internet, as before.
    pub offline: Option<Offline>,
    /// **On-prem** (`--lan-publish`): TCP ports of ONE named server served on this
    /// machine's LAN address, so a home router that forwards 80/443/2222 to this
    /// machine reaches the front as the internet reaches it on UpCloud. Empty (the
    /// default): nothing is served beyond loopback, exactly as before.
    pub lan_publish: Vec<LanPublish>,
    /// **The host's own caps** (`--max-cores`, `--max-mem-mb`, round 5.3.1): a
    /// guest gets at most this many cores and MiB, whatever its plan says — so
    /// a busy host (or a shared box's lane) runs the estate's plans without
    /// pinning. The API still reports the PLAN, as UpCloud does: the cap is the
    /// test host's, not the provider's. `None` (the default): the plan, exactly.
    pub max_cores: Option<u32>,
    pub max_mem_mb: Option<u32>,
}

/// One `--lan-publish` forward: `bind:host_port` on this machine to `guest_port` of the
/// server whose hostname is `hostname`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LanPublish {
    pub hostname: String,
    pub bind: std::net::Ipv4Addr,
    pub host_port: u16,
    pub guest_port: u16,
}

/// Parse `HOSTNAME=BIND:HOSTPORT-GUESTPORT[,BIND:HOSTPORT-GUESTPORT…]`, refusing by name.
/// A loopback bind is refused: that is `--public-on-loopback`'s job, and a LAN
/// publish that reaches nobody is a publish that proves nothing.
pub fn parse_lan_publish(v: &str) -> Result<Vec<LanPublish>, String> {
    let (hostname, rest) = v.split_once('=').ok_or("--lan-publish wants HOSTNAME=BIND:HOSTPORT-GUESTPORT[,...]")?;
    if hostname.is_empty() {
        return Err("--lan-publish: the server's hostname is empty".into());
    }
    let mut out = Vec::new();
    for f in rest.split(',').filter(|s| !s.is_empty()) {
        let (hostpart, guest) = f.split_once('-').ok_or_else(|| format!("--lan-publish `{f}`: BIND:HOSTPORT-GUESTPORT"))?;
        let (bind, hp) = hostpart.rsplit_once(':').ok_or_else(|| format!("--lan-publish `{f}`: BIND:HOSTPORT-GUESTPORT"))?;
        let bind: std::net::Ipv4Addr = bind.parse().map_err(|_| format!("--lan-publish `{f}`: `{bind}` is not an IPv4 address"))?;
        if bind.is_loopback() {
            return Err(format!("--lan-publish `{f}`: {bind} is loopback; a LAN publish is for the router to reach"));
        }
        let host_port: u16 = hp.parse().map_err(|_| format!("--lan-publish `{f}`: `{hp}` is not a port"))?;
        let guest_port: u16 = guest.parse().map_err(|_| format!("--lan-publish `{f}`: `{guest}` is not a port"))?;
        out.push(LanPublish { hostname: hostname.to_string(), bind, host_port, guest_port });
    }
    if out.is_empty() {
        return Err("--lan-publish names no forward".into());
    }
    Ok(out)
}

/// Pure: the slirp `hostfwd` rules (`tcp:BIND:HP-:GP`) `lan` publishes for the server
/// called `hostname`; none for any other server.
pub fn lan_forwards(lan: &[LanPublish], hostname: &str) -> Vec<String> {
    lan.iter().filter(|l| l.hostname == hostname).map(|l| format!("tcp:{}:{}-:{}", l.bind, l.host_port, l.guest_port)).collect()
}

/// **What an offline guest may still reach**: the listed ports of THIS machine's loopback,
/// each at [`OFFLINE_HOST`]`:<port>` inside the guest, through a `guestfwd` whose command is
/// this program's own `--relay 127.0.0.1:<port>` (one relay per connection). The NoCloud
/// listener of a template boot is added by the engine.
///
/// MEASURED 2026-09-26 (loki, gunnar bench run 8 retry): the three laid VMs' first boot of the
/// Ubuntu template reached Canonical (185.125.188.x/189.x:443) through slirp's NAT. The owner's
/// rule: a mock run leaves this machine for nothing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Offline {
    /// The program QEMU runs per connection (`<relay> --relay 127.0.0.1:<port>`).
    pub relay: PathBuf,
    /// Loopback ports an offline guest reaches at [`OFFLINE_HOST`].
    pub ports: Vec<u16>,
}

/// **The relay an offline guest's `guestfwd` runs**, one process per connection: this
/// process's stdin to one TCP connection to `to` (loopback), and that connection to stdout,
/// until either side ends.
pub fn relay_stdio(to: &str) -> Result<(), String> {
    relay(std::io::stdin(), std::io::stdout(), to)
}

/// [`relay_stdio`] over any two ends (the test drives it with pipes).
pub fn relay<R: std::io::Read + Send + 'static, W: std::io::Write>(mut from: R, mut into: W, to: &str) -> Result<(), String> {
    let addr: std::net::SocketAddr = to.parse().map_err(|e| format!("--relay {to}: {e}"))?;
    if !addr.ip().is_loopback() {
        return Err(format!("--relay {to}: an offline guest is relayed to this machine's loopback only"));
    }
    let tcp = std::net::TcpStream::connect(addr).map_err(|e| format!("--relay {to}: {e}"))?;
    let mut up = tcp.try_clone().map_err(|e| e.to_string())?;
    std::thread::spawn(move || {
        let _ = std::io::copy(&mut from, &mut up);
        let _ = up.shutdown(std::net::Shutdown::Write);
    });
    let mut down = tcp;
    let mut buf = [0u8; 16 * 1024];
    loop {
        match std::io::Read::read(&mut down, &mut buf) {
            Ok(0) | Err(_) => break,
            Ok(n) => {
                if into.write_all(&buf[..n]).and_then(|_| into.flush()).is_err() {
                    break;
                }
            }
        }
    }
    Ok(())
}

/// The file in a server's directory its `user_data` is kept in: the utility fabric answers it
/// at `169.254.169.254:80` while it stands (`mock_upcloud::fabric::Fabric::serve_metadata`).
pub const USER_DATA_FILE: &str = "user-data";

/// A server's `user_data` into its file, 0600 (it is a pairing seed).
fn write_user_data(file: &std::path::Path, user_data: &str) -> Result<(), String> {
    use std::io::Write as _;
    use std::os::unix::fs::OpenOptionsExt as _;
    if let Some(d) = file.parent() {
        std::fs::create_dir_all(d).map_err(|e| format!("{}: {e}", d.display()))?;
    }
    let mut f = std::fs::OpenOptions::new().write(true).create(true).truncate(true).mode(0o600).open(file).map_err(|e| format!("{}: {e}", file.display()))?;
    f.write_all(user_data.as_bytes()).map_err(|e| format!("{}: {e}", file.display()))
}

/// The marker in a server's directory: it has booted a medium at least once.
pub const MEDIUM_BOOTED: &str = "medium-booted";

/// **Whether an offline boot is sealed** (`restrict=on`), pure: only a template clone that
/// boots neither a medium nor a disk a medium already wrote — the Ubuntu image itself.
pub fn template_seal(template: bool, medium_now: bool, medium_before: bool) -> bool {
    template && !medium_now && !medium_before
}

/// The address an offline guest reaches this machine's relayed ports at. Not `10.0.2.2`:
/// slirp refuses a `guestfwd` on its host alias, and `restrict=on` drops every other
/// connection to it.
pub const OFFLINE_HOST: &str = "10.0.2.100";

/// **One user-mode `-netdev` made offline**, pure: `restrict=on` when `restrict`, and on `n0`
/// a `guestfwd` per port to the relay. Any other argument is returned as it was.
///
/// `restrict` is for a TEMPLATE boot (the Ubuntu image, which phones Canonical) and not for a
/// MEDIUM boot: slirp's `restrict=on` drops EVERY datagram a guest sends, the replies to a UDP
/// hostfwd included, so the installer's WireGuard link (udp/51821) never handshakes.
/// MEASURED loki 2026-09-26 (bench run 8 retry 2): all three VMs installed and booted from
/// disk, and the installer saw "no WireGuard handshake yet" for minutes. A medium boot keeps an
/// unrestricted eth0 with the relays, and the run's socket recorder is the proof that it
/// reaches nothing away from this machine.
pub fn offline_netdev(netdev: &str, o: &Offline, extra_ports: &[u16], restrict: bool) -> String {
    if !netdev.starts_with("user,") {
        return netdev.to_string();
    }
    let mut n = if restrict { format!("{netdev},restrict=on") } else { netdev.to_string() };
    if netdev.starts_with("user,id=n0") {
        let mut ports: Vec<u16> = o.ports.iter().chain(extra_ports).copied().collect();
        ports.sort_unstable();
        ports.dedup();
        for p in ports {
            n.push_str(&format!(",guestfwd=tcp:{OFFLINE_HOST}:{p}-cmd:{} --relay 127.0.0.1:{p}", o.relay.display()));
        }
    }
    n
}

/// The template images this engine can boot, and the metadata their first
/// boot asks for.
///
/// **The one deliberate difference from UpCloud, and it is the front's first
/// boot only — never the installer's.** UpCloud's Ubuntu reads its metadata at
/// `169.254.169.254`. A user-mode NIC can forward that address only to ONE
/// chardev connection, which cloud-init's several requests cannot share. So
/// the mock names its own metadata to cloud-init as NoCloud over HTTP — the
/// SMBIOS serial `ds=nocloud;s=http://10.0.2.2:<port>/nocloud/<server>/` — and
/// serves the same facts: the hostname, and the keys `login_user` named.
pub struct Templates {
    /// Title prefix → the image a clone of that template boots.
    images: Vec<(String, PathBuf)>,
    /// Where the NoCloud listener is, on the host's loopback (`10.0.2.2` to a guest).
    port: u16,
    /// Server uuid → (hostname, keys).
    meta: std::sync::Arc<Mutex<BTreeMap<String, NoCloudMeta>>>,
}

impl Templates {
    fn image_for(&self, title: &str) -> Option<&PathBuf> {
        self.images.iter().find(|(p, _)| title.starts_with(p.as_str())).map(|(_, i)| i)
    }
}

/// **A qcow2 overlay of `backing`**, at least `size_bytes` large: the disk a
/// template clone is, without copying the template. draupnir's writer makes
/// the empty image; this adds the two header fields and the backing-format
/// extension (`qcow2`) that point it at `backing`, read-only.
pub fn create_overlay(path: &Path, size_bytes: u64, backing: &Path) -> Result<(), String> {
    use std::io::{Seek, SeekFrom, Write};
    let backing = backing.canonicalize().map_err(|e| format!("{}: {e}", backing.display()))?;
    let virt = size_bytes.max(qemu::qcow2_virtual_size(&backing).map_err(|e| e.to_string())?);
    qemu::create_qcow2(path, virt).map_err(|e| e.to_string())?;
    let name = backing.display().to_string();
    // Header extensions begin at header_length (104): the backing format, then
    // the end marker. The backing file name follows them.
    let mut ext = Vec::new();
    ext.extend_from_slice(&0xe279_2acau32.to_be_bytes());
    ext.extend_from_slice(&5u32.to_be_bytes());
    ext.extend_from_slice(b"qcow2\0\0\0");
    ext.extend_from_slice(&[0u8; 8]);
    let name_off = 104 + ext.len() as u64;
    if name_off as usize + name.len() > 65_536 {
        return Err(format!("overlay: backing path too long: {name}"));
    }
    let mut f = std::fs::OpenOptions::new().write(true).open(path).map_err(|e| e.to_string())?;
    let mut put = |off: u64, b: &[u8]| -> Result<(), String> {
        f.seek(SeekFrom::Start(off)).map_err(|e| e.to_string())?;
        f.write_all(b).map_err(|e| e.to_string())
    };
    put(104, &ext)?;
    put(name_off, name.as_bytes())?;
    put(8, &name_off.to_be_bytes())?;
    put(16, &(name.len() as u32).to_be_bytes())
}

/// What one template boot is told: what the create said about it.
#[derive(Clone, Debug)]
pub struct NoCloudMeta {
    pub hostname: String,
    pub keys: Vec<String>,
    /// The utility NIC, by the MAC the API reports.
    pub utility_mac: String,
}

/// The NoCloud files for one server.
///
/// **The utility NIC is brought up by user-data, not by `network-config`.**
/// NoCloud over HTTP is read at cloud-init's NETWORK stage, after the network
/// is already configured, so its `network-config` is never applied (measured
/// 2026-09-24: `ens4`, the utility NIC, stayed DOWN and only the first NIC got
/// DHCP). UpCloud's own datasource configures both NICs at the local stage;
/// here a netplan file for the utility MAC is written and applied instead,
/// which ends in the same machine: both NICs by DHCP.
fn nocloud_file(uuid: &str, m: &NoCloudMeta, file: &str) -> Option<String> {
    match file {
        "meta-data" => Some(format!("instance-id: {uuid}\nlocal-hostname: {}\n", m.hostname)),
        "user-data" => {
            let mut u = String::from("#cloud-config\ndisable_root: false\nssh_pwauth: false\nssh_authorized_keys:\n");
            for k in &m.keys {
                u.push_str(&format!("  - {}\n", k.trim()));
            }
            u.push_str(&format!(
                "write_files:\n  - path: /etc/netplan/60-utility.yaml\n    permissions: '0600'\n    content: |\n      network:\n        version: 2\n        ethernets:\n          utility:\n            match:\n              macaddress: \"{}\"\n            dhcp4: true\nruncmd:\n  - [netplan, apply]\n",
                m.utility_mac
            ));
            Some(u)
        }
        "vendor-data" => Some(String::new()),
        _ => None,
    }
}

fn serve_nocloud(listener: std::net::TcpListener, meta: std::sync::Arc<Mutex<BTreeMap<String, NoCloudMeta>>>) {
    use std::io::{BufRead, BufReader, Write};
    for conn in listener.incoming().flatten() {
        let meta = meta.clone();
        std::thread::spawn(move || {
            let mut line = String::new();
            let mut r = BufReader::new(&conn);
            if r.read_line(&mut line).is_err() {
                return;
            }
            // Drain the headers.
            let mut h = String::new();
            while r.read_line(&mut h).map(|n| n > 2).unwrap_or(false) {
                h.clear();
            }
            let path = line.split_whitespace().nth(1).unwrap_or("");
            let parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
            let body = match parts.as_slice() {
                ["nocloud", uuid, file] => meta
                    .lock()
                    .ok()
                    .and_then(|m| m.get(*uuid).cloned())
                    .and_then(|m| nocloud_file(uuid, &m, file)),
                _ => None,
            };
            let mut w = &conn;
            let _ = match body {
                Some(b) => write!(w, "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{b}", b.len()),
                None => write!(w, "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"),
            };
        });
    }
}

impl Default for DraupnirGuest {
    fn default() -> Self {
        DraupnirGuest {
            root: std::env::temp_dir().join("draupnir-guest"),
            // The measured install is 10 011 ms under KVM; the budget is the
            // window a guest gets to do it in, not a claim about how long it takes.
            install_budget: Duration::from_secs(90),
            frame_gap: Duration::from_millis(200),
            vnc: Some("127.0.0.1:91".to_string()),
            fleet: Mutex::new(Fleet::default()),
            machine: draupnir::MachineType::Pc,
            mtls: false,
            hotplug: false,
            utility: None,
            templates: None,
            offline: None,
            lan_publish: Vec::new(),
            max_cores: None,
            max_mem_mb: None,
        }
    }
}

/// What a guest run left behind — the only evidence there is, kept so a caller
/// can quote it instead of a log that does not exist.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Evidence {
    pub argv: Vec<String>,
    pub workdir: PathBuf,
    /// sha256 of each screendump never seen before in this run, in the order
    /// first seen. A cursor blink adds at most two, however long it blinks.
    pub frames: Vec<String>,
    /// The qcow2's size on disk before and after.
    pub disk_bytes: (u64, u64),
    /// Bytes the guest put on the host's stdout. On this machine shape it is 0,
    /// and that number IS behaviour 13.
    pub stdout_bytes: u64,
    pub outcome: GuestOutcome,
}

impl DraupnirGuest {
    /// The draupnir spec for one mock [`GuestSpec`] — the whole machine shape in
    /// one place, so the clause table has exactly one rendering.
    pub fn spec_for(&self, spec: &GuestSpec) -> BootSpec {
        let mut b = BootSpec::kvm_bootloader_disk(&spec.name, &spec.disk);
        b.mem_mb = spec.mem_mb;
        b.cores = spec.cores;
        if let Some(medium) = &spec.medium {
            b.medium = Some(medium.clone());
        }
        let mut b = b.cloud_machine_shape(self.vnc.clone());
        b.machine = self.machine;
        // The mock decides the skew and the boot order; the shape supplies the
        // rest. `cdrom_first` false with a medium in the tray is the ejected-CD
        // case only if the medium is gone, so the order is taken verbatim.
        b.rtc = Rtc::SkewedBy {
            seconds: spec.rtc_skew_seconds,
        };
        if spec.medium.is_some() && !spec.cdrom_first {
            b.boot_order = draupnir::BootOrder::Auto;
        }
        // `disk,cdrom` (the blank-disk install): the disk first, and no `-no-reboot` —
        // the installer's reboot resets the machine in place, as the cloud does.
        if spec.medium.is_some() && spec.disk_first {
            b.boot_order = draupnir::BootOrder::DiskThenMedium;
        }
        b
    }

    /// **Check the rendered argv against the clause table, before any guest runs.**
    ///
    /// Returns the offending clause's name and why, or `None` when the machine
    /// really is the machine. This reads the ARGV rather than the spec on
    /// purpose: the spec is what was asked for, the argv is what will happen.
    pub fn preflight(&self, spec: &GuestSpec, argv: &[String]) -> Option<(&'static str, String)> {
        let j = argv.join(" ");
        if !j.contains("-serial none") || j.contains("mon:stdio") {
            return Some((
                "no-serial-console",
                format!(
                    "the argv gives this guest a serial console, so a PID-1 panic would reach \
                     the host and behaviour 13 could not be observed: {j}"
                ),
            ));
        }
        if j.contains("pflash") || j.contains("OVMF") {
            return Some((
                "seabios",
                format!(
                    "the argv boots UEFI (pflash), and the BIOS path is the one that breaks \
                     on UpCloud — a proof on OVMF proves the wrong machine: {j}"
                ),
            ));
        }
        if !j.contains("-vga std") || j.contains("-nographic") {
            return Some((
                "framebuffer",
                format!("no framebuffer: the only surface a PID-1 panic appears on is gone: {j}"),
            ));
        }
        if !j.contains("-rtc base=") {
            return Some((
                "rtc-skew",
                "the guest boots at the right time, which is the one thing every yvra VM does \
                 not do — gunnar-clock exists for this skew"
                    .to_string(),
            ));
        }
        // The front (a template boot) resolves: its apt works on UpCloud. The
        // DNS-to-nowhere is the gunnar boxes' clause, and inbound UDP is
        // refused to every guest alike.
        // A guest with no user-mode NIC (the lean estate's main and twin: the utility fabric
        // alone) has nothing that could answer its DNS or carry a UDP reply: behaviour 18 holds.
        let user_nic = j.contains("user,id=");
        if (user_nic && !j.contains("dns=") && spec.template.is_none()) || !udp_forwards_are_wireguard_only(&j) {
            return Some((
                "no-inbound-udp",
                format!(
                    "the guest's UDP replies would arrive, so DNS and NTP would work and \
                     behaviour 18 could not happen: {j}"
                ),
            ));
        }
        if spec.cdrom_first && spec.medium.is_some() && !j.contains("-boot order=dc") {
            return Some((
                "cd-first",
                format!(
                    "the CD is supposed to be first while it is inserted; without it the \
                     installer loop (behaviour 17) cannot happen: {j}"
                ),
            ));
        }
        // L46: i440fx, so the cdrom is a PIIX IDE drive. On q35 it rides AHCI,
        // and a kernel that reads one may not read the other.
        if self.machine == draupnir::MachineType::Pc && !j.starts_with("-machine pc,") {
            return Some((
                "machine-pc",
                format!(
                    "the argv is not the i440fx machine UpCloud hands out (`-machine pc`, the cdrom on \
                     IDE): a medium proven here would be proven on the wrong chipset: {j}"
                ),
            ));
        }
        None
    }

    /// Boot the guest and keep everything it left behind.
    pub fn run(&self, spec: &GuestSpec) -> Evidence {
        let workdir = self.root.join(&spec.name);
        if let Some(why) = refuse_long_socket(&workdir.join("qmp.sock")) {
            return Evidence {
                argv: Vec::new(),
                workdir,
                frames: Vec::new(),
                disk_bytes: (0, 0),
                stdout_bytes: 0,
                outcome: GuestOutcome::Refused { clause: "qmp-socket-path", why },
            };
        }
        let boot = self.spec_for(spec);
        let argv = match qemu::argv(&boot, None, Some(&workdir.join("qmp.sock"))) {
            Ok(a) => a,
            Err(e) => {
                return Evidence {
                    argv: Vec::new(),
                    workdir,
                    frames: Vec::new(),
                    disk_bytes: (0, 0),
                    stdout_bytes: 0,
                    outcome: GuestOutcome::Refused {
                        clause: "spec",
                        why: e.to_string(),
                    },
                }
            }
        };
        if let Some((clause, why)) = self.preflight(spec, &argv) {
            return Evidence {
                argv,
                workdir,
                frames: Vec::new(),
                disk_bytes: (0, 0),
                stdout_bytes: 0,
                outcome: GuestOutcome::Refused { clause, why },
            };
        }

        let disk = Path::new(&spec.disk);
        let before = file_len(disk);
        let mut guest = match QemuGuest::start(&boot, &workdir) {
            Ok(g) => g,
            Err(e) => {
                return Evidence {
                    argv,
                    workdir,
                    frames: Vec::new(),
                    disk_bytes: (before, before),
                    stdout_bytes: 0,
                    // A host that cannot run the machine is REFUSED by the name of
                    // the thing it cannot do, never reported as a guest that failed.
                    outcome: GuestOutcome::Refused {
                        clause: "kvm-host",
                        why: e.to_string(),
                    },
                }
            }
        };

        let started = Instant::now();
        let mut frames: Vec<String> = Vec::new();
        let deadline = started + self.install_budget;
        let mut exited_at: Option<Duration> = None;
        // Keep at most this many distinct frames: enough to show a narration,
        // bounded so a guest animating a spinner cannot fill the disk.
        const MAX_FRAMES: usize = 64;
        while Instant::now() < deadline {
            if !guest.is_running() {
                exited_at = Some(started.elapsed());
                break;
            }
            if frames.len() < MAX_FRAMES {
                // Written to frame-{n}.ppm where n is the next distinct slot, so
                // an unchanged screen overwrites its own file and every kept
                // hash has its picture beside it.
                if let Some(h) = self.frame_hash(&guest, frames.len()) {
                    if !frames.contains(&h) {
                        frames.push(h);
                    }
                }
            }
            std::thread::sleep(self.frame_gap);
        }
        let stdout_bytes = guest.stdout_bytes();
        let after = file_len(disk);
        // THREE, not two: a frozen text screen with a blinking cursor is two
        // images, so "two frames differ" is true of a dead PID 1 too. Measured
        // on the installed system's panic screen, which alternated between
        // exactly two hashes for twenty seconds.
        let progressed = frames.len() >= PROGRESS_FRAMES;
        let wrote = after > before;

        let outcome = match (exited_at, wrote, progressed) {
            // The guest powered itself off (`-no-reboot`) and the disk grew: the
            // installer ran and the box came down. The ms is measured, not a
            // constant — there is nothing else to measure it with.
            (Some(d), true, _) => GuestOutcome::Installed {
                ms: d.as_millis() as u64,
            },
            // Still running at the budget, having written, with the CD still
            // first: it installed and the firmware went straight back to the CD.
            // `rounds: 2` means "at least twice" — nothing outside the guest can
            // count them, and inventing a number would be the lie this whole
            // half exists to avoid.
            (None, true, _) if spec.cdrom_first && spec.medium.is_some() => {
                GuestOutcome::Looped { rounds: 2 }
            }
            // Alive, wrote nothing, and the screen stopped changing. There is no
            // log to quote, so the frame IS the evidence.
            (None, false, false) => GuestOutcome::Panicked {
                frame_sha256: frames.last().cloned().unwrap_or_default(),
            },
            // Alive and still moving at the budget: it ran and did not finish in
            // the window it was given. Refused by name rather than called either
            // an install or a panic — both would be a guess.
            (None, _, true) => GuestOutcome::Refused {
                clause: "install-budget",
                why: format!(
                    "the guest was still progressing after {:?}: frames {:?} differ and the \
                     disk grew by {} bytes, which is neither an install that finished nor a \
                     PID 1 that died",
                    self.install_budget,
                    frames,
                    after.saturating_sub(before)
                ),
            },
            // Exited without writing: it came up, did nothing, and went away.
            (Some(_), false, _) => GuestOutcome::Panicked {
                frame_sha256: frames.last().cloned().unwrap_or_default(),
            },
            // Alive, wrote, and the screen stopped changing with no CD to loop
            // back to: it is doing something the screen no longer narrates, and
            // neither "installed" nor "panicked" is a fact yet.
            (None, true, false) => GuestOutcome::Refused {
                clause: "install-budget",
                why: format!(
                    "the guest wrote {} bytes and then froze the screen within {:?}: that is \
                     neither a finished install nor a dead PID 1",
                    after.saturating_sub(before),
                    self.install_budget
                ),
            },
        };
        let _ = guest.kill();
        Evidence {
            argv,
            workdir,
            frames,
            disk_bytes: (before, after),
            stdout_bytes,
            outcome,
        }
    }

    fn frame_hash(&self, guest: &QemuGuest, n: usize) -> Option<String> {
        let to = guest.workdir().join(format!("frame-{n}.ppm"));
        guest.screendump(&to).ok()?;
        let bytes = std::fs::read(&to).ok()?;
        Some(sha256_hex(&bytes))
    }
}

impl GuestEngine for DraupnirGuest {
    fn name(&self) -> &'static str {
        "draupnir"
    }

    fn boot(&self, spec: &GuestSpec) -> GuestOutcome {
        self.run(spec).outcome
    }

    fn power_on(&self, m: &Machine) -> Result<(), String> {
        self.fleet_power_on(m)
    }
    fn power_off(&self, server: &str, hard: bool) {
        self.fleet_power_off(server, hard)
    }
    fn running(&self, server: &str) -> Option<bool> {
        self.fleet_running(server)
    }
    fn eject(&self, server: &str) {
        self.fleet_eject(server)
    }
    fn console(&self, server: &str) -> Option<(String, u16)> {
        let fleet = self.fleet.lock().unwrap();
        fleet.machines.get(server)?.boots.last()?.vnc.clone()
    }
    fn load(&self, server: &str, storage: &str) {
        self.fleet_load(server, storage)
    }
    fn resize_disk(&self, storage: &str, size_gib: u64) -> Result<(), String> {
        self.fleet_resize(storage, size_gib)
    }
    fn copy_disk(&self, from: &str, to: &str) -> Result<(), String> {
        let (src, fmt) = self.disk_file(from);
        if !src.is_file() {
            // A storage the engine never made a file for holds nothing yet.
            return Ok(());
        }
        let dst = if self.medium_path(to).is_file() || (fmt == "raw" && !self.disk_path(to).is_file()) { self.medium_path(to) } else { self.disk_path(to) };
        if let Some(p) = dst.parent() {
            std::fs::create_dir_all(p).map_err(|e| format!("{}: {e}", p.display()))?;
        }
        // A reflink where the filesystem has one (instant), a copy otherwise.
        let tmp = dst.with_extension("copying");
        let st = std::process::Command::new("cp").arg("--reflink=auto").arg(&src).arg(&tmp).status().map_err(|e| format!("cp: {e}"))?;
        if !st.success() {
            let _ = std::fs::remove_file(&tmp);
            return Err(format!("cp {} → {}: {st}", src.display(), tmp.display()));
        }
        std::fs::rename(&tmp, &dst).map_err(|e| format!("{} → {}: {e}", tmp.display(), dst.display()))
    }
    fn attach(&self, server: &str, storage: &str, address: &str, size_gib: u64) -> Result<(), String> {
        self.fleet_attach(server, storage, address, size_gib)
    }
    fn detach(&self, server: &str, storage: &str, address: &str) -> Result<(), String> {
        self.fleet_detach(server, storage, address)
    }
    fn store_medium(&self, storage: &str, bytes: &[u8]) -> Result<(), String> {
        let p = self.medium_path(storage);
        std::fs::create_dir_all(p.parent().expect("media dir")).map_err(|e| e.to_string())?;
        std::fs::write(&p, bytes).map_err(|e| format!("{}: {e}", p.display()))
    }
    fn firewall(&self, server: &str, admitted: &[(&'static str, u16)]) -> Result<(), String> {
        self.fleet_firewall(server, admitted)
    }
    /// The fabric reads the file per connection: written, the user_data is answered; removed,
    /// the next connection is refused.
    fn metadata(&self, server: &str, user_data: Option<&str>) {
        let file = self.server_dir(server).join(USER_DATA_FILE);
        match user_data {
            Some(u) => {
                let _ = write_user_data(&file, u);
            }
            None => {
                let _ = std::fs::remove_file(&file);
            }
        }
    }
    fn forget_server(&self, server: &str) {
        self.fleet_forget_server(server)
    }
    fn forget_storage(&self, storage: &str) {
        let _ = std::fs::remove_file(self.disk_path(storage));
        let _ = std::fs::remove_file(self.medium_path(storage));
    }
    fn forget_all(&self) {
        self.kill_all();
        for d in ["servers", "disks", "media"] {
            let _ = std::fs::remove_dir_all(self.root.join(d));
        }
    }
    fn evidence(&self, server: &str) -> Option<Value> {
        self.fleet_evidence(server)
    }
}

// ── the fleet: a machine behind every server of the mock ────────────────────
//
// Layout under `root`, and nothing else is written anywhere:
//
//   disks/<storage-uuid>.qcow2     one per disk storage, sized from its record
//   media/<storage-uuid>.iso       the bytes an upload session received
//   servers/<server-uuid>/qemu.pid the live QEMU's pid (the leftover check)
//   servers/<server-uuid>/boot-N/  one per power-on: QMP socket, streams, frames
//
// **Lifetime.** The engine owns every QEMU it starts. `kill_all` is called when
// the mock exits (the binary traps SIGINT/SIGTERM), and each `QemuGuest` kills
// its QEMU when dropped. A mock that is SIGKILLed cannot run either, so its
// QEMUs survive it — and the NEXT start refuses by name ([`for_estate`]) until
// they are gone, rather than serving a new estate beside machines nothing
// can name.
//
// [`for_estate`]: DraupnirGuest::for_estate

/// Every server's machine.
#[derive(Default)]
pub struct Fleet {
    machines: BTreeMap<String, Running>,
    /// **Who holds the mTLS door** (`--mtls`): the server whose guest `:50052`
    /// is forwarded from host `127.0.0.1:50052` — the first one to take it, and
    /// then that one at every boot it has. MEASURED 2026-09-22 (T19r, the
    /// from-zero ladder on the mock): main was RESTARTED for its role while its
    /// old QEMU still held the door, so main's new boot fell back to an adjacent
    /// pair and the twin's restart, a moment later, took `:50052` — and the twin,
    /// dialling `10.0.2.2:50052`, reached ITSELF: same issuer name, another key,
    /// `invalid peer certificate: BadSignature` on every poll, never followed.
    mtls_door: Option<String>,
}

/// What [`wire_nics`] may do with host `:50052`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Mtls {
    /// No mTLS door on the utility NIC (the default).
    Off,
    /// This server holds the door, or nobody does yet: take host `:50052`,
    /// waiting briefly for a previous QEMU of the same server to let go of it.
    Door,
    /// Another server holds the door: never take `:50052`, an adjacent pair only.
    Other,
}

struct Running {
    guest: Option<QemuGuest>,
    /// The disk files the live QEMU has open — never grown under it, unless a
    /// hot-plug node ([`Running::nodes`]) lets `block_resize` do it.
    open_disks: Vec<String>,
    /// **The hot-plug nodes** (`hotplug` on): storage uuid → the `-blockdev`
    /// node the live QEMU holds for it and whether its `virtio-blk-pci` card
    /// is plugged right now. A node outlives its card's unplug, so a detached
    /// member is resized and re-plugged over the SAME node.
    nodes: BTreeMap<String, Node>,
    boots: Vec<Boot>,
    /// The last refusal, by name, if the last power-on did not start.
    refused: Option<String>,
    ejected: bool,
}

/// One `-blockdev` node of a running machine: `id` names the format node
/// (`f<id>` its file node, `v<id>` the card over it); `bus` is the PCIe root
/// port the card sits behind on q35, none on i440fx (the card goes on `pci.0`).
#[derive(Clone, Debug, PartialEq, Eq)]
struct Node {
    id: String,
    plugged: bool,
    bus: Option<String>,
    /// The card's PCI slot on i440fx (`pci.0`), pinned so a re-plug lands where the
    /// card was and the boot disk keeps the lowest slot; none on q35 (the root port
    /// is the slot).
    addr: Option<String>,
}

/// **The first PCI slot a hot-pluggable data card takes on i440fx** (`0x10`, then
/// one per card up to `0x1f`). Left to QEMU, the `-device` cards took LOWER slots
/// than the boot disk's implicit `-drive if=virtio`: the guest named the data
/// members vda..vdd and SeaBIOS booted the first of them — MEASURED loki
/// 2026-09-26, a 4-member appliance hung at "Booting from Hard Disk..." after its
/// install pass (the one-member twin happened to boot). Pinned high, the boot disk
/// is the first disk the firmware and the kernel see, as on UpCloud.
pub const HOTPLUG_SLOT_BASE: usize = 0x10;
/// The last slot i440fx has.
pub const HOTPLUG_SLOT_LAST: usize = 0x1f;

fn pc_slot(taken: &[String]) -> Option<String> {
    (HOTPLUG_SLOT_BASE..=HOTPLUG_SLOT_LAST).map(|s| format!("0x{s:x}")).find(|a| !taken.contains(a))
}

/// **A QEMU node name for a storage uuid.** Node names must start with a
/// letter and be under 32 bytes; a uuid is 32 hex digits, and the mock's uuids
/// vary in their first 8 and last 12 (`01000009-0000-4000-8000-0000000714a1`),
/// so those 20 are kept: `d` + 20 hex = 21 bytes, distinct for distinct uuids.
pub fn node_id(storage: &str) -> String {
    let hex: String = storage.chars().filter(|c| *c != '-').collect();
    if hex.len() <= 20 {
        format!("d{hex}")
    } else {
        format!("d{}{}", &hex[..8], &hex[hex.len() - 12..])
    }
}

/// Spare PCIe root ports rendered on a q35 hot-plug machine beyond its data
/// disks, so a volume attached later has a slot to plug into.
const SPARE_ROOT_PORTS: usize = 4;

/// **The data disks' argv, in the shape the engine runs.** Boot disk excluded
/// (draupnir renders it). Without hot-plug: the implicit `-drive if=virtio` of
/// every earlier run. With it: `-blockdev` file + format nodes and a
/// `virtio-blk-pci` card per disk (behind a root port on q35), the shape QMP
/// `device_del` / `device_add` / `block_resize` can address; `nodes` is filled
/// with what was rendered. Pure: a test renders it without a hypervisor.
pub fn data_disk_args(hotplug: bool, machine: draupnir::MachineType, disks: &[(String, PathBuf, &'static str)], nodes: &mut BTreeMap<String, Node>) -> Vec<String> {
    let mut a = Vec::new();
    nodes.clear();
    if !hotplug {
        for (_, path, fmt) in disks {
            a.push("-drive".into());
            a.push(format!("file={},if=virtio,format={fmt}", path.display()));
        }
        return a;
    }
    let q35 = machine == draupnir::MachineType::Q35;
    if q35 {
        // The rig's shape (gunnar-growbench `rig.rs`): q35's `pcie.0` refuses
        // `device_del`, a root port is what a hot-pluggable slot IS on PCIe,
        // and the port stays when its card is unplugged.
        for i in 0..disks.len() + SPARE_ROOT_PORTS {
            a.push("-device".into());
            a.push(format!("pcie-root-port,id=rp{i},chassis={},slot={}", i + 1, i + 1));
        }
    }
    for (i, (storage, path, fmt)) in disks.iter().enumerate() {
        let id = node_id(storage);
        let bus = q35.then(|| format!("rp{i}"));
        let addr = (!q35 && HOTPLUG_SLOT_BASE + i <= HOTPLUG_SLOT_LAST).then(|| format!("0x{:x}", HOTPLUG_SLOT_BASE + i));
        a.push("-blockdev".into());
        a.push(format!("driver=file,filename={},node-name=f{id}", path.display()));
        a.push("-blockdev".into());
        a.push(format!("driver={fmt},node-name={id},file=f{id}"));
        a.push("-device".into());
        a.push(format!(
            "virtio-blk-pci,drive={id},id=v{id}{}{}",
            bus.as_deref().map(|b| format!(",bus={b}")).unwrap_or_default(),
            addr.as_deref().map(|s| format!(",addr={s}")).unwrap_or_default()
        ));
        nodes.insert(storage.clone(), Node { id, plugged: true, bus, addr });
    }
    a
}

/// One power-on, and everything it left: there is no log, so this is it.
struct Boot {
    argv: Vec<String>,
    workdir: PathBuf,
    started: Instant,
    ran_ms: Option<u64>,
    frames: Vec<String>,
    /// ms after power-on at which each frame was FIRST seen. The last one is
    /// when the screen stopped telling anything new — on a PID-1 panic, the
    /// moment of the panic, which no log records.
    frame_ms: Vec<u64>,
    stdout_bytes: u64,
    disks: Vec<(String, u64, Option<u64>)>,
    /// guest port → `127.0.0.1:<host port>` on the utility NIC.
    forwards: Vec<(u16, String)>,
    /// `(proto, guest port, host address)` on the server's own public address
    /// (`--public-on-loopback`), at the real port numbers.
    public: Vec<(String, u16, String)>,
    /// The public address eth0 (`n0`) serves them on, when it does — the one a
    /// firewall change adds forwards on or takes them off.
    public_ip: Option<String>,
    /// Every firewall change applied to this boot live, in order: what was
    /// asked, and what QEMU answered. Evidence, not a log line.
    firewall: Vec<Value>,
    /// `(mac, address)` on the estate's one utility L2 (`--utility-fabric`).
    utility: Option<(String, String)>,
    /// Where this boot's VNC listens: loopback, a fresh port every boot —
    /// which is behaviour 14's premise, and now true of a real socket.
    vnc: Option<(String, u16)>,
    /// Screendumps that FAILED, and the last error. Evidence, not a log line:
    /// a sampler that cannot reach QMP must say so on the server, or its
    /// empty frames read as a guest that froze.
    qmp_failures: u64,
    qmp_error: Option<String>,
}

/// **The guest ports reachable from the host, each on the NIC it is served on**
/// — as on UpCloud, where the appliance serves git on its PUBLIC address and
/// its control plane on the UTILITY network: `--grpc-bind eth1:50051`, and the
/// forge's `:2222` on eth0. Forwarding 2222 on the utility NIC reached a port
/// nothing listens on there, and git got "kex_exchange_identification:
/// Connection reset by peer" (T15's TD8). Every forward is bound to 127.0.0.1
/// and nowhere else: the appliance's bootstrap window is open to anyone who
/// can reach 50051, and "anyone" must not include the LAN this rig sits on.
///
/// :80/:443 on EVERY public NIC (2026-09-26): the front is no longer a template
/// clone — every VM is laid on a BLANK disk and boots its medium — so "the
/// web is served by the template boot" stopped naming the front. A port
/// nothing listens on is refused by the guest, as on UpCloud.
const PUBLIC_FORWARDS: &[u16] = &[80, 443, 2222];
/// A template boot (the front) is administered over SSH, and serves the web and
/// the forge's DNAT on its public address.
const TEMPLATE_PUBLIC_FORWARDS: &[u16] = &[22, 80, 443, 2222];
const UTILITY_FORWARDS: &[u16] = &[50051];
/// eth1's own subnet (UpCloud's utility network is not the public one) and its DNS-to-nowhere.
const UTILITY_NET: &str = "10.0.3.0/24";
const UTILITY_DNS: &str = "10.0.3.99";
/// With [`DraupnirGuest::mtls`]: the utility NIC's second door.
const UTILITY_MTLS_FORWARDS: &[u16] = &[50051, 50052];
/// gunnar's mutual-TLS replication listener (`--grpc-replication-bind`, default `:50052`).
const MTLS_PORT: u16 = 50052;

/// **The two NICs UpCloud gives a server** (ledger L46/L86): eth0 public —
/// draupnir's own `n0`, its forwards pinned to loopback — and eth1 utility, a
/// second user-mode NIC with the same DNS-to-nowhere. Each guest port is
/// forwarded on the NIC it is served on ([`PUBLIC_FORWARDS`],
/// [`UTILITY_FORWARDS`]). Returns guest port → `127.0.0.1:<host port>`.
fn wire_nics(boot: &mut BootSpec, mtls: Mtls, fabric: Option<(std::net::SocketAddrV4, &str)>, template: bool) -> Vec<(u16, String)> {
    let mut forwards: Vec<(u16, String)> = Vec::new();
    let mut held = HeldPorts::default();
    // eth0, the PUBLIC NIC, is draupnir's own `n0`: its forwards go through
    // the spec, pinned to loopback (`forward_bind`).
    boot.forward_bind = Some("127.0.0.1".into());
    for guest_port in if template { TEMPLATE_PUBLIC_FORWARDS } else { PUBLIC_FORWARDS } {
        if let Some(hp) = held.port() {
            boot.port_maps.push(draupnir::PortMap::new(hp, *guest_port));
            forwards.push((*guest_port, format!("127.0.0.1:{hp}")));
        }
    }
    // **eth1 on the estate's ONE utility L2** (`--utility-fabric`): a `dgram` multicast group,
    // this server's MAC as the API reports it, and the fabric answering its DHCP with the API's
    // address. No host forwards on it: as on UpCloud, the utility network is reached from the
    // front, not from the laptop.
    if let Some((group, mac)) = fabric {
        boot.extra_qemu_args.extend([
            "-netdev".to_string(),
            format!("dgram,id=n1,remote.type=inet,remote.host={},remote.port={}", group.ip(), group.port()),
            "-device".into(),
            format!("virtio-net-pci,netdev=n1,mac={mac}"),
        ]);
        return forwards;
    }
    // **eth1 is its OWN network**, as UpCloud's utility network is a different subnet from the
    // public one. Both NICs on slirp's default 10.0.2.0/24 gave the guest the SAME address on
    // eth0 and eth1 (10.0.2.15 twice), so a reply to a connection that came in on eth1 could be
    // routed out of eth0 and lost. MEASURED 2026-09-22 (growbench TD8): the twin's TLS
    // ClientHello sat unread (Recv-Q 234) in the primary's slirp at host :50052 and the twin
    // never followed; on the OVMF rig, whose second NIC is 10.0.3.0/24, the same medium follows.
    // The DNS stays pointed at nothing, inside this subnet (slirp requires it there).
    let mut n1 = format!("user,id=n1,net={UTILITY_NET},dns={UTILITY_DNS}");
    if mtls != Mtls::Off {
        // **The mTLS door is dialled at the port the PRIMARY BINDS**, not at a port derived from
        // its control plane: `twin certify` hands the twin the replication port the primary
        // listens on inside its guest (50052), and the twin dials `<primary host>:50052`.
        // MEASURED 2026-09-22 (growbench, gunnar bf16b8e): `ConnectError(10.0.2.2:50052, Connection
        // refused)` while the forward sat one port above the control plane. So the host side is
        // 127.0.0.1:50052 itself, for the first guest that can take it — the primary, booted first
        // — and from then on for THAT server only (`Fleet::mtls_door`): a later guest (the twin,
        // which nobody dials for mTLS) gets an adjacent pair, even when the door is momentarily
        // free because its holder is between two boots.
        let fixed = mtls == Mtls::Door && mtls_door_free();
        // the pair `p`, `p + 1`: `p + 1` never one of this server's other ports
        let mut hp = None;
        for _ in 0..16 {
            match held.port() {
                Some(p) if p < u16::MAX && !held.holds(p + 1) => {
                    hp = Some(p);
                    break;
                }
                Some(_) => continue,
                None => break,
            }
        }
        for guest_port in UTILITY_MTLS_FORWARDS {
            let h = match (*guest_port, fixed, hp) {
                (MTLS_PORT, true, _) => Some(MTLS_PORT),
                (MTLS_PORT, false, Some(p)) => Some(p + 1),
                (_, _, p) => p,
            };
            if let Some(h) = h {
                n1.push_str(&format!(",hostfwd=tcp:127.0.0.1:{h}-:{guest_port}"));
                forwards.push((*guest_port, format!("127.0.0.1:{h}")));
            }
        }
    } else {
        for guest_port in UTILITY_FORWARDS {
            if let Some(hp) = held.port() {
                n1.push_str(&format!(",hostfwd=tcp:127.0.0.1:{hp}-:{guest_port}"));
                forwards.push((*guest_port, format!("127.0.0.1:{hp}")));
            }
        }
    }
    boot.extra_qemu_args.extend(["-netdev".to_string(), n1, "-device".into(), "virtio-net-pci,netdev=n1".into()]);
    forwards
}

/// **The server's own PUBLIC address, served at the installer's real ports**
/// (`--public-on-loopback`): eth0's user-mode NIC also forwards
/// [`mock_upcloud::kvm::INSTALLER_PUBLIC_PORTS`] from `public` — the loopback
/// address the API reports for this server — to the same port in the guest.
/// So the installer on this host dials `<public>:8474` (direct) or
/// `<public>:51821`/udp (its WireGuard link), and the front's relay at
/// `<front public>:51820`/udp, exactly the addresses and ports it dials on
/// UpCloud.
///
/// draupnir renders only TCP forwards, all on one bind address, so eth0 is
/// rendered here instead (`-nic none` from draupnir, this `n0` beside it),
/// carrying the same loopback TCP forwards and the same DNS-to-nowhere it
/// always had. Inbound UDP is admitted to the WireGuard ports ONLY, as the
/// estate's firewall admits it on UpCloud; replies to the guest's own DNS and
/// NTP still go nowhere (behaviour 18 is the `dns=` clause, untouched).
///
/// **Only the ports the server's firewall admits** (`admitted`, from
/// [`mock_upcloud::kvm::admitted_public_ports`]): on UpCloud a port no rule
/// opens above the catch-all drop is silence, so here it is not forwarded at
/// all. eth0 is rendered even when nothing is admitted, so a rule written later
/// can open a port on it live ([`DraupnirGuest::fleet_firewall`]).
///
/// Returns `(proto, guest port, host address)` for each public forward.
fn serve_public_on(boot: &mut BootSpec, public: &str, admitted: &[(&'static str, u16)]) -> Vec<(String, u16, String)> {
    let bind = boot.forward_bind.clone().unwrap_or_else(|| "127.0.0.1".into());
    let mut n0 = "user,id=n0".to_string();
    for (h, g) in boot.tcp_forwards() {
        n0.push_str(&format!(",hostfwd=tcp:{bind}:{h}-:{g}"));
    }
    let mut public_forwards = Vec::new();
    for (proto, port) in public_ports(admitted) {
        n0.push_str(&format!(",hostfwd={}", hostfwd_rule(proto, public, port)));
        public_forwards.push((proto.to_string(), port, format!("{public}:{port}")));
    }
    if let Some(dns) = &boot.guest_dns {
        n0.push_str(&format!(",dns={dns}"));
    }
    // The forwards now live in this n0; draupnir refuses `-nic none` with port maps left over.
    boot.ports.clear();
    boot.port_maps.clear();
    boot.net = draupnir::NetMode::None;
    boot.extra_qemu_args.splice(0..0, ["-netdev".to_string(), n0, "-device".into(), "virtio-net-pci,netdev=n0".into()]);
    public_forwards
}

/// **A server with no public address gets no public NIC** (owner 2026-10-23, the lean estate:
/// main and the twin are laid with a utility interface alone). Draupnir's own NIC stands down
/// (`-nic none`) and the host forwards [`wire_nics`] gave the public NIC go with it, so the
/// utility fabric's NIC is the guest's only one (its eth0) and nothing NATs the guest out.
/// Returns the forwards that are left: none.
fn no_public_nic(boot: &mut BootSpec, _public_forwards: Vec<(u16, String)>) -> Vec<(u16, String)> {
    boot.ports.clear();
    boot.port_maps.clear();
    boot.net = draupnir::NetMode::None;
    Vec::new()
}

/// The installer's ports in `admitted`, in [`mock_upcloud::kvm::INSTALLER_PUBLIC_PORTS`]
/// order — and never anything else, whatever a caller passes: a public forward
/// is an installer port or it is not rendered.
fn public_ports(admitted: &[(&str, u16)]) -> Vec<(&'static str, u16)> {
    mock_upcloud::kvm::INSTALLER_PUBLIC_PORTS.iter().copied().filter(|p| admitted.contains(p)).collect()
}

/// **Is a public forward already held on this host?** (stinkies mock-boot-udp-port-collision:
/// the forwards sit at the real port numbers, 51820-51822, inside Linux's ephemeral range, and
/// a process bound to the WILDCARD address — kdeconnectd on 0.0.0.0:51821/udp — makes QEMU's
/// `hostfwd` fail and the machine exit at once on every boot.) Each forward is bound for a
/// moment where QEMU will bind it; the first that cannot be is named, with how to see who
/// holds it. `None`: every forward is free.
fn public_port_taken(public: &[(String, u16, String)]) -> Option<String> {
    for (proto, port, at) in public {
        let held = match proto.as_str() {
            "udp" => std::net::UdpSocket::bind(at.as_str()).err(),
            _ => std::net::TcpListener::bind(at.as_str()).err(),
        };
        if let Some(e) = held.filter(|e| e.kind() == std::io::ErrorKind::AddrInUse) {
            let flag = if proto == "udp" { "u" } else { "t" };
            return Some(format!(
                "mock-public-port-in-use: {proto} {at} cannot be forwarded to the guest — a process on this host already holds port {port} (on this address or the wildcard: `ss -l{flag}np | grep :{port}`), and QEMU would exit at once on every boot ({e})"
            ));
        }
    }
    None
}

/// One public forward as slirp spells it, `<proto>:<public>:<port>-:<port>` —
/// the same text for `-netdev user,hostfwd=` at start and for HMP
/// `hostfwd_add` live; `hostfwd_remove` takes the part before `-`.
fn hostfwd_rule(proto: &str, public: &str, port: u16) -> String {
    format!("{proto}:{public}:{port}-:{port}")
}

/// Is every UDP forward in this argv one of the installer's WireGuard ports on
/// a public address? The only inbound UDP UpCloud's estate firewall admits.
fn udp_forwards_are_wireguard_only(j: &str) -> bool {
    j.split("hostfwd=udp:").skip(1).all(|rest| {
        let rule = rest.split([',', ' ']).next().unwrap_or("");
        // `<bind>:<host port>-:<guest port>`
        let Some((host, guest)) = rule.split_once("-:") else { return false };
        let Some((bind, hp)) = host.rsplit_once(':') else { return false };
        mock_upcloud::kvm::is_loopback_public(bind)
            && hp == guest
            && mock_upcloud::kvm::INSTALLER_PUBLIC_PORTS.iter().any(|(p, n)| *p == "udp" && n.to_string() == guest)
    })
}

/// **Whose door is it.** Off without `--mtls`; the holder's at every boot it has; and until
/// there is a holder, the first server booted WITH A MEDIUM in its tray — the appliance
/// rung installs main from the medium before anything else is booted from it, while the
/// front (laid from a template, no tray) boots first of all and must never take it.
/// MEASURED 2026-09-22 (T19r, from-zero on the mock, third run): the front's guest held
/// host `:50052`, main got an adjacent pair, and the twin dialled a machine with no gunnar.
fn mtls_role(mtls: bool, holder: Option<&str>, server: &str, from_medium: bool) -> Mtls {
    match (mtls, holder) {
        (false, _) => Mtls::Off,
        (true, None) if from_medium => Mtls::Door,
        (true, None) => Mtls::Other,
        (true, Some(h)) if h == server => Mtls::Door,
        (true, Some(_)) => Mtls::Other,
    }
}

/// Is host `:50052` free for the door's holder? A holder that is being RESTARTED
/// probes while its previous QEMU may still be letting go of the port, so the
/// answer is waited for — briefly, and said out loud when it stays no: a door
/// held by something else on this box (another mock with `--mtls`) is a run
/// whose twin can never follow, and that must be read in the transcript, not
/// discovered 600 s later.
fn mtls_door_free() -> bool {
    let t0 = Instant::now();
    loop {
        if std::net::TcpListener::bind(("127.0.0.1", MTLS_PORT)).is_ok() {
            return true;
        }
        if t0.elapsed() > Duration::from_secs(5) {
            eprintln!(
                "mock-upcloud  host 127.0.0.1:{MTLS_PORT} is held by another process (a second mock with --mtls on \
                 this box?): this guest gets an adjacent pair instead, and no twin can reach its mTLS door at :{MTLS_PORT}"
            );
            return false;
        }
        std::thread::sleep(Duration::from_millis(250));
    }
}

/// Two ADJACENT free loopback ports `(p, p + 1)`, held together while checked.
#[allow(dead_code)]
fn free_loopback_pair() -> Option<u16> {
    for _ in 0..64 {
        let a = std::net::TcpListener::bind("127.0.0.1:0").ok()?;
        let p = a.local_addr().ok()?.port();
        if p < u16::MAX && std::net::TcpListener::bind(("127.0.0.1", p + 1)).is_ok() {
            return Some(p);
        }
    }
    None
}

/// A free loopback port, by asking the kernel for one and letting it go.
fn free_loopback_port() -> Option<u16> {
    std::net::TcpListener::bind("127.0.0.1:0").ok()?.local_addr().ok().map(|a| a.port())
}

/// **Free loopback ports for ONE server, each HELD until the server's ports are all picked** —
/// so the kernel never hands the same port out twice for one QEMU (dwarf 2026-10-12: 35453 for
/// both `-:80` and `-:2222`). Dropped right before QEMU binds them; a port another process takes
/// in that gap is a [`FirstBreath::TakenPort`] and the server boots again with fresh ones.
#[derive(Default)]
struct HeldPorts(Vec<std::net::TcpListener>);

impl HeldPorts {
    fn port(&mut self) -> Option<u16> {
        let l = std::net::TcpListener::bind("127.0.0.1:0").ok()?;
        let p = l.local_addr().ok()?.port();
        self.0.push(l);
        Some(p)
    }
    /// `p` is one of the held ones (an adjacent `p + 1` must not be).
    fn holds(&self, p: u16) -> bool {
        self.0.iter().any(|l| l.local_addr().is_ok_and(|a| a.port() == p))
    }
}

/// Did this QEMU exit at once because a port it was told to bind was taken? It
/// binds its `-vnc` and `hostfwd` ports while starting, so a lost port shows
/// within the first second, as `Address already in use` in its stderr.
fn first_breath(guest: &mut QemuGuest, workdir: &std::path::Path) -> FirstBreath {
    let t0 = Instant::now();
    while t0.elapsed() < std::time::Duration::from_millis(1500) {
        if !guest.is_running() {
            let err = std::fs::read_to_string(workdir.join("stderr.log")).unwrap_or_default();
            return breath_of(&err);
        }
        std::thread::sleep(std::time::Duration::from_millis(100));
    }
    FirstBreath::Alive
}

/// How a QEMU's first second and a half went ([`first_breath`]).
#[derive(Debug, Clone, PartialEq, Eq)]
enum FirstBreath {
    /// Still running (or gone with nothing said: a guest's own power-off).
    Alive,
    /// It lost a port to another socket: boot again with fresh ports.
    TakenPort,
    /// **The HOST refused it** (round 5.3.1): QEMU said why on stderr and
    /// exited — out of memory on a busy host, a missing accelerator. The
    /// mock says this by name; it is never UpCloud's capacity.
    Died(String),
}

/// Pure: [`FirstBreath`] of a QEMU that exited at once, from its stderr.
fn breath_of(stderr: &str) -> FirstBreath {
    // QEMU says a lost host port in either words: the socket's own, or only that the rule
    // could not be set up (a port named twice, or taken between the pick and its bind).
    if stderr.contains("Address already in use") || stderr.contains("Could not set up host forwarding rule") {
        return FirstBreath::TakenPort;
    }
    let said: Vec<&str> = stderr.lines().map(str::trim).filter(|l| !l.is_empty()).collect();
    if said.is_empty() {
        return FirstBreath::Alive;
    }
    FirstBreath::Died(said[said.len().saturating_sub(3)..].join(" | "))
}

/// Pure: **a plan's cores and MiB under the host's caps** — never above the
/// plan, never below one core.
fn capped(cores: u32, mem_mb: u32, max_cores: Option<u32>, max_mem_mb: Option<u32>) -> (u32, u32) {
    (max_cores.map_or(cores, |c| cores.min(c.max(1))), max_mem_mb.map_or(mem_mb, |m| mem_mb.min(m)))
}

/// At most this many distinct frames per boot: enough to show the narration,
/// bounded so a spinner cannot fill the disk.
const MAX_FRAMES: usize = 64;

impl DraupnirGuest {
    /// **The engine for a whole mock estate**, rooted at `root`.
    ///
    /// REFUSES to start, naming each one, if a QEMU from a previous run is
    /// still alive under this root: a new estate beside machines that nothing
    /// in it can name would be a leak the API could never report. Dead
    /// leftovers (disks, media, workdirs of a run that did not exit cleanly)
    /// cannot be referred to by the fresh estate either, so they are removed —
    /// and returned, so the caller can print what was reclaimed.
    pub fn for_estate(root: PathBuf) -> Result<(DraupnirGuest, Vec<String>), String> {
        let longest = longest_qmp_path(&root);
        if longest > UNIX_SOCKET_PATH_MAX {
            let room = UNIX_SOCKET_PATH_MAX - (longest - root.as_os_str().len());
            return Err(format!(
                "qmp-socket-path: root {} ({} bytes) makes QMP sockets up to {longest} bytes \
                 (<root>/servers/<36-char uuid>/boot-9999/qmp.sock), and a Unix socket path is at \
                 most {UNIX_SOCKET_PATH_MAX}. Give a root of at most {room} bytes, e.g. \
                 /tmp/mkvm-<name>",
                root.display(),
                root.as_os_str().len()
            ));
        }
        let mut live = Vec::new();
        let mut dead = Vec::new();
        let root_s = root.display().to_string();
        if let Ok(rd) = std::fs::read_dir(root.join("servers")) {
            for ent in rd.flatten() {
                let server = ent.file_name().to_string_lossy().to_string();
                let pid = std::fs::read_to_string(ent.path().join("qemu.pid"))
                    .ok()
                    .and_then(|s| s.trim().parse::<u32>().ok());
                match pid {
                    Some(pid) if qemu_alive_under(pid, &root_s) => {
                        live.push(format!("server {server}: qemu pid {pid}"))
                    }
                    _ => dead.push(format!("servers/{server}")),
                }
            }
        }
        if !live.is_empty() {
            return Err(format!(
                "leftover guests from a previous run are still running under {root_s}: {}. \
                 Kill them (kill <pid>) and start again; this mock will not serve a new \
                 estate beside machines it cannot name",
                live.join("; ")
            ));
        }
        for d in ["disks", "media"] {
            if let Ok(rd) = std::fs::read_dir(root.join(d)) {
                for ent in rd.flatten() {
                    dead.push(format!("{d}/{}", ent.file_name().to_string_lossy()));
                }
            }
        }
        for d in ["servers", "disks", "media"] {
            let _ = std::fs::remove_dir_all(root.join(d));
        }
        std::fs::create_dir_all(&root).map_err(|e| format!("{root_s}: {e}"))?;
        Ok((
            DraupnirGuest {
                root,
                // Loopback VNC is the one-shot proof's; behind the mock the
                // console is answered by the wire fake, and a VNC per guest
                // would be a port to collide on for nothing.
                vnc: None,
                ..DraupnirGuest::default()
            },
            dead,
        ))
    }

    /// **Put every guest's utility NIC on one L2**, and serve that L2's DHCP,
    /// ARP and routing from a thread of this process. The group is derived from
    /// [`DraupnirGuest::root`], so two mocks on one machine never share it.
    pub fn with_utility_fabric(mut self) -> Result<DraupnirGuest, String> {
        // Said at once, not discovered as guests with no address 10 minutes in.
        if let Err(why) = mock_upcloud::fabric::multicast_loops_back("utility-fabric") {
            eprintln!("mock-upcloud-kvm  WARNING: --utility-fabric on a host that carries no multicast L2 — no guest will be leased an address: {why}. Run it in a network namespace that loops multicast (or add `ip route add 239.0.0.0/8 dev lo`).");
        }
        let group = mock_upcloud::fabric::group_for(&self.root.display().to_string());
        let fabric = std::sync::Arc::new(Mutex::new(mock_upcloud::fabric::Fabric::new()));
        let serving = fabric.clone();
        std::thread::Builder::new()
            .name("utility-fabric".into())
            .spawn(move || {
                if let Err(e) = mock_upcloud::fabric::Fabric::serve(serving, group) {
                    eprintln!("mock-upcloud-kvm: the utility fabric on {group} stopped: {e}");
                }
            })
            .map_err(|e| format!("utility-fabric: {e}"))?;
        self.utility = Some((fabric, group));
        Ok(self)
    }

    /// **Boot a clone of the template titled `prefix…` as `image`** (a qcow2
    /// cloud image). The first call also starts the NoCloud listener that
    /// template's first boot reads its hostname and keys from.
    pub fn with_template_image(mut self, prefix: &str, image: PathBuf) -> Result<DraupnirGuest, String> {
        if !image.is_file() {
            return Err(format!("template-image: {} is not a file", image.display()));
        }
        if self.templates.is_none() {
            let listener = std::net::TcpListener::bind("127.0.0.1:0").map_err(|e| format!("nocloud: {e}"))?;
            let port = listener.local_addr().map_err(|e| e.to_string())?.port();
            let meta = std::sync::Arc::new(Mutex::new(BTreeMap::new()));
            let serving = meta.clone();
            std::thread::Builder::new()
                .name("nocloud".into())
                .spawn(move || serve_nocloud(listener, serving))
                .map_err(|e| format!("nocloud: {e}"))?;
            self.templates = Some(Templates { images: Vec::new(), port, meta });
        }
        if let Some(t) = self.templates.as_mut() {
            t.images.push((prefix.to_string(), image));
        }
        Ok(self)
    }

    fn disk_path(&self, storage: &str) -> PathBuf {
        self.root.join("disks").join(format!("{storage}.qcow2"))
    }
    pub fn medium_path(&self, storage: &str) -> PathBuf {
        self.root.join("media").join(format!("{storage}.iso"))
    }
    /// The file behind a disk storage, and its format: the uploaded image if
    /// the storage received one, the engine's qcow2 otherwise.
    fn disk_file(&self, storage: &str) -> (PathBuf, &'static str) {
        let up = self.medium_path(storage);
        if up.is_file() {
            (up, "raw")
        } else {
            (self.disk_path(storage), "qcow2")
        }
    }
    fn server_dir(&self, server: &str) -> PathBuf {
        self.root.join("servers").join(server)
    }

    fn fleet_power_on(&self, m: &Machine) -> Result<(), String> {
        let mut fleet = self.fleet.lock().unwrap();
        let mtls = mtls_role(self.mtls, fleet.mtls_door.as_deref(), &m.server, m.medium.is_some());
        // The API assigned this NIC its utility address: the fabric offers exactly that.
        let utility = match &self.utility {
            Some((fabric, group)) => {
                let mac = mock_upcloud::fabric::parse_mac(&m.utility_mac)
                    .ok_or_else(|| format!("utility-mac: server {} has no usable utility MAC {:?}", m.server, m.utility_mac))?;
                let mut f = fabric.lock().unwrap();
                f.lease(mac, m.utility_offer.clone());
                // The FIRST boot only: a server that has booted before is answered at once.
                let first_boot = fleet.machines.get(&m.server).is_none_or(|r| r.boots.is_empty());
                if first_boot && m.dhcp_late_first_boot_ms > 0 {
                    eprintln!("mock-upcloud-kvm  utility-dhcp-late-first-boot: server {} ({}) gets no utility DHCP answer for {} s from its first ask", m.server, m.hostname, m.dhcp_late_first_boot_ms / 1000);
                    f.lease_late(mac, m.dhcp_late_first_boot_ms);
                }
                Some((*group, m.utility_mac.clone()))
            }
            None => None,
        };
        let entry = fleet.machines.entry(m.server.clone()).or_insert_with(|| Running {
            guest: None,
            open_disks: Vec::new(),
            nodes: BTreeMap::new(),
            boots: Vec::new(),
            refused: None,
            ejected: false,
        });
        let r = self.start_machine(m, entry, mtls, utility.as_ref().map(|(g, mac)| (*g, mac.as_str())));
        entry.refused = r.as_ref().err().cloned();
        let took_door = r.is_ok()
            && entry.boots.last().is_some_and(|b| b.forwards.iter().any(|(g, h)| *g == MTLS_PORT && h.ends_with(&format!(":{MTLS_PORT}"))));
        if took_door && fleet.mtls_door.is_none() {
            fleet.mtls_door = Some(m.server.clone());
        }
        r
    }

    fn start_machine(&self, m: &Machine, entry: &mut Running, mtls: Mtls, utility: Option<(std::net::SocketAddrV4, &str)>) -> Result<(), String> {
        if let Some(g) = entry.guest.as_mut() {
            if g.is_running() {
                return Err(format!("already-running: server {} has a live machine", m.server));
            }
        }
        entry.guest = None;
        let Some(boot_disk) = m.disks.first() else {
            return Err(format!("no-disk: server {} has no disk to boot", m.server));
        };
        // The disks, from the records: created at their size, grown to it if a
        // resize happened while the server was up.
        std::fs::create_dir_all(self.root.join("disks")).map_err(|e| e.to_string())?;
        for d in &m.disks {
            // A disk storage that received an UPLOAD is that image, attached raw
            // — the ladder uploads the installer medium a second time as a
            // virtio disk, because korp-installer probes virtio and a kernel
            // without ATAPI never sees the IDE cdrom. No qcow2 is made for it.
            if self.medium_path(&d.storage).is_file() {
                continue;
            }
            let p = self.disk_path(&d.storage);
            let want = d.size_gib << 30;
            if !p.exists() {
                match &d.template {
                    // A template clone IS that template: an overlay of its image.
                    Some(t) => {
                        let image = self.templates.as_ref().and_then(|x| x.image_for(t)).ok_or_else(|| {
                            format!(
                                "template-image-missing: disk {} is a clone of `{t}` and this engine has no image \
                                 for it — start mock-upcloud-kvm with --template-image '{t}=<qcow2>'",
                                d.storage
                            )
                        })?;
                        create_overlay(&p, want, image).map_err(|e| format!("disk {}: {e}", d.storage))?;
                    }
                    None => qemu::create_qcow2(&p, want).map_err(|e| format!("disk {}: {e}", d.storage))?,
                }
            } else if qemu::qcow2_virtual_size(&p).map_err(|e| e.to_string())? < want {
                qemu::grow_qcow2(&p, want).map_err(|e| format!("disk {}: {e}", d.storage))?;
            }
        }
        let medium = match &m.medium {
            None => None,
            Some(st) => {
                let p = self.medium_path(st);
                if !p.is_file() {
                    return Err(format!(
                        "medium-missing: storage {st} is in the tray but no bytes were ever uploaded to it"
                    ));
                }
                Some(p.display().to_string())
            }
        };
        let mut gs = GuestSpec::new(m.server.clone(), self.disk_file(&boot_disk.storage).0.display().to_string());
        (gs.cores, gs.mem_mb) = capped(m.cores, m.mem_mb, self.max_cores, self.max_mem_mb);
        gs.medium = medium;
        gs.cdrom_first = m.cdrom_first;
        gs.disk_first = m.disk_first;
        gs.rtc_skew_seconds = m.rtc_skew_seconds;
        gs.template = boot_disk.template.clone();
        // A template boot reads its hostname and keys from the NoCloud listener.
        if gs.template.is_some() {
            if let Some(t) = &self.templates {
                t.meta.lock().unwrap().insert(m.server.clone(), NoCloudMeta { hostname: m.hostname.clone(), keys: m.ssh_keys.clone(), utility_mac: m.utility_mac.clone() });
            }
        }
        // **A port taken between the pick and QEMU's bind is retried, not reported as a boot.**
        // Every port below is picked by binding :0 and letting go, and QEMU binds it a moment
        // later; anything on this box that connects out in that moment can be handed the same
        // port as its ephemeral source. MEASURED 2026-09-22 (growbench TD8R, the restore's
        // restart): `-vnc 127.0.0.1:40085: Failed to find an available port: Address already in
        // use`, the machine never ran, and the API said `started`. UpCloud does not lose a boot to
        // its own console port, so the mock picks fresh ports and boots again (at most 3 times).
        let mut attempt = 0u32;
        let (guest, forwards, public, vnc, disks, workdir) = loop {
            let mut boot = self.spec_for(&gs);
            // **A template boot is the front's Ubuntu**: DNS that answers (the
            // front's apt works on UpCloud), and its metadata named by SMBIOS.
            if gs.template.is_some() {
                boot.guest_dns = None;
                if let Some(t) = &self.templates {
                    boot.extra_qemu_args.extend([
                        "-smbios".to_string(),
                        format!("type=1,serial=ds=nocloud;s=http://{}:{}/nocloud/{}/", if self.offline.is_some() { OFFLINE_HOST } else { "10.0.2.2" }, t.port, m.server),
                    ]);
                }
            }
            // **The console, for real.** UpCloud's is a VNC the API reports; the
            // mock's is this guest's own, on loopback and on a port the kernel
            // hands out fresh each boot, so a caller that dials what the API
            // reports sees this machine's screen — and a stale report (14) is a
            // socket that really went away. QEMU takes a display number, so the
            // port must be at least 5900; an ephemeral port always is.
            let vnc = free_loopback_port().filter(|p| *p >= 5900).map(|p| ("127.0.0.1".to_string(), p));
            if let Some((h, p)) = &vnc {
                boot.display = draupnir::Display::Framebuffer { vnc: Some(format!("{h}:{}", p - 5900)) };
            }
            // **The second NIC.** An UpCloud server has TWO: public (eth0) and
            // utility (eth1), and the estate's media bind their control plane to
            // eth1 — a guest with one NIC is not the machine, and the medium's
            // gunnar refuses to start on it (measured: `--grpc-bind eth1:50051
            // names interface "eth1" … No such device`, then a PID-1 panic). Same
            // clause as eth0: DNS pointed at nothing, no UDP inbound. The only
            // forwards are TCP, on loopback.
            let mut forwards = wire_nics(&mut boot, mtls, utility, gs.template.is_some());
            // The lean estate (2026-10-23): a server the API gives no public address has no
            // public NIC — on the utility fabric, its one NIC is the utility network.
            let has_public = !(m.public_ip.is_empty() && utility.is_some());
            if !has_public {
                forwards = no_public_nic(&mut boot, forwards);
            }
            // The installer's ports on the address the API reports (`--public-on-loopback`).
            let public = if mock_upcloud::kvm::is_loopback_public(&m.public_ip) { serve_public_on(&mut boot, &m.public_ip, &m.public_admitted) } else { Vec::new() };
            // A forward another host process holds is refused by name — never a machine
            // that exits at once while the API says `started`.
            if let Some(why) = public_port_taken(&public) {
                return Err(why);
            }
            // **The metadata service** (no operator machine in a sale, 2026-10-08): a server
            // created with `metadata: "yes"` and a `user_data` reads it at 169.254.169.254, as on
            // UpCloud — a release image's per-order pairing seed — over the UTILITY network: its
            // DHCP routes the /32 via the net's `.1` and the fabric answers the TCP. (A slirp
            // guestfwd on eth0 cannot: slirp takes one only inside 10.0.2.0/24 — MEASURED
            // NODWARF-1 2026-10-09, every guest refused to start.)
            let file = self.server_dir(&m.server).join(USER_DATA_FILE);
            match (&m.user_data, &self.utility) {
                (Some(user_data), Some((fabric, _))) => {
                    let mac = mock_upcloud::fabric::parse_mac(&m.utility_mac).ok_or_else(|| format!("utility-mac: server {} has no usable utility MAC {:?}", m.server, m.utility_mac))?;
                    write_user_data(&file, user_data)?;
                    fabric.lock().unwrap().serve_metadata(mac, Some(file.clone()));
                }
                (Some(_), None) => return Err("metadata-needs-utility-fabric: the metadata service is answered on the utility network (--utility-fabric)".to_string()),
                (None, _) => {
                    let _ = std::fs::remove_file(&file);
                }
            }
            // **On-prem** (`--lan-publish`): this server's named ports on the LAN, in the same eth0.
            let lan = lan_forwards(&self.lan_publish, &m.hostname);
            if !lan.is_empty() && has_public {
                let Some(n0) = boot.extra_qemu_args.iter_mut().find(|a| a.starts_with("user,id=n0")) else {
                    return Err("lan-publish-needs-public-on-loopback: --lan-publish adds to the eth0 this engine renders itself (--public-on-loopback)".to_string());
                };
                for r in lan {
                    n0.push_str(&format!(",hostfwd={r}"));
                }
            }
            // **Offline** (`--offline`): no user-mode NIC NATs out; the relays are the way home.
            if let Some(o) = self.offline.as_ref().filter(|_| has_public) {
                if !boot.extra_qemu_args.iter().any(|a| a.starts_with("user,id=n0")) {
                    return Err("offline-needs-public-on-loopback: --offline restricts the eth0 this engine renders itself (--public-on-loopback); draupnir's own eth0 would still NAT out".to_string());
                }
                // Sealed (restrict=on) while the disk is still the TEMPLATE's Ubuntu: a template
                // clone that has never booted a medium. From the first medium boot on, the disk is
                // the installer's, and the guest needs its UDP replies (WireGuard).
                let marker = self.server_dir(&m.server).join(MEDIUM_BOOTED);
                if gs.medium.is_some() {
                    let _ = std::fs::create_dir_all(self.server_dir(&m.server));
                    let _ = std::fs::write(&marker, b"this server booted a medium; its disk is no longer the template's\n");
                }
                let seal = template_seal(gs.template.is_some(), gs.medium.is_some(), marker.exists());
                let nocloud: Vec<u16> = match (&gs.template, &self.templates) {
                    (Some(_), Some(t)) => vec![t.port],
                    _ => Vec::new(),
                };
                for a in boot.extra_qemu_args.iter_mut() {
                    *a = offline_netdev(a, o, &nocloud, seal);
                }
            }
            let data: Vec<(String, PathBuf, &'static str)> = m
                .disks
                .iter()
                .skip(1)
                .map(|d| {
                    let (path, fmt) = self.disk_file(&d.storage);
                    (d.storage.clone(), path, fmt)
                })
                .collect();
            boot.extra_qemu_args.extend(data_disk_args(self.hotplug, self.machine, &data, &mut entry.nodes));
            let workdir = self.server_dir(&m.server).join(format!("boot-{}", entry.boots.len()));
            if let Some(why) = refuse_long_socket(&qmp_path(&self.root, &m.server, entry.boots.len())) {
                return Err(why);
            }
            let argv = qemu::argv(&boot, None, Some(&workdir.join("qmp.sock"))).map_err(|e| format!("spec: {e}"))?;
            if let Some((clause, why)) = self.preflight(&gs, &argv) {
                return Err(format!("{clause}: {why}"));
            }
            let disks: Vec<(String, u64, Option<u64>)> = m
                .disks
                .iter()
                .map(|d| (d.storage.clone(), file_len(&self.disk_file(&d.storage).0), None))
                .collect();
            let mut guest = QemuGuest::start(&boot, &workdir).map_err(|e| format!("kvm-host: {e}"))?;
            let breath = first_breath(&mut guest, &workdir);
            if let FirstBreath::Died(why) = breath {
                return Err(format!("kvm-host: QEMU exited at once on this host: {why}"));
            }
            if attempt < 3 && breath == FirstBreath::TakenPort {
                attempt += 1;
                eprintln!("mock-upcloud  server {}: QEMU lost a port to another socket before it bound it; booting again with fresh ports ({attempt}/3)", m.server);
                continue;
            }
            break (guest, forwards, public, vnc, disks, workdir);
        };
        let _ = std::fs::write(self.server_dir(&m.server).join("qemu.pid"), guest.pid().to_string());
        entry.boots.push(Boot {
            argv: guest.argv().to_vec(),
            workdir,
            started: Instant::now(),
            ran_ms: None,
            frames: Vec::new(),
            frame_ms: Vec::new(),
            stdout_bytes: 0,
            disks,
            forwards,
            public,
            public_ip: mock_upcloud::kvm::is_loopback_public(&m.public_ip).then(|| m.public_ip.clone()),
            firewall: Vec::new(),
            utility: utility.map(|(_, mac)| (mac.to_string(), m.utility_offer.address.clone())),
            vnc,
            qmp_failures: 0,
            qmp_error: None,
        });
        entry.open_disks = m.disks.iter().map(|d| d.storage.clone()).collect();
        entry.guest = Some(guest);
        Ok(())
    }

    /// Close the books on a machine that is gone: how long it ran, what it put
    /// on stdout, and what each disk weighs now.
    fn finish(&self, r: &mut Running) {
        let Some(g) = r.guest.take() else { return };
        let stdout = g.stdout_bytes();
        drop(g);
        if let Some(b) = r.boots.last_mut() {
            b.ran_ms.get_or_insert(b.started.elapsed().as_millis() as u64);
            b.stdout_bytes = stdout;
            for d in b.disks.iter_mut() {
                d.2 = Some(file_len(&self.disk_file(&d.0).0));
            }
        }
        r.open_disks.clear();
        r.nodes.clear();
    }

    fn fleet_power_off(&self, server: &str, hard: bool) {
        let mut fleet = self.fleet.lock().unwrap();
        let Some(r) = fleet.machines.get_mut(server) else { return };
        let Some(g) = r.guest.as_mut() else { return };
        if !g.is_running() {
            self.finish(r);
            return;
        }
        if hard {
            let _ = g.kill();
            self.finish(r);
        } else {
            // The ACPI button, and return: the estate's lock is held.
            let _ = g.qmp_execute("system_powerdown", None);
        }
    }

    fn fleet_running(&self, server: &str) -> Option<bool> {
        let mut fleet = self.fleet.lock().unwrap();
        let r = fleet.machines.get_mut(server)?;
        let alive = r.guest.as_mut().is_some_and(|g| g.is_running());
        if !alive {
            self.finish(r);
        }
        Some(alive)
    }

    fn fleet_eject(&self, server: &str) {
        let mut fleet = self.fleet.lock().unwrap();
        let Some(r) = fleet.machines.get_mut(server) else { return };
        r.ejected = true;
        let Some(g) = r.guest.as_mut() else { return };
        if !g.is_running() {
            return;
        }
        // The tray is whichever block device is removable; `-cdrom` names it
        // for us, and asking beats guessing `ide2-cd0`. `qmp_execute` hands
        // back the WHOLE reply, so the list is under "return" — reading the
        // reply itself as the list made this a silent no-op on a live machine.
        if let Ok(blocks) = g.qmp_execute("query-block", None) {
            for b in blocks["return"].as_array().into_iter().flatten() {
                if b["removable"].as_bool() == Some(true) {
                    if let Some(dev) = b["device"].as_str().filter(|d| !d.is_empty()) {
                        let _ = g.qmp_execute("eject", Some(json!({"device": dev, "force": true})));
                    } else if let Some(qdev) = b["qdev"].as_str() {
                        let _ = g.qmp_execute("eject", Some(json!({"id": qdev, "force": true})));
                    }
                }
            }
        }
    }

    /// `cdrom/load` on a RUNNING machine: the medium goes into the tray now
    /// (QMP `blockdev-change-medium`). A stopped server needs nothing here —
    /// its next power-on reads the tray from the device row.
    fn fleet_load(&self, server: &str, storage: &str) {
        let mut fleet = self.fleet.lock().unwrap();
        let Some(r) = fleet.machines.get_mut(server) else { return };
        r.ejected = false;
        let Some(g) = r.guest.as_mut() else { return };
        if !g.is_running() {
            return;
        }
        let file = self.medium_path(storage).display().to_string();
        if let Ok(blocks) = g.qmp_execute("query-block", None) {
            for b in blocks["return"].as_array().into_iter().flatten() {
                if b["removable"].as_bool() == Some(true) {
                    let mut args = json!({"filename": file, "format": "raw"});
                    match b["device"].as_str().filter(|d| !d.is_empty()) {
                        Some(dev) => args["device"] = json!(dev),
                        None => args["id"] = json!(b["qdev"].as_str().unwrap_or("")),
                    }
                    let _ = g.qmp_execute("blockdev-change-medium", Some(args));
                }
            }
        }
    }

    fn fleet_resize(&self, storage: &str, size_gib: u64) -> Result<(), String> {
        let mut fleet = self.fleet.lock().unwrap();
        // **Under the running machine, on its node** (hot-plug): QEMU extends
        // the file and, if the card is plugged, raises the virtio config
        // change; unplugged (a detached member, the growth's case) the node
        // simply grows and the re-plug shows the guest the new capacity.
        let held = fleet.machines.iter_mut().find_map(|(server, r)| {
            let node = r.nodes.get(storage)?.clone();
            let live = r.guest.as_mut().is_some_and(|g| g.is_running());
            live.then(|| (server.clone(), node, r.boots.last().map(|b| b.workdir.join("qmp.sock"))))
        });
        if let Some((server, node, sock)) = held {
            let sock = sock.ok_or_else(|| format!("server {server}: a running machine with no boot record"))?;
            let want = size_gib << 30;
            let have = qmp_cmd(&sock, json!({"execute": "query-named-block-nodes"}))?
                .as_array()
                .into_iter()
                .flatten()
                .find(|n| n["node-name"] == node.id)
                .and_then(|n| n["image"]["virtual-size"].as_u64())
                .unwrap_or(0);
            if have < want {
                qmp_cmd(&sock, json!({"execute": "block_resize", "arguments": {"node-name": node.id, "size": want}}))?;
            }
            return Ok(());
        }
        if let Some((server, _)) = fleet.machines.iter().find(|(_, r)| r.open_disks.iter().any(|d| d == storage)) {
            return Err(format!(
                "disk-in-use: {storage} is open by the running machine of server {server}; \
                 it is grown at that server's next power-on, never under it"
            ));
        }
        let p = self.disk_path(storage);
        if !p.exists() {
            // Created at its recorded size at the first power-on.
            return Ok(());
        }
        let want = size_gib << 30;
        if qemu::qcow2_virtual_size(&p).map_err(|e| e.to_string())? < want {
            qemu::grow_qcow2(&p, want).map_err(|e| e.to_string())?;
        }
        Ok(())
    }

    /// **Re-plug, or plug for the first time** — `storage/attach` on a running
    /// server. A node the machine already holds (a member the growth detached)
    /// gets its card back over that node; a storage the machine never saw gets
    /// its disk created, `blockdev-add`ed and a card. Without `hotplug`, or
    /// with no live machine, the row is the record and the disk is seen at the
    /// next power-on, as before.
    fn fleet_attach(&self, server: &str, storage: &str, address: &str, size_gib: u64) -> Result<(), String> {
        if !self.hotplug {
            return Ok(());
        }
        let mut fleet = self.fleet.lock().unwrap();
        let Some(r) = fleet.machines.get_mut(server) else { return Ok(()) };
        if !r.guest.as_mut().is_some_and(|g| g.is_running()) {
            return Ok(());
        }
        let sock = r.boots.last().map(|b| b.workdir.join("qmp.sock")).ok_or("a running machine with no boot record")?;
        let q35 = self.machine == draupnir::MachineType::Q35;
        let node = match r.nodes.get(storage).cloned() {
            Some(n) if n.plugged => return Ok(()),
            Some(n) => n,
            None => {
                let (path, fmt) = self.disk_file(storage);
                if !path.exists() {
                    std::fs::create_dir_all(self.root.join("disks")).map_err(|e| e.to_string())?;
                    qemu::create_qcow2(&path, size_gib << 30).map_err(|e| format!("disk {storage}: {e}"))?;
                }
                let id = node_id(storage);
                let bus = q35.then(|| {
                    let taken: Vec<String> = r.nodes.values().filter_map(|n| n.bus.clone()).collect();
                    (0..).map(|i| format!("rp{i}")).find(|b| !taken.contains(b)).expect("an unbounded range has a free port")
                });
                qmp_cmd(&sock, json!({"execute": "blockdev-add", "arguments": {"driver": "file", "filename": path.display().to_string(), "node-name": format!("f{id}")}}))?;
                let addr = if q35 {
                    None
                } else {
                    let taken: Vec<String> = r.nodes.values().filter_map(|n| n.addr.clone()).collect();
                    Some(pc_slot(&taken).ok_or("i440fx has no free PCI slot left for another data card (0x10..0x1f)")?)
                };
                qmp_cmd(&sock, json!({"execute": "blockdev-add", "arguments": {"driver": fmt, "node-name": id, "file": format!("f{id}")}}))?;
                Node { id, plugged: false, bus, addr }
            }
        };
        qmp_device_add_held(&sock, &node.id, node.bus.as_deref(), node.addr.as_deref(), Duration::from_secs(2), 3).map_err(|e| format!("{address}: {e}"))?;
        r.nodes.insert(storage.to_string(), Node { plugged: true, ..node });
        if !r.open_disks.iter().any(|d| d == storage) {
            r.open_disks.push(storage.to_string());
        }
        Ok(())
    }

    /// **The unplug** — `storage/detach` on a running server: `device_del` and
    /// the wait for `DEVICE_DELETED`, which only a guest with PCI hot-plug ever
    /// sends (tunnr-hotplug-proof). No ack within the wait is the `Err` the
    /// estate turns into 511. The node stays, so the member can be resized and
    /// re-plugged in place. Without `hotplug`, or with no live machine or no
    /// node for the storage (the implicit shape), the record is all there is.
    fn fleet_detach(&self, server: &str, storage: &str, address: &str) -> Result<(), String> {
        if !self.hotplug {
            return Ok(());
        }
        let mut fleet = self.fleet.lock().unwrap();
        let Some(r) = fleet.machines.get_mut(server) else { return Ok(()) };
        if !r.guest.as_mut().is_some_and(|g| g.is_running()) {
            return Ok(());
        }
        let Some(node) = r.nodes.get(storage).cloned() else { return Ok(()) };
        if !node.plugged {
            return Ok(());
        }
        let sock = r.boots.last().map(|b| b.workdir.join("qmp.sock")).ok_or("a running machine with no boot record")?;
        qmp_device_del_wait(&sock, &format!("v{}", node.id), Duration::from_secs(15)).map_err(|e| format!("{address}: {e}"))?;
        r.nodes.insert(storage.to_string(), Node { plugged: false, ..node });
        Ok(())
    }

    /// **The firewall, applied to a RUNNING machine** — UpCloud applies a rule
    /// set the moment it is written, and slirp's forwards are fixed at QEMU
    /// start, so each change is made live over QMP (HMP `hostfwd_remove` for a
    /// port no longer admitted, `hostfwd_add` for a newly admitted one, on eth0's
    /// `n0`). A stopped machine needs nothing: its next power-on renders
    /// [`Machine::public_admitted`]. A machine that does not serve a public
    /// address (no `--public-on-loopback`) has nothing to change.
    ///
    /// Removals go first, so a port is never served past the write that closed
    /// it. Every step is recorded on the boot (`firewall` in the evidence); the
    /// first one QEMU refuses is the `Err`, and what was already applied stays
    /// applied and recorded.
    pub fn fleet_firewall(&self, server: &str, admitted: &[(&'static str, u16)]) -> Result<(), String> {
        let mut fleet = self.fleet.lock().unwrap();
        let Some(r) = fleet.machines.get_mut(server) else { return Ok(()) };
        let Some(g) = r.guest.as_mut() else { return Ok(()) };
        if !g.is_running() {
            return Ok(());
        }
        let Some(b) = r.boots.last_mut() else { return Ok(()) };
        let Some(public) = b.public_ip.clone() else { return Ok(()) };
        let want = public_ports(admitted);
        let have: Vec<(String, u16)> = b.public.iter().map(|(p, n, _)| (p.clone(), *n)).collect();
        let mut steps: Vec<(bool, &'static str, u16)> = Vec::new();
        for (p, n) in &have {
            if !want.iter().any(|(wp, wn)| wp == p && wn == n) {
                let proto = mock_upcloud::kvm::INSTALLER_PUBLIC_PORTS.iter().find(|(ip, _)| ip == p).map(|(ip, _)| *ip).unwrap_or("tcp");
                steps.push((false, proto, *n));
            }
        }
        for (p, n) in &want {
            if !have.iter().any(|(hp, hn)| hp == p && hn == n) {
                steps.push((true, p, *n));
            }
        }
        let t = b.started.elapsed().as_millis() as u64;
        for (add, proto, port) in steps {
            let rule = hostfwd_rule(proto, &public, port);
            let line = if add {
                format!("hostfwd_add n0 {rule}")
            } else {
                format!("hostfwd_remove n0 {}", rule.split('-').next().unwrap_or(&rule))
            };
            let answer = hmp(g, &line);
            b.firewall.push(json!({"at_ms": t, "hmp": line, "answer": answer.as_ref().map(|s| s.clone()).unwrap_or_else(|e| e.clone())}));
            let refused = match &answer {
                Err(e) => Some(e.clone()),
                // HMP answers "" on success and prints its complaint otherwise.
                Ok(s) if !s.trim().is_empty() => Some(s.trim().to_string()),
                Ok(_) => None,
            };
            if let Some(why) = refused {
                return Err(format!("firewall-live: server {server}: `{line}` → {why}"));
            }
            if add {
                b.public.push((proto.to_string(), port, format!("{public}:{port}")));
            } else {
                b.public.retain(|(p, n, _)| !(p == proto && *n == port));
            }
        }
        // Keep the record in the installer ports' order, as a boot renders it.
        let order = |p: &str, n: u16| mock_upcloud::kvm::INSTALLER_PUBLIC_PORTS.iter().position(|x| x.0 == p && x.1 == n).unwrap_or(usize::MAX);
        b.public.sort_by_key(|(p, n, _)| order(p, *n));
        Ok(())
    }

    fn fleet_forget_server(&self, server: &str) {
        let mut fleet = self.fleet.lock().unwrap();
        if let Some(mut r) = fleet.machines.remove(server) {
            if let Some(g) = r.guest.as_mut() {
                let _ = g.kill();
            }
            self.finish(&mut r);
        }
        let _ = std::fs::remove_dir_all(self.server_dir(server));
    }

    /// **No storage outlives its mock** (round 5.3.1: "old mock disks linger after
    /// `mock down`"). The estate is in memory: once the mock exits, no API can
    /// name a disk or a medium again, and the next start only reclaimed them
    /// then. So on the way out — after [`DraupnirGuest::kill_all`] — every disk
    /// and medium file under the root goes, and the count is said. The
    /// machines' boot evidence (`servers/`) stays for reading until the next
    /// start. Answers how many files and bytes were removed.
    pub fn sweep_storage_files(&self) -> (usize, u64) {
        let (mut n, mut bytes) = (0usize, 0u64);
        for d in ["disks", "media"] {
            if let Ok(rd) = std::fs::read_dir(self.root.join(d)) {
                for ent in rd.flatten() {
                    let len = ent.metadata().map(|m| m.len()).unwrap_or(0);
                    if std::fs::remove_file(ent.path()).is_ok() {
                        n += 1;
                        bytes += len;
                    }
                }
            }
        }
        (n, bytes)
    }

    /// Kill every machine. The mock calls this on its way out.
    pub fn kill_all(&self) -> usize {
        let mut fleet = self.fleet.lock().unwrap();
        let mut n = 0;
        let keys: Vec<String> = fleet.machines.keys().cloned().collect();
        for k in keys {
            let r = fleet.machines.get_mut(&k).expect("key");
            if let Some(g) = r.guest.as_mut() {
                if g.is_running() {
                    n += 1;
                }
                let _ = g.kill();
            }
            self.finish(r);
            let _ = std::fs::remove_file(self.server_dir(&k).join("qemu.pid"));
        }
        n
    }

    /// **Photograph every running machine once.** The mock binary calls this
    /// every [`frame_gap`](DraupnirGuest::frame_gap) (200 ms): the whole install
    /// is 2–3 s under KVM, so anything slower sees nothing. A frame equal to the
    /// one before it is not kept.
    pub fn sample(&self) {
        let mut fleet = self.fleet.lock().unwrap();
        for r in fleet.machines.values_mut() {
            let Some(g) = r.guest.as_mut() else { continue };
            if !g.is_running() {
                self.finish(r);
                continue;
            }
            let Some(b) = r.boots.last_mut() else { continue };
            // Past the cap, the screen is still dumped — to ONE rolling file, `frame-last.ppm`, never
            // counted as a distinct frame — so the newest screen always exists. With the old hard
            // stop, a guest that filled 64 frames in its first minutes (a console scrolling log
            // lines) showed nothing of what came after: T15's TD8 lost the twin's follower that way.
            if b.frames.len() >= MAX_FRAMES {
                let _ = g.screendump(&b.workdir.join("frame-last.ppm"));
                continue;
            }
            let to = b.workdir.join(format!("frame-{}.ppm", b.frames.len()));
            match g.screendump(&to).map_err(|e| e.to_string()).and_then(|()| std::fs::read(&to).map_err(|e| e.to_string())) {
                Ok(bytes) => {
                    let h = sha256_hex(&bytes);
                    if !b.frames.contains(&h) {
                        b.frames.push(h);
                        b.frame_ms.push(b.started.elapsed().as_millis() as u64);
                    }
                }
                // QEMU binds its QMP socket a few ms after it starts; a miss
                // in the first second is that, not a fault.
                Err(e) if b.started.elapsed() < Duration::from_secs(1) => {
                    let _ = e;
                }
                Err(e) => {
                    b.qmp_failures += 1;
                    b.qmp_error = Some(e);
                }
            }
        }
    }

    fn fleet_evidence(&self, server: &str) -> Option<Value> {
        let acked: Vec<String> = self.utility.as_ref().map(|(f, _)| f.lock().unwrap().acked()).unwrap_or_default();
        let mut fleet = self.fleet.lock().unwrap();
        let r = fleet.machines.get_mut(server)?;
        let running = r.guest.as_mut().is_some_and(|g| g.is_running());
        let nodes: Vec<Value> = r.nodes.iter().map(|(s, n)| json!({"storage": s, "node": n.id, "plugged": n.plugged, "bus": n.bus})).collect();
        let live_stdout = r.guest.as_ref().map(|g| g.stdout_bytes());
        let boots: Vec<Value> = r
            .boots
            .iter()
            .enumerate()
            .map(|(i, b)| {
                let last = i + 1 == r.boots.len();
                json!({
                    "argv": b.argv.join(" "),
                    "workdir": b.workdir.display().to_string(),
                    "ran_ms": b.ran_ms,
                    "frames": b.frames,
                    "frame_ms": b.frame_ms,
                    "forwards": b.forwards.iter().map(|(g, h)| json!({"guest_port": g, "host": h})).collect::<Vec<_>>(),
                    "public": b.public.iter().map(|(p, g, h)| json!({"proto": p, "guest_port": g, "at": h})).collect::<Vec<_>>(),
                    "firewall": b.firewall,
                    "utility": b.utility.as_ref().map(|(mac, addr)| json!({"mac": mac, "address": addr, "acked": acked.contains(addr)})),
                    "vnc": b.vnc.as_ref().map(|(h, p)| format!("{h}:{p}")),
                    "progressed": progressed_verdict(b.frames.len(), b.qmp_failures),
                    "qmp_failures": b.qmp_failures,
                    "qmp_error": b.qmp_error,
                    "stdout_bytes": if last && running { live_stdout.unwrap_or(0) } else { b.stdout_bytes },
                    "disks": b.disks.iter().map(|(st, before, after)| json!({
                        "storage": st, "bytes_before": before,
                        "bytes_after": after.or_else(|| last.then(|| file_len(&self.disk_file(st).0))),
                        // What the header claims NOW — how a resize shows.
                        "virtual_bytes": qemu::qcow2_virtual_size(&self.disk_file(st).0).ok(),
                    })).collect::<Vec<_>>(),
                })
            })
            .collect();
        Some(json!({
            "running": running,
            "ejected": r.ejected,
            "refused": r.refused,
            "boots": boots,
            "nodes": nodes,
        }))
    }
}

/// Is `pid` a QEMU started under `root`? Read from `/proc`, so a recycled pid
/// that is now some other program is not mistaken for a leftover guest.
fn qemu_alive_under(pid: u32, root: &str) -> bool {
    std::fs::read(format!("/proc/{pid}/cmdline"))
        .map(|c| {
            let c = String::from_utf8_lossy(&c).replace('\0', " ");
            c.contains("qemu-system") && c.contains(root)
        })
        .unwrap_or(false)
}

// ── QMP, event-aware ─────────────────────────────────────────────────────────
//
// draupnir's `qmp_execute` reads a command's `return`; the unplug needs the
// EVENT that follows it (`DEVICE_DELETED`, sent only once the guest has let
// go), so these open their own session. The shape is gunnar-growbench's
// `rig.rs` (`qmp_device_del`, `qmp_device_add_blk_held`), which measured it.

/// **One HMP command through QMP** (`human-monitor-command`), for the two slirp
/// verbs QMP has no command of its own for: `hostfwd_add` / `hostfwd_remove`.
/// The answer is HMP's text — empty on success. QEMU binds its QMP socket a few
/// ms after it starts, and a rule set is often written right after the
/// power-on, so a connect that finds no socket yet is tried again for 3 s.
fn hmp(g: &QemuGuest, line: &str) -> Result<String, String> {
    let t0 = Instant::now();
    loop {
        match g.qmp_execute("human-monitor-command", Some(json!({"command-line": line}))) {
            Ok(v) => return Ok(v.as_str().unwrap_or_default().to_string()),
            Err(e) => {
                let e = e.to_string();
                if e.contains("qmp connect") && t0.elapsed() < Duration::from_secs(3) {
                    std::thread::sleep(Duration::from_millis(50));
                    continue;
                }
                return Err(e);
            }
        }
    }
}

/// One QMP session past the greeting and `qmp_capabilities`.
fn qmp_open(sock: &Path, read_timeout: Duration) -> Result<(std::os::unix::net::UnixStream, std::io::BufReader<std::os::unix::net::UnixStream>), String> {
    use std::io::{BufRead, Write};
    let s = std::os::unix::net::UnixStream::connect(sock).map_err(|e| format!("qmp {}: {e}", sock.display()))?;
    s.set_read_timeout(Some(read_timeout)).map_err(|e| e.to_string())?;
    let mut w = s.try_clone().map_err(|e| e.to_string())?;
    let mut r = std::io::BufReader::new(s);
    let mut line = String::new();
    r.read_line(&mut line).map_err(|e| format!("qmp greeting: {e}"))?;
    w.write_all(b"{\"execute\":\"qmp_capabilities\"}\n").map_err(|e| e.to_string())?;
    loop {
        line.clear();
        r.read_line(&mut line).map_err(|e| format!("qmp capabilities: {e}"))?;
        if line.contains("\"return\"") || line.contains("\"error\"") {
            break;
        }
    }
    Ok((w, r))
}

/// One command → its `return` value; an `error` reply is the `Err`, with
/// QEMU's own words. Events between are skipped.
fn qmp_cmd(sock: &Path, msg: Value) -> Result<Value, String> {
    use std::io::{BufRead, Write};
    let (mut w, mut r) = qmp_open(sock, Duration::from_secs(20))?;
    w.write_all(format!("{msg}\n").as_bytes()).map_err(|e| e.to_string())?;
    let mut line = String::new();
    loop {
        line.clear();
        if r.read_line(&mut line).map_err(|e| format!("qmp answer to {msg}: {e}"))? == 0 {
            return Err(format!("qmp: QEMU closed the socket after {msg}"));
        }
        let v: Value = match serde_json::from_str(line.trim()) {
            Ok(v) => v,
            Err(_) => continue,
        };
        if let Some(e) = v.get("error") {
            return Err(format!("qmp {} → {}", msg["execute"].as_str().unwrap_or("?"), e["desc"].as_str().unwrap_or(&e.to_string())));
        }
        if let Some(ret) = v.get("return") {
            return Ok(ret.clone());
        }
    }
}

/// `device_del` of `dev` and the WAIT for the guest's acknowledgement: QEMU
/// emits `DEVICE_DELETED` for the id only after the guest's `_EJ0` / slot
/// power-off. `Err` when QEMU refuses, or no ack arrives within `wait` (a
/// kernel without PCI hot-plug never produces one — that is the 511).
fn qmp_device_del_wait(sock: &Path, dev: &str, wait: Duration) -> Result<Duration, String> {
    use std::io::{BufRead, Write};
    let (mut w, mut r) = qmp_open(sock, Duration::from_secs(2))?;
    w.write_all(format!("{}\n", json!({"execute": "device_del", "arguments": {"id": dev}})).as_bytes()).map_err(|e| e.to_string())?;
    let t0 = Instant::now();
    let mut accepted = false;
    let mut line = String::new();
    while t0.elapsed() < wait {
        line.clear();
        match r.read_line(&mut line) {
            Ok(0) => return Err("qmp: QEMU closed the socket during device_del".into()),
            Ok(_) => {}
            Err(e) if matches!(e.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => continue,
            Err(e) => return Err(format!("qmp device_del {dev}: {e}")),
        }
        let Ok(v) = serde_json::from_str::<Value>(line.trim()) else { continue };
        if let Some(e) = v.get("error") {
            return Err(format!("qmp device_del {dev} → {}", e["desc"].as_str().unwrap_or(&e.to_string())));
        }
        if v.get("return").is_some() {
            accepted = true;
        }
        if v["event"] == "DEVICE_DELETED" && v["data"]["device"] == dev {
            return Ok(t0.elapsed());
        }
    }
    Err(format!(
        "device_del {dev}: {} — no DEVICE_DELETED from the guest within {} s (the kernel did not acknowledge the eject)",
        if accepted { "accepted by QEMU" } else { "not even accepted" },
        wait.as_secs()
    ))
}

/// Is `v<id>` a card QEMU holds right now? `query-block` names each attached
/// block device's qdev path; a node whose card was unplugged has none.
fn qmp_device_present(sock: &Path, id: &str) -> Result<bool, String> {
    let blocks = qmp_cmd(sock, json!({"execute": "query-block"}))?;
    Ok(blocks.as_array().into_iter().flatten().any(|b| b["qdev"].as_str().is_some_and(|q| q.contains(&format!("/peripheral/v{id}/")) || q == format!("v{id}"))))
}

/// **Re-plug and make sure it stays plugged.** MEASURED by the rig
/// (2026-09-15, hot-plug kernel): a `device_add` sent right after the guest
/// acked the unplug returned success, yet the card was gone moments later —
/// the slot was still powering off and took the new card with it. Watch the
/// card for `hold`; add again when it vanishes, up to `tries` times.
fn qmp_device_add_held(sock: &Path, id: &str, bus: Option<&str>, addr: Option<&str>, hold: Duration, tries: u32) -> Result<u32, String> {
    for attempt in 1..=tries {
        let mut args = json!({"driver": "virtio-blk-pci", "drive": id, "id": format!("v{id}")});
        if let Some(b) = bus {
            args["bus"] = json!(b);
        }
        if let Some(s) = addr {
            args["addr"] = json!(s);
        }
        qmp_cmd(sock, json!({"execute": "device_add", "arguments": args}))?;
        let t0 = Instant::now();
        let mut present = true;
        while t0.elapsed() < hold {
            std::thread::sleep(Duration::from_millis(250));
            if !qmp_device_present(sock, id)? {
                present = false;
                break;
            }
        }
        if present {
            return Ok(attempt);
        }
        std::thread::sleep(Duration::from_secs(2));
    }
    Err(format!("v{id}: re-plugged {tries} times and never stayed attached"))
}

fn file_len(p: &Path) -> u64 {
    std::fs::metadata(p).map(|m| m.len()).unwrap_or(0)
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Why a live test cannot boot a QEMU under KVM here (`None`: it can): `dev` must open
    /// read-write, as QEMU opens it. A build container has no `/dev/kvm`.
    fn kvm_absent(dev: &std::path::Path) -> Option<String> {
        std::fs::OpenOptions::new().read(true).write(true).open(dev).err().map(|e| format!("{}: {e}", dev.display()))
    }

    /// The live tests' guard, instead of `#[ignore]`: `true` boots; `false` printed
    /// `SKIP <name>: …` (no `/dev/kvm`), and the test passes having booted nothing.
    fn live_or_skip(name: &str) -> bool {
        match kvm_absent(std::path::Path::new("/dev/kvm")) {
            None => true,
            Some(why) => {
                eprintln!("SKIP {name}: needs KVM ({why})");
                false
            }
        }
    }

    /// ★ **No KVM, no boot, and the reason by name** (score bar tests.no-ignore): a device that
    /// is not there or does not open read-write is named; one that opens is no reason.
    #[test]
    fn kvm_is_absent_when_the_device_does_not_open_read_write() {
        let dir = std::env::temp_dir().join(format!("kvm-absent-{}", std::process::id()));
        std::fs::create_dir_all(&dir).unwrap();
        let missing = dir.join("kvm");
        let why = kvm_absent(&missing).expect("a missing device is a reason");
        assert!(why.contains(&missing.display().to_string()), "{why}");
        std::fs::write(&missing, b"").unwrap();
        assert_eq!(kvm_absent(&missing), None, "a device that opens read-write");
        let mut ro = std::fs::metadata(&missing).unwrap().permissions();
        ro.set_readonly(true);
        std::fs::set_permissions(&missing, ro).unwrap();
        // root opens a read-only file read-write anyway; only an ordinary user sees the refusal.
        let root = std::fs::OpenOptions::new().write(true).open(&missing).is_ok();
        assert_eq!(kvm_absent(&missing).is_none(), root, "a device that does not open read-write");
        std::fs::remove_dir_all(&dir).unwrap();
    }

    /// ★ **The live tests skip by name, they are not ignored**: every `live_` test asks
    /// [`live_or_skip`] first, so `cargo test` runs them wherever KVM is and says SKIP elsewhere.
    #[test]
    fn every_live_test_skips_by_name_and_none_is_ignored() {
        let src = include_str!("lib.rs");
        let lines: Vec<&str> = src.lines().collect();
        let attr = ["#[", "ignore"].concat();
        assert!(!lines.iter().any(|l| l.trim_start().starts_with(&attr)), "an ignored test is left");
        let mut live = 0;
        for (i, l) in lines.iter().enumerate() {
            if let Some(rest) = l.trim_start().strip_prefix("fn live_") {
                let name = format!("live_{}", rest.split('(').next().unwrap());
                if name == "live_or_skip" {
                    continue;
                }
                live += 1;
                let first = lines[i + 1..].iter().find(|l| !l.trim().is_empty() && !l.trim_start().starts_with("use ")).unwrap();
                assert!(first.contains(&format!("live_or_skip(\"{name}\")")), "{name} boots without asking for KVM: {first}");
            }
        }
        assert_eq!(live, 2, "the two live tests");
    }

    /// ★ **The host's caps** (round 5.3.1, the UpCloud box: `-smp` could not be
    /// lowered, so each lane was pinned to a core set instead): a guest gets at
    /// most the cap, never more than its plan, and never no core at all.
    #[test]
    fn a_guest_gets_its_plan_under_the_hosts_caps() {
        assert_eq!(capped(4, 16_384, None, None), (4, 16_384), "no cap: the plan, exactly");
        assert_eq!(capped(4, 16_384, Some(2), Some(8_192)), (2, 8_192));
        assert_eq!(capped(2, 4_096, Some(8), Some(65_536)), (2, 4_096), "a cap never raises the plan");
        assert_eq!(capped(4, 4_096, Some(0), None), (1, 4_096), "never no core");
    }

    /// ★ **A QEMU that exits at once said why** (round 5.3.1): a taken port is
    /// booted again; words on stderr are the HOST's refusal, by name; an exit
    /// with nothing said is a guest's own power-off.
    /// ★ A host port lost between the pick and QEMU's own bind is a TAKEN port (boot again with
    /// fresh ports), whatever words QEMU says it in. Dwarf 2026-10-12: the twin's forwards named
    /// 127.0.0.1:35453 twice (`-:80` and `-:2222`), QEMU said only "Could not set up host
    /// forwarding rule", the mock called it the host's refusal, never booted the twin again, and
    /// the install waited 30 min on "no WireGuard handshake yet".
    #[test]
    fn a_forwarding_rule_qemu_could_not_set_up_is_a_taken_port() {
        assert_eq!(breath_of("qemu-system-x86_64: -netdev user,id=n0,hostfwd=tcp:127.0.0.1:35453-:80,hostfwd=tcp:127.0.0.1:35453-:2222: Could not set up host forwarding rule 'tcp:127.0.0.1:35453-:80'\n"), FirstBreath::TakenPort);
    }

    /// ★ The host ports of ONE server are distinct: each is held while the next is picked.
    #[test]
    fn one_servers_host_ports_are_never_the_same_port_twice() {
        let g = DraupnirGuest::default();
        for _ in 0..200 {
            let mut boot = g.spec_for(&spec());
            let fw = wire_nics(&mut boot, Mtls::Door, None, false);
            let mut ports: Vec<&str> = fw.iter().map(|(_, a)| a.as_str()).collect();
            let n = ports.len();
            ports.sort();
            ports.dedup();
            assert_eq!(ports.len(), n, "a host port twice: {fw:?}");
        }
    }

    #[test]
    fn a_qemu_that_exits_at_once_is_the_hosts_refusal_by_its_own_words() {
        assert_eq!(breath_of("qemu-system-x86_64: -netdev user: Could not set up host forwarding rule: Address already in use\n"), FirstBreath::TakenPort);
        assert_eq!(breath_of(""), FirstBreath::Alive);
        assert_eq!(breath_of("\n  \n"), FirstBreath::Alive);
        assert_eq!(
            breath_of("qemu-system-x86_64: cannot set up guest memory 'pc.ram': Cannot allocate memory\n"),
            FirstBreath::Died("qemu-system-x86_64: cannot set up guest memory 'pc.ram': Cannot allocate memory".into())
        );
    }

    use draupnir::{Display, Firmware, SerialConsole};

    fn spec() -> GuestSpec {
        let mut s = GuestSpec::new("appliance", "/vm/appliance.qcow2");
        s.medium = Some("/vm/gunnar.iso".to_string());
        s.cdrom_first = true;
        s
    }

    /// **A public forward another process holds is refused BY NAME before QEMU starts**
    /// (stinkies mock-boot-udp-port-collision, loki 2026-09-30: kdeconnectd held
    /// 0.0.0.0:51821/udp — inside Linux's ephemeral range — so every guest's
    /// `hostfwd=udp:127.86.33.13:51821-:51821` failed, QEMU exited at once on every
    /// boot and the API still said `started`). Held here the way it was there: a UDP
    /// socket on the WILDCARD address and the port asked on a 127.x public address.
    #[test]
    fn a_public_forward_a_host_process_holds_is_refused_by_name() {
        let held = std::net::UdpSocket::bind("0.0.0.0:0").expect("a wildcard udp socket");
        let port = held.local_addr().unwrap().port();
        let taken = vec![("udp".to_string(), port, format!("127.86.33.13:{port}"))];
        let why = public_port_taken(&taken).expect("a held port is named");
        assert!(why.starts_with("mock-public-port-in-use:") && why.contains(&format!("127.86.33.13:{port}")) && why.contains("ss -lunp"), "{why}");
        drop(held);
        assert_eq!(public_port_taken(&taken), None, "a free port is not refused");
    }

    /// The machine this engine renders honours every clause — and the check is
    /// the engine's OWN preflight, so the test and the shipped path agree by
    /// construction rather than by a second reading of the same table.
    #[test]
    fn the_rendered_machine_passes_its_own_clause_check() {
        let g = DraupnirGuest::default();
        let s = spec();
        let argv = qemu::argv(&g.spec_for(&s), None, None).unwrap();
        assert_eq!(g.preflight(&s, &argv), None, "argv: {}", argv.join(" "));
    }

    /// **The refusals, by name.** A guest handed a machine with a serial console
    /// or a UEFI pflash must be refused, not booted: those two are exactly what a
    /// tunnr-shaped guest would have produced, and booting them would report
    /// behaviour 13 and 15 as tested when they were not.
    #[test]
    fn a_serial_console_or_a_pflash_is_refused_by_name() {
        let g = DraupnirGuest::default();
        let s = spec();
        let good = qemu::argv(&g.spec_for(&s), None, None).unwrap();

        let with_serial: Vec<String> = good
            .iter()
            .map(|a| if a == "none" { "mon:stdio".into() } else { a.clone() })
            .collect();
        assert_eq!(
            g.preflight(&s, &with_serial).map(|(c, _)| c),
            Some("no-serial-console")
        );

        let mut with_pflash = good.clone();
        with_pflash.push("-drive".into());
        with_pflash.push("if=pflash,format=raw,readonly=on,file=/fw/OVMF_CODE_4M.fd".into());
        assert_eq!(g.preflight(&s, &with_pflash).map(|(c, _)| c), Some("seabios"));

        let no_screen: Vec<String> = good
            .iter()
            .filter(|a| a.as_str() != "std")
            .cloned()
            .collect();
        assert_eq!(
            g.preflight(&s, &no_screen).map(|(c, _)| c),
            Some("framebuffer")
        );

        // L46: an argv on q35 while the engine owes i440fx is refused by name.
        let on_q35: Vec<String> = good
            .iter()
            .map(|a| if a.starts_with("pc,") { a.replacen("pc,", "q35,", 1) } else { a.clone() })
            .collect();
        assert_eq!(g.preflight(&s, &on_q35).map(|(c, _)| c), Some("machine-pc"));
    }

    /// **Each forward on the NIC it is served on** (T15's TD8): git's 2222 on
    /// eth0 (`n0`, the public NIC), gRPC's 50051 on eth1 (`n1`, utility), both
    /// bound to 127.0.0.1 and nowhere else, and nothing forwarded twice.
    #[test]
    fn git_rides_the_public_nic_and_grpc_the_utility_nic() {
        let g = DraupnirGuest::default();
        let mut boot = g.spec_for(&spec());
        let fw = wire_nics(&mut boot, Mtls::Off, None, false);
        let j = qemu::argv(&boot, None, None).unwrap().join(" ");
        let n0 = j.split(" -netdev ").find(|a| a.starts_with("user,id=n0")).unwrap().split(' ').next().unwrap().to_string();
        let n1 = j.split(" -netdev ").find(|a| a.starts_with("user,id=n1")).unwrap().split(' ').next().unwrap().to_string();
        assert!(n0.contains("-:2222") && !n0.contains("-:50051"), "eth0: {n0}");
        assert!(n1.contains("-:50051") && !n1.contains("-:2222"), "eth1: {n1}");
        assert!(n1.contains("net=10.0.3.0/24") && !n0.contains("net=10.0.3"), "eth1 is its own subnet, eth0 is not: {n0} / {n1}");
        for nic in [&n0, &n1] {
            assert!(nic.contains("hostfwd=tcp:127.0.0.1:") && !nic.contains("hostfwd=tcp::"), "loopback only: {nic}");
        }
        assert_eq!(fw.iter().map(|(p, _)| *p).collect::<Vec<_>>(), [80, 443, 2222, 50051]);
        assert_eq!(g.preflight(&spec(), &j.split(' ').map(String::from).collect::<Vec<_>>()), None);
    }

    /// **The installer dials the API's public address at the real ports**
    /// (`--public-on-loopback`): eth0 carries tcp/8474, udp/51820 and udp/51821
    /// from the server's own loopback address to the same port in the guest,
    /// beside the loopback TCP forwards it always had and the DNS-to-nowhere;
    /// draupnir's own NIC stands down (`-nic none`) so there is ONE eth0.
    #[test]
    fn the_installer_ports_ride_the_public_nic_on_the_servers_own_address() {
        let g = DraupnirGuest::default();
        let mut boot = g.spec_for(&spec());
        let fw = wire_nics(&mut boot, Mtls::Off, None, false);
        let public = serve_public_on(&mut boot, "127.80.99.12", mock_upcloud::kvm::INSTALLER_PUBLIC_PORTS);
        let argv = qemu::argv(&boot, None, None).unwrap();
        let j = argv.join(" ");
        let n0s: Vec<&str> = j.split(" -netdev ").filter(|a| a.starts_with("user,id=n0")).collect();
        assert_eq!(n0s.len(), 1, "one eth0: {j}");
        let n0 = n0s[0].split(' ').next().unwrap();
        for rule in ["hostfwd=tcp:127.80.99.12:8474-:8474", "hostfwd=udp:127.80.99.12:51820-:51820", "hostfwd=udp:127.80.99.12:51821-:51821", "hostfwd=tcp:127.80.99.12:8475-:8475", "hostfwd=udp:127.80.99.12:51822-:51822", "hostfwd=udp:127.80.99.12:51830-:51830"] {
            assert!(n0.contains(rule), "{rule} missing: {n0}");
        }
        let (_, git) = fw.iter().find(|(p, _)| *p == 2222).unwrap();
        assert!(n0.contains(&format!("hostfwd=tcp:{git}-:2222")), "the loopback forward stays: {n0}");
        assert!(n0.contains("dns="), "behaviour 18 stays: {n0}");
        assert!(j.contains("-nic none"), "draupnir's NIC stands down: {j}");
        assert_eq!(public.iter().map(|(p, n, a)| format!("{p}/{n}@{a}")).collect::<Vec<_>>(), [
            "tcp/8474@127.80.99.12:8474",
            "udp/51820@127.80.99.12:51820",
            "udp/51821@127.80.99.12:51821",
            "tcp/8475@127.80.99.12:8475",
            // DEV-NARRATE (2026-09-30): the sealed early-boot narration, when admitted.
            "udp/51822@127.80.99.12:51822",
            // OX-WGDS3: the front's downstream WireGuard interface.
            "udp/51830@127.80.99.12:51830"
        ]);
        assert_eq!(g.preflight(&spec(), &argv), None, "{j}");
    }

    /// ★ **A server with no public address has no public NIC** (owner 2026-10-23, the lean
    /// estate): its ONE NIC is the utility fabric (it is the guest's eth0), draupnir's own NIC
    /// stands down, and nothing is forwarded from the host — no user-mode NIC NATs it out.
    #[test]
    fn a_server_with_no_public_address_has_only_its_utility_nic() {
        let g = DraupnirGuest::default();
        let mut boot = g.spec_for(&spec());
        let group: std::net::SocketAddrV4 = "239.1.2.3:4567".parse().unwrap();
        let fw = wire_nics(&mut boot, Mtls::Off, Some((group, "52:54:00:aa:bb:cc")), false);
        let fw = no_public_nic(&mut boot, fw);
        let j = qemu::argv(&boot, None, None).unwrap().join(" ");
        assert!(fw.is_empty(), "nothing forwarded: {fw:?}");
        assert!(!j.contains("user,id="), "no user-mode NIC: {j}");
        assert!(j.contains("-nic none"), "draupnir's NIC stands down: {j}");
        assert_eq!(j.matches("virtio-net-pci").count(), 1, "one NIC: {j}");
        assert!(j.contains("dgram,id=n1") && j.contains("mac=52:54:00:aa:bb:cc"), "the utility fabric: {j}");
        // The preflight's behaviour 18 (no DNS answer, no inbound UDP) holds: there is no
        // user-mode NIC to answer one (the lean gate's main stood at `no-inbound-udp`).
        let argv: Vec<String> = j.split(' ').map(String::from).collect();
        assert_eq!(g.preflight(&spec(), &argv), None, "{j}");
    }

    /// **A port the firewall does not admit is not forwarded at boot.** Only
    /// the admitted installer ports ride eth0; with nothing admitted eth0 is
    /// still `n0` (so a later rule can open a port on it live) and carries no
    /// public forward at all; a port that is not an installer port is never
    /// rendered, whatever the caller passes.
    #[test]
    fn only_the_admitted_installer_ports_are_forwarded_at_boot() {
        let g = DraupnirGuest::default();
        let n0_of = |admitted: &[(&'static str, u16)]| {
            let mut boot = g.spec_for(&spec());
            wire_nics(&mut boot, Mtls::Off, None, false);
            let public = serve_public_on(&mut boot, "127.80.99.12", admitted);
            let argv = qemu::argv(&boot, None, None).unwrap();
            let j = argv.join(" ");
            let n0 = j.split(" -netdev ").find(|a| a.starts_with("user,id=n0")).unwrap().split(' ').next().unwrap().to_string();
            assert_eq!(g.preflight(&spec(), &argv), None, "{j}");
            (n0, public)
        };
        let (n0, public) = n0_of(&[("udp", 51821)]);
        assert!(n0.contains("hostfwd=udp:127.80.99.12:51821-:51821"), "{n0}");
        assert!(!n0.contains(":8474-") && !n0.contains(":51820-"), "not admitted, still forwarded: {n0}");
        assert_eq!(public, vec![("udp".to_string(), 51821, "127.80.99.12:51821".to_string())]);

        let (n0, public) = n0_of(&[]);
        assert!(!n0.contains("127.80.99.12"), "nothing admitted, something forwarded: {n0}");
        assert!(public.is_empty());

        let (n0, _) = n0_of(&[("udp", 53), ("tcp", 22), ("tcp", 8474)]);
        assert!(n0.contains("hostfwd=tcp:127.80.99.12:8474-:8474") && !n0.contains("127.80.99.12:53") && !n0.contains("127.80.99.12:22"), "{n0}");
    }

    /// **Live, under KVM, through the estate: the public address answers
    /// exactly what the server's firewall admits, and follows every write.**
    /// A server laid with `firewall: on` and no rules is wide open (all three
    /// ports held by its QEMU); the rule set with its catch-all drop is then
    /// written and ONLY the admitted port answers — a tcp/8474 without a rule is
    /// `connection refused` at the public address, the udp/51821 with one is
    /// held; a second write turns it round, live, with no restart; and the next
    /// power-on starts with just the admitted set. Fails before the firewall
    /// reached the engine: every port stayed forwarded whatever the rules said.
    /// Without /dev/kvm it says SKIP by name.
    #[test]
    fn live_the_public_address_answers_exactly_what_the_firewall_admits() {
        if !live_or_skip("live_the_public_address_answers_exactly_what_the_firewall_admits") {
            return;
        }
        use mock_upcloud::estate::Rule;
        use mock_upcloud::{Clock, Estate, Faults};
        let root = std::env::temp_dir().join(format!("lane-c-fw-{}", std::process::id()));
        let (g, _) = DraupnirGuest::for_estate(root.clone()).unwrap();
        let g = std::sync::Arc::new(g);
        let mut e = Estate::new(Clock::virtual_only(), Faults::none(), 23).with_public_on_loopback(80, 97).with_engine(g.clone());
        let uuid = e.create_server("main", "main", "1xCPU-1GB", "se-sto1", vec![], "boot", 1).unwrap();
        e.configure_server(&uuid, vec![], None, true, false, "UTC").unwrap();
        e.run_to_quiet();
        let public = e.server(&uuid).unwrap().public_ip.clone();
        assert!(public.starts_with("127.80.97."), "{public}");
        std::thread::sleep(Duration::from_millis(1500));

        let held = |proto: &str, port: u16| {
            let taken = match proto {
                "tcp" => std::net::TcpListener::bind((public.as_str(), port)).err().map(|e| e.kind()),
                _ => std::net::UdpSocket::bind((public.as_str(), port)).err().map(|e| e.kind()),
            };
            taken == Some(std::io::ErrorKind::AddrInUse)
        };
        let dial_8474 = || std::net::TcpStream::connect_timeout(&format!("{public}:8474").parse().unwrap(), Duration::from_secs(2)).map(|_| ()).map_err(|e| e.kind());
        let rule = |proto: &str, port: &str| Rule {
            direction: "in".into(),
            action: "accept".into(),
            family: "IPv4".into(),
            protocol: proto.into(),
            destination_port_start: port.into(),
            destination_port_end: port.into(),
            ..Rule::default()
        };
        let drop_all = Rule { direction: "in".into(), action: "drop".into(), ..Rule::default() };
        let mut verdicts = Vec::new();
        let mut check = |when: &str, want: &[(&str, u16, bool)]| {
            for (p, n, w) in want {
                let h = held(p, *n);
                if h != *w {
                    verdicts.push(format!("{when}: {p}/{public}:{n} held={h}, want {w}"));
                }
            }
        };

        // 1. On, no rules: wide open.
        check("no rules", &[("tcp", 8474, true), ("udp", 51820, true), ("udp", 51821, true)]);
        assert_eq!(dial_8474(), Ok(()), "open firewall: 8474 must connect");

        // 2. The WireGuard link admitted, then the catch-all: tcp/8474 has no rule.
        e.set_rules(&uuid, vec![rule("udp", "51821"), drop_all.clone()]).unwrap();
        check("udp/51821 + drop", &[("tcp", 8474, false), ("udp", 51820, false), ("udp", 51821, true)]);
        assert_eq!(dial_8474(), Err(std::io::ErrorKind::ConnectionRefused), "8474 has no rule and still connects");

        // 3. Turned round, live: 8474 with a rule, 51821 without.
        e.set_rules(&uuid, vec![rule("tcp", "8474"), drop_all.clone()]).unwrap();
        check("tcp/8474 + drop", &[("tcp", 8474, true), ("udp", 51820, false), ("udp", 51821, false)]);
        assert_eq!(dial_8474(), Ok(()), "8474 has a rule and does not connect");

        // 4. The next power-on starts with just what is admitted.
        e.set_rules(&uuid, vec![rule("udp", "51821"), drop_all]).unwrap();
        e.stop_server(&uuid, true).unwrap();
        e.run_to_quiet();
        e.start_server(&uuid).unwrap();
        e.run_to_quiet();
        std::thread::sleep(Duration::from_millis(1500));
        check("after a restart", &[("tcp", 8474, false), ("udp", 51820, false), ("udp", 51821, true)]);
        let ev = g.fleet_evidence(&uuid).unwrap();
        let last_argv = ev["boots"].as_array().unwrap().last().unwrap()["argv"].as_str().unwrap().to_string();
        let changes = ev["boots"][0]["firewall"].clone();

        g.kill_all();
        let _ = std::fs::remove_dir_all(&root);
        assert!(verdicts.is_empty(), "{verdicts:#?}\nlive changes: {changes}");
        assert!(last_argv.contains(&format!("hostfwd=udp:{public}:51821-:51821")) && !last_argv.contains(&format!("{public}:8474")), "{last_argv}");
        assert_eq!(changes.as_array().map(|a| a.len()), Some(6), "two per write, applied live: {changes}");
    }

    /// **Inbound UDP is the WireGuard ports on a public address, and nothing
    /// else.** A forward of DNS, or of a WireGuard port on 127.0.0.1 (a port the
    /// installer cannot tell apart per server), or to a different guest port, is
    /// `no-inbound-udp`.
    #[test]
    fn any_other_inbound_udp_is_refused_by_name() {
        let g = DraupnirGuest::default();
        for bad in [
            "hostfwd=udp:127.80.99.12:53-:53",
            "hostfwd=udp:127.80.99.12:51821-:51820",
            "hostfwd=udp::51821-:51821",
            "hostfwd=udp:0.0.0.0:51821-:51821",
            "hostfwd=udp:192.0.2.10:51821-:51821",
        ] {
            let mut argv = qemu::argv(&g.spec_for(&spec()), None, None).unwrap();
            let i = argv.iter().position(|a| a.starts_with("user,id=n0")).unwrap();
            argv[i].push_str(&format!(",{bad}"));
            assert_eq!(g.preflight(&spec(), &argv).map(|(c, _)| c), Some("no-inbound-udp"), "{bad}");
        }
    }

    /// ★ **The front's downstream WireGuard port is inbound UDP the mock admits** (lane
    /// OX-WGDS3): a forward of udp/51830 on a public address, to the same guest port, passes the
    /// guard; on another guest port it is still `no-inbound-udp`.
    #[test]
    fn the_downstream_wireguard_port_is_an_admitted_inbound_udp() {
        let g = DraupnirGuest::default();
        let mut argv = qemu::argv(&g.spec_for(&spec()), None, None).unwrap();
        let i = argv.iter().position(|a| a.starts_with("user,id=n0")).unwrap();
        let mut bad = argv.clone();
        argv[i].push_str(",hostfwd=udp:127.80.99.12:51830-:51830");
        assert!(udp_forwards_are_wireguard_only(&argv.join(" ")), "{}", argv.join(" "));
        bad[i].push_str(",hostfwd=udp:127.80.99.12:51830-:51831");
        assert_eq!(g.preflight(&spec(), &bad).map(|(c, _)| c), Some("no-inbound-udp"));
    }

    /// **Live, under KVM: each server's public address is HELD, at the
    /// installer's ports, by that server's QEMU** — so the installer's dial
    /// lands in that guest and nowhere else. Three machines, three addresses,
    /// the same three ports on each. Fails on the mock before
    /// `--public-on-loopback`: the API's address was TEST-NET and nothing on
    /// this host answered at it. Without /dev/kvm it says SKIP by name.
    #[test]
    fn live_each_servers_public_address_is_held_at_the_installer_ports() {
        if !live_or_skip("live_each_servers_public_address_is_held_at_the_installer_ports") {
            return;
        }
        let root = std::env::temp_dir().join(format!("lane-c-public-{}", std::process::id()));
        let (g, _) = DraupnirGuest::for_estate(root.clone()).unwrap();
        let publics = ["127.80.98.10", "127.80.98.11", "127.80.98.12"];
        let servers: Vec<String> = (0..3).map(|i| format!("00000000-0000-4000-8000-00000000c0{i}")).collect();
        for (s, p) in servers.iter().zip(publics) {
            let m = Machine {
                server: s.clone(),
                mem_mb: 256,
                cores: 1,
                disks: vec![mock_upcloud::kvm::MachineDisk { storage: format!("01{}", &s[2..]), size_gib: 1, template: None }],
                medium: None,
                cdrom_first: false,
                disk_first: false,
                rtc_skew_seconds: 7200,
                utility_mac: String::new(),
                utility_offer: mock_upcloud::net::DhcpOffer::for_address("10.13.8.100"),
                dhcp_late_first_boot_ms: 0,
                hostname: String::new(),
                ssh_keys: Vec::new(),
                user_data: None,
                public_ip: p.to_string(),
                public_admitted: mock_upcloud::kvm::INSTALLER_PUBLIC_PORTS.to_vec(),
            };
            g.fleet_power_on(&m).unwrap();
        }
        std::thread::sleep(Duration::from_millis(1500));
        let mut held = Vec::new();
        for p in publics {
            for (proto, port) in mock_upcloud::kvm::INSTALLER_PUBLIC_PORTS {
                let taken = match *proto {
                    "tcp" => std::net::TcpListener::bind((p, *port)).err().map(|e| e.kind()),
                    _ => std::net::UdpSocket::bind((p, *port)).err().map(|e| e.kind()),
                };
                held.push((format!("{proto}/{p}:{port}"), taken == Some(std::io::ErrorKind::AddrInUse)));
            }
            // And the dial the installer makes in direct mode connects.
            assert!(std::net::TcpStream::connect_timeout(&format!("{p}:8474").parse().unwrap(), Duration::from_secs(2)).is_ok(), "{p}:8474");
        }
        for s in &servers {
            g.fleet_power_off(s, true);
        }
        let _ = std::fs::remove_dir_all(&root);
        let missing: Vec<_> = held.iter().filter(|(_, h)| !h).map(|(w, _)| w.clone()).collect();
        assert!(missing.is_empty(), "not held by the server's QEMU: {missing:?}");
    }

    /// **The door is main's**: never the front's (no medium), the first server booted from the tray
    /// takes it, and it stays that server's across its restarts while the twin never gets it.
    #[test]
    fn the_mtls_door_goes_to_the_first_server_booted_from_the_medium_and_stays_its() {
        assert_eq!(mtls_role(false, None, "front", false), Mtls::Off);
        assert_eq!(mtls_role(true, None, "front", false), Mtls::Other, "the front boots first, from a template");
        assert_eq!(mtls_role(true, None, "main", true), Mtls::Door, "main installs from the tray");
        assert_eq!(mtls_role(true, Some("main"), "twin", true), Mtls::Other, "the twin installs from the same medium, later");
        assert_eq!(mtls_role(true, Some("main"), "main", false), Mtls::Door, "main restarted with the tray ejected keeps it");
        assert_eq!(mtls_role(true, Some("main"), "front", false), Mtls::Other);
    }

    /// **The mTLS door, only when asked** (`--mtls`): eth1 carries 50051 AND 50052, both on
    /// loopback; eth0 is untouched; without the flag the utility NIC is exactly as before.
    #[test]
    fn the_mtls_door_rides_the_utility_nic_only_when_asked() {
        let g = DraupnirGuest::default();
        let n1_of = |mtls: Mtls| {
            let mut boot = g.spec_for(&spec());
            let fw = wire_nics(&mut boot, mtls, None, false);
            let j = qemu::argv(&boot, None, None).unwrap().join(" ");
            let n0 = j.split(" -netdev ").find(|a| a.starts_with("user,id=n0")).unwrap().split(' ').next().unwrap().to_string();
            let n1 = j.split(" -netdev ").find(|a| a.starts_with("user,id=n1")).unwrap().split(' ').next().unwrap().to_string();
            (n0, n1, fw)
        };
        let port_of = |fw: &Vec<(u16, String)>, g: u16| fw.iter().find(|(p, _)| *p == g).and_then(|(_, h)| h.rsplit(':').next()?.parse::<u16>().ok()).unwrap();
        let (n0, n1, fw) = n1_of(Mtls::Door);
        assert!(n1.contains("-:50051") && n1.contains("-:50052"), "eth1: {n1}");
        assert!(!n0.contains("-:50052"), "eth0 must not carry the mTLS door: {n0}");
        assert_eq!(n1.matches("hostfwd=tcp:127.0.0.1:").count(), 2, "loopback only: {n1}");
        assert_eq!(fw.iter().map(|(p, _)| *p).collect::<Vec<_>>(), [80, 443, 2222, 50051, 50052]);
        // The twin dials the port the primary binds: host 50052 itself when it is free.
        assert!(port_of(&fw, 50052) == 50052 || port_of(&fw, 50052) == port_of(&fw, 50051) + 1, "mTLS door at {}", port_of(&fw, 50052));
        // **Another server never takes the door**, free or not: it gets the adjacent pair, so a
        // twin restarted while its primary is between boots cannot end up dialling itself.
        let (_, other, fw_other) = n1_of(Mtls::Other);
        assert!(other.contains("-:50052"), "the other server still has an mTLS forward: {other}");
        assert_ne!(port_of(&fw_other, 50052), 50052, "the door's host port is the holder's alone");
        assert_eq!(port_of(&fw_other, 50052), port_of(&fw_other, 50051) + 1, "an adjacent pair: {other}");
        let (_, off, fw_off) = n1_of(Mtls::Off);
        assert!(!off.contains("-:50052"), "off by default: {off}");
        assert_eq!(fw_off.iter().map(|(p, _)| *p).collect::<Vec<_>>(), [80, 443, 2222, 50051]);
    }

    /// **`pc` by default, q35 only by name.** The default engine renders
    /// i440fx; an engine told `Q35` renders q35 and passes its own check, and
    /// nothing else in the argv moves.
    #[test]
    fn the_machine_is_pc_unless_q35_is_asked_for() {
        let s = spec();
        let pc = DraupnirGuest::default();
        let a = qemu::argv(&pc.spec_for(&s), None, None).unwrap();
        assert_eq!(&a[..2], ["-machine", "pc,accel=kvm:tcg"]);
        assert_eq!(pc.preflight(&s, &a), None);
        let q = DraupnirGuest { machine: draupnir::MachineType::Q35, ..DraupnirGuest::default() };
        let b = qemu::argv(&q.spec_for(&s), None, None).unwrap();
        assert_eq!(b[1], "q35,accel=kvm:tcg");
        assert_eq!(q.preflight(&s, &b), None);
        assert_eq!(a[2..], b[2..]);
    }

    /// The engine's spec really is the cloud shape, field by field — the shape
    /// the argv check above is checking.
    #[test]
    fn the_spec_is_seabios_no_serial_a_framebuffer_and_two_hours_ahead() {
        let g = DraupnirGuest::default();
        let b = g.spec_for(&spec());
        assert!(matches!(b.firmware, Firmware::Bios { .. }));
        assert_eq!(b.serial, SerialConsole::None);
        assert!(matches!(b.display, Display::Framebuffer { .. }));
        assert_eq!(b.rtc, Rtc::SkewedBy { seconds: 7200 });
        assert_eq!(b.guest_dns.as_deref(), Some("10.0.2.99"));
        assert_eq!(b.boot_order, draupnir::BootOrder::MediumThenDisk);
    }

    /// A host with no QEMU is a REFUSAL naming the host, never a guest that
    /// failed to boot: the two are different facts and only one of them is about
    /// the software under test.
    /// **A root too long for its QMP sockets is refused BY NAME at start**,
    /// with the limit and the length — the defect T3 met was a 108-byte path
    /// that failed silently and read as a hung guest.
    #[test]
    fn a_root_too_long_for_its_qmp_sockets_is_refused_by_name() {
        let long = PathBuf::from("/home/rickard/scratch/t3-twin-restore/mkvm/root");
        assert!(longest_qmp_path(&long) > UNIX_SOCKET_PATH_MAX);
        let Err(why) = DraupnirGuest::for_estate(long) else {
            panic!("a root whose sockets exceed 107 bytes was accepted")
        };
        assert!(why.starts_with("qmp-socket-path:"), "{why}");
        assert!(why.contains("107") && why.contains(&longest_qmp_path(Path::new("/home/rickard/scratch/t3-twin-restore/mkvm/root")).to_string()), "{why}");

        // The longest root that fits is accepted; one byte more is not.
        // What a root adds to its own length: "/servers/<uuid>/boot-9999/qmp.sock".
        let suffix = longest_qmp_path(Path::new("/tmp/r")) - "/tmp/r".len();
        let fits = format!("/tmp/{}", "r".repeat(UNIX_SOCKET_PATH_MAX - suffix - "/tmp/".len()));
        assert_eq!(longest_qmp_path(Path::new(&fits)), UNIX_SOCKET_PATH_MAX);
        assert!(DraupnirGuest::for_estate(PathBuf::from(&fits)).is_ok());
        let _ = std::fs::remove_dir_all(&fits);
        assert!(DraupnirGuest::for_estate(PathBuf::from(format!("{fits}x"))).is_err());

        // The one-shot run refuses too, rather than booting blind.
        let g = DraupnirGuest { root: PathBuf::from(format!("/tmp/{}", "y".repeat(100))), ..Default::default() };
        match g.run(&GuestSpec::new("appliance", "/nonexistent.qcow2")).outcome {
            GuestOutcome::Refused { clause, .. } => assert_eq!(clause, "qmp-socket-path"),
            other => panic!("{other}"),
        }
    }

    // ── the hot-plug shape (T21r) ────────────────────────────────────────────

    fn three_disks() -> Vec<(String, PathBuf, &'static str)> {
        vec![
            ("01000009-0000-4000-8000-0000000714a1".into(), PathBuf::from("/d/a.qcow2"), "qcow2"),
            ("0100000a-0000-4000-8000-0000000714a2".into(), PathBuf::from("/d/b.qcow2"), "qcow2"),
            ("0100000b-0000-4000-8000-0000000714a3".into(), PathBuf::from("/m/iso.iso"), "raw"),
        ]
    }

    /// RED-when-broken: without `--hotplug` a guest keeps EXACTLY the machine it
    /// had — the implicit `-drive if=virtio` per data disk, no node, no card.
    #[test]
    fn without_hotplug_the_data_disks_are_the_implicit_virtio_drives_they_always_were() {
        let mut nodes = BTreeMap::new();
        let a = data_disk_args(false, draupnir::MachineType::Pc, &three_disks(), &mut nodes);
        assert_eq!(
            a,
            vec![
                "-drive", "file=/d/a.qcow2,if=virtio,format=qcow2",
                "-drive", "file=/d/b.qcow2,if=virtio,format=qcow2",
                "-drive", "file=/m/iso.iso,if=virtio,format=raw",
            ]
        );
        assert!(nodes.is_empty(), "no node is remembered for a disk QMP cannot address");
        let g = DraupnirGuest::default();
        assert!(!g.hotplug, "off by default");
    }

    /// With hot-plug every data disk is a `-blockdev` file node, a format node
    /// over it and a `virtio-blk-pci` card over that, in disk order; on i440fx
    /// the card goes on `pci.0` (no bus), and every node is remembered plugged.
    #[test]
    fn with_hotplug_the_data_disks_are_blockdev_nodes_behind_virtio_blk_pci_cards() {
        let mut nodes = BTreeMap::new();
        let disks = three_disks();
        let a = data_disk_args(true, draupnir::MachineType::Pc, &disks, &mut nodes);
        let id = node_id(&disks[0].0);
        assert_eq!(&a[..6], &[
            "-blockdev".to_string(), format!("driver=file,filename=/d/a.qcow2,node-name=f{id}"),
            "-blockdev".to_string(), format!("driver=qcow2,node-name={id},file=f{id}"),
            "-device".to_string(), format!("virtio-blk-pci,drive={id},id=v{id},addr=0x10"),
        ]);
        // Pinned HIGH, one slot per card: the boot disk keeps the lowest slot
        // (MEASURED 2026-09-26: left to QEMU the cards came first and SeaBIOS
        // booted a data member).
        assert!(a.iter().any(|x| x.ends_with(",addr=0x12")), "{a:?}");
        assert_eq!(nodes[&disks[2].0].addr.as_deref(), Some("0x12"));
        assert_eq!(pc_slot(&["0x10".into(), "0x11".into()]).as_deref(), Some("0x12"));
        assert_eq!(pc_slot(&(0x10..=0x1f).map(|s| format!("0x{s:x}")).collect::<Vec<_>>()), None);
        assert!(a.iter().any(|x| x == "driver=raw,node-name=d0100000b0000000714a3,file=fd0100000b0000000714a3"), "{a:?}");
        assert!(!a.iter().any(|x| x.contains("pcie-root-port")), "i440fx: no root ports");
        assert_eq!(nodes.len(), 3);
        assert!(nodes.values().all(|n| n.plugged && n.bus.is_none()));
        assert!(nodes.values().all(|n| n.addr.is_some()));
        assert_eq!(nodes[&disks[1].0].id, "d0100000a0000000714a2");
    }

    /// On q35 `pcie.0` refuses `device_del`, so each card sits behind its own
    /// root port and spare ports are rendered for volumes attached later.
    #[test]
    fn on_q35_each_card_sits_behind_a_root_port_and_spare_ports_wait_for_later_attaches() {
        let mut nodes = BTreeMap::new();
        let disks = three_disks();
        let a = data_disk_args(true, draupnir::MachineType::Q35, &disks, &mut nodes);
        let ports: Vec<&String> = a.iter().filter(|x| x.starts_with("pcie-root-port,id=rp")).collect();
        assert_eq!(ports.len(), 3 + SPARE_ROOT_PORTS);
        assert_eq!(ports[0], "pcie-root-port,id=rp0,chassis=1,slot=1");
        assert!(a.iter().any(|x| x.ends_with(",bus=rp0")) && a.iter().any(|x| x.ends_with(",bus=rp2")), "{a:?}");
        assert_eq!(nodes[&disks[2].0].bus.as_deref(), Some("rp2"));
        assert!(nodes.values().all(|n| n.addr.is_none()) && !a.iter().any(|x| x.contains(",addr=")), "q35: the root port is the slot");
    }

    /// A node name starts with a letter, is under 32 bytes, and two of the
    /// mock's uuids (which differ in their first 8 and last 12 hex digits)
    /// never share one.
    #[test]
    fn node_ids_are_valid_qemu_node_names_and_distinct() {
        let a = node_id("01000009-0000-4000-8000-0000000714a1");
        let b = node_id("01000009-0000-4000-8000-0000000714a2");
        let c = node_id("0100000a-0000-4000-8000-0000000714a1");
        for id in [&a, &b, &c] {
            assert!(id.starts_with('d') && id.len() < 32 && id.chars().all(|ch| ch.is_ascii_alphanumeric()), "{id}");
        }
        assert!(a != b && a != c && b != c);
        assert_eq!(node_id("short"), "dshort");
    }

    /// An unreadable screen is UNKNOWN, never "did not progress".
    #[test]
    fn a_screen_qmp_cannot_read_is_unknown_not_frozen() {
        assert_eq!(progressed_verdict(0, 5), None);
        assert_eq!(progressed_verdict(0, 0), Some(false));
        assert_eq!(progressed_verdict(2, 3), Some(false));
        assert_eq!(progressed_verdict(3, 0), Some(true));
    }

    #[test]
    fn a_host_without_qemu_is_refused_as_a_host() {
        std::env::set_var("DRAUPNIR_QEMU_BIN", "/nonexistent/qemu-system-x86_64");
        let dir = std::env::temp_dir().join(format!("dg-nohost-{}", std::process::id()));
        std::fs::create_dir_all(&dir).unwrap();
        let disk = dir.join("d.qcow2");
        qemu::create_qcow2(&disk, 1 << 30).unwrap();
        let g = DraupnirGuest {
            root: dir.clone(),
            ..Default::default()
        };
        let mut s = GuestSpec::new("x", disk.display().to_string());
        s.medium = None;
        s.cdrom_first = false;
        match g.run(&s).outcome {
            GuestOutcome::Refused { clause, .. } => assert_eq!(clause, "kvm-host"),
            other => panic!("{other}"),
        }
        std::env::remove_var("DRAUPNIR_QEMU_BIN");
        let _ = std::fs::remove_dir_all(&dir);
    }
}

#[cfg(test)]
mod template_tests {
    use super::*;

    /// ★ The overlay names its backing file and format where QEMU reads them:
    /// backing_file_offset/size in the header, the `qcow2` format extension
    /// right after the header, and a virtual size no smaller than either the
    /// ask or the image.
    #[test]
    fn an_overlay_points_at_its_image() {
        let dir = std::env::temp_dir().join(format!("overlay-test-{}", std::process::id()));
        std::fs::create_dir_all(&dir).unwrap();
        let image = dir.join("image.qcow2");
        qemu::create_qcow2(&image, 3 << 30).unwrap();
        let disk = dir.join("disk.qcow2");
        create_overlay(&disk, 25 << 30, &image).unwrap();
        let b = std::fs::read(&disk).unwrap();
        let off = u64::from_be_bytes(b[8..16].try_into().unwrap()) as usize;
        let len = u32::from_be_bytes(b[16..20].try_into().unwrap()) as usize;
        assert_eq!(std::str::from_utf8(&b[off..off + len]).unwrap(), image.canonicalize().unwrap().display().to_string());
        assert_eq!(&b[104..108], &0xe279_2acau32.to_be_bytes(), "the backing-format extension");
        assert_eq!(&b[112..117], b"qcow2");
        assert_eq!(qemu::qcow2_virtual_size(&disk).unwrap(), 25 << 30);
        let small = dir.join("small.qcow2");
        create_overlay(&small, 1 << 30, &image).unwrap();
        assert_eq!(qemu::qcow2_virtual_size(&small).unwrap(), 3 << 30, "never smaller than the image");
        let _ = std::fs::remove_dir_all(&dir);
    }

    /// ★ The NoCloud files say what the create said: the hostname, root let in
    /// with exactly the create's keys, every NIC by DHCP; anything else is 404.
    #[test]
    fn nocloud_serves_the_creates_hostname_and_keys() {
        let m = NoCloudMeta { hostname: "gunnar-front".into(), keys: vec!["ssh-ed25519 AAAA one".into()], utility_mac: "0a:00:00:00:c1:02".into() };
        assert_eq!(nocloud_file("u1", &m, "meta-data").unwrap(), "instance-id: u1\nlocal-hostname: gunnar-front\n");
        let u = nocloud_file("u1", &m, "user-data").unwrap();
        assert!(u.starts_with("#cloud-config\n") && u.contains("disable_root: false") && u.contains("  - ssh-ed25519 AAAA one\n"), "{u}");
        assert!(u.contains("macaddress: \"0a:00:00:00:c1:02\"") && u.contains("dhcp4: true") && u.contains("[netplan, apply]"), "the utility NIC by its API MAC: {u}");
        assert_eq!(nocloud_file("u1", &m, "passwd"), None);
    }
}

/// `--offline` (2026-09-26): no user-mode NIC NATs out, and what a guest may still
/// reach is exactly the relayed loopback ports.
#[cfg(test)]
mod offline {
    use super::*;

    fn o() -> Offline {
        Offline { relay: PathBuf::from("/x/mock-upcloud-kvm"), ports: vec![18453] }
    }

    #[test]
    fn every_user_mode_nic_is_restricted_and_only_eth0_carries_the_relays() {
        let n0 = offline_netdev("user,id=n0,hostfwd=tcp:127.0.0.1:4000-:2222,dns=10.0.2.99", &o(), &[40123], true);
        assert!(n0.starts_with("user,id=n0,hostfwd=tcp:127.0.0.1:4000-:2222,dns=10.0.2.99,restrict=on,"), "{n0}");
        assert!(n0.contains(",guestfwd=tcp:10.0.2.100:18453-cmd:/x/mock-upcloud-kvm --relay 127.0.0.1:18453"), "{n0}");
        assert!(n0.contains(",guestfwd=tcp:10.0.2.100:40123-cmd:/x/mock-upcloud-kvm --relay 127.0.0.1:40123"), "the template's NoCloud: {n0}");
        assert_eq!(n0.matches("guestfwd=").count(), 2);
        let n1 = offline_netdev("user,id=n1,net=10.0.3.0/24,dns=10.0.3.99", &o(), &[40123], true);
        assert_eq!(n1, "user,id=n1,net=10.0.3.0/24,dns=10.0.3.99,restrict=on", "eth1 is restricted and relays nothing");
        let fabric = "dgram,id=n1,remote.type=inet,remote.host=239.1.2.3,remote.port=2000";
        assert_eq!(offline_netdev(fabric, &o(), &[], true), fabric, "the utility fabric never NATs; untouched");
        // A medium boot: the relays, and NO restrict (UDP hostfwd replies must pass).
        let m0 = offline_netdev("user,id=n0,hostfwd=udp:127.80.99.12:51821-:51821", &o(), &[], false);
        assert_eq!(m0, "user,id=n0,hostfwd=udp:127.80.99.12:51821-:51821,guestfwd=tcp:10.0.2.100:18453-cmd:/x/mock-upcloud-kvm --relay 127.0.0.1:18453");
        assert_eq!(offline_netdev("-device", &o(), &[], true), "-device");
        assert!(template_seal(true, false, false), "the Ubuntu template boot is sealed");
        assert!(!template_seal(true, true, false), "a medium boot on a template clone is not");
        assert!(!template_seal(true, false, true), "nor the installed disk after it");
        assert!(!template_seal(false, false, false), "a blank disk laid for a medium is not a template");
    }

    #[test]
    fn the_relay_carries_both_ways_and_refuses_anything_but_loopback() {
        use std::io::{Read as _, Write as _};
        let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
        let at = l.local_addr().unwrap().to_string();
        std::thread::spawn(move || {
            let (mut s, _) = l.accept().unwrap();
            let mut got = [0u8; 4];
            s.read_exact(&mut got).unwrap();
            assert_eq!(&got, b"ping");
            s.write_all(b"pong").unwrap();
        });
        let mut out = Vec::new();
        relay(std::io::Cursor::new(b"ping".to_vec()), &mut out, &at).unwrap();
        assert_eq!(out, b"pong");
        assert!(relay(std::io::Cursor::new(Vec::new()), Vec::new(), "185.125.188.55:443").unwrap_err().contains("loopback only"));
    }
}

/// `--lan-publish` (on-prem, 2026-09-27): the named server's ports on the LAN,
/// nobody else's, never on loopback.
#[cfg(test)]
mod lan_publish {
    use super::*;

    #[test]
    fn only_the_named_server_is_published_on_the_lan() {
        let lan = parse_lan_publish("gunnar-front-test3=192.168.1.20:8080-80,192.168.1.20:8443-443,0.0.0.0:2222-2222").unwrap();
        assert_eq!(lan.len(), 3);
        assert_eq!(
            lan_forwards(&lan, "gunnar-front-test3"),
            vec!["tcp:192.168.1.20:8080-:80", "tcp:192.168.1.20:8443-:443", "tcp:0.0.0.0:2222-:2222"]
        );
        assert!(lan_forwards(&lan, "gunnar-appliance-test3").is_empty(), "main is never on the LAN");
        assert!(lan_forwards(&[], "gunnar-front-test3").is_empty(), "no flag: nothing beyond loopback");
    }

    #[test]
    fn a_bad_publish_is_refused_by_name() {
        for (bad, why) in [
            ("192.168.1.20:8080-80", "no hostname"),
            ("=192.168.1.20:8080-80", "empty hostname"),
            ("front=127.0.0.1:8080-80", "loopback"),
            ("front=192.168.1.20:8080", "no guest port"),
            ("front=lan:8080-80", "not an address"),
            ("front=192.168.1.20:x-80", "not a port"),
            ("front=", "nothing"),
        ] {
            assert!(parse_lan_publish(bad).is_err(), "{bad} ({why}) was accepted");
        }
    }
}