Add integration tests based on VM

This commit is contained in:
2026-10-26 22:34:00 +02:00
parent f4a6540df9
commit 7d0f006601
22 changed files with 3826 additions and 69 deletions
+15 -13
View File
@@ -97,22 +97,24 @@ The host cannot run this build directly because its musl target is absent.
The digest-pinned official Rust 1.97.1 Alpine builder command in
[TESTING.md](TESTING.md) built the library test harness on 2026-10-23. `file`
identified a static PIE and `readelf -l` showed no interpreter; the ignored
readiness selector then passed 1/1 in the pinned Alpine runtime using a
read-only bind of the nonsecret binary. The selected-daemon M8.4 Testcontainers
smoke later copied the current binary into a fresh server container and passed
the test-only Create/Commit/first delivery/fixture Apply sequence with real
runtime observation. M8.5 traffic and remaining lifecycle scenarios remain
pending. The readiness wrapper
invokes the harness as
readiness selector passed 1/1 in the pinned Alpine runtime using a read-only
bind of the nonsecret binary. The selected-daemon M8.4 Testcontainers smoke
copied the current binary into a fresh server container and passed the
test-only Create/Commit/first delivery/fixture Apply sequence with real runtime
observation. M8.5 and M8.7 subsequently completed and M8 was accepted for the
documented IPv4 scope. The current integrated host gates and three fresh
selected-daemon lifecycle runs are recorded in
[evidence/milestone-8.md](evidence/milestone-8.md). The runs covered IPv4
traffic, deletion, restore, import/reissue and mandatory-backup failure. The
fixture Apply callback is not a systemd job. IPv6 capability/traffic and real
systemd/D-Bus remain unverified. The readiness wrapper invokes the harness as
`PATH --ignored --exact container_driver::alpine_readiness --test-threads=1`.
Cargo printed the executable under
`/tmp/wga-m8-build-target/x86_64-unknown-linux-musl/debug/deps/wireguard_admin-<hash>`
for the builder command. Discover the current hash from each build; the driver
must be recopied after code changes. A target name or successful compilation
alone is not Alpine runtime evidence. Shell-tool readiness, Rust harness
readiness and the M8.4 Application smoke passed on the selected daemon. No
client tunnel traffic or systemd job has been tested. Musl readiness is a
milestone 8 prerequisite, not a milestone 1 acceptance gate.
for the builder command. Discover the current hash from each build; rebuild the
driver after relevant code changes. A target name or successful compilation
alone is not Alpine runtime evidence. Musl readiness is an M8 fixture build
prerequisite; it is not a milestone 1 acceptance gate.
## Observed preparation state
+150
View File
@@ -0,0 +1,150 @@
# Operator setup, backup and recovery
This guide describes the supported single-interface installation and recovery
boundaries. Use it only after qualifying the application for the target host;
the current evidence and gaps are listed in
[`RELEASE_READINESS.md`](RELEASE_READINESS.md). Do not trial the application on
a production service as a substitute for the pending disposable systemd VM
qualification.
## Installation prerequisites and placement
The application manages one already-configured Linux WireGuard interface and
its existing systemd service. The host must provide working WireGuard kernel
support, systemd and its system D-Bus, plus the required service setup (such as
`wg-quick` when the selected unit uses it). Version 1 does not provision a
server, install a service, or manage multiple interfaces. Do not mount the
development host's D-Bus into a test environment.
Build with the pinned Rust 1.97.1 toolchain and locked dependencies. Before
installing an artifact, verify its target and dynamic library requirements
against the chosen Linux distribution. The current x86-64 GNU release candidate
is host-assessed only; it has not been started in a guest. Keep the executable
and `wireguard-admin.toml` together in a dedicated root-owned directory that
ordinary users cannot modify. Keep the executable outside `PATH`; no installer,
service unit, or automatic privilege wrapper is supplied.
Copy [`../config/wireguard-admin.example.toml`](../config/wireguard-admin.example.toml)
to `wireguard-admin.toml` beside the executable and edit it for the existing
interface, server configuration, client source directory, address pools,
endpoint, DNS, systemd unit, and timeout. The server config path and interface
name are installation invariants. Relative paths resolve beside the executable,
not from the shell's working directory. By default working data is
`Backup/data` and archives are `Backup/archives`, also beside the executable.
Test the resolved paths before running management. Preserve the existing
configuration and imported client files; import is read-only and anomaly
reports require operator decisions.
Restrict the settings file, server configuration, vault, internal state, and
backup tree to root. Existing server configuration and vault files must already
be mode `0600` or stricter when opened. Keep private directories at `0700` and
secret-bearing files at `0600`; the application creates new owned outputs with
these modes but does not silently chmod an unsafe existing server/vault file.
Do not make the executable or settings writable by unprivileged users. The
application requires root for management; `--help` and `--version` are
unprivileged.
## Initial vault, import and delivery
At initial setup, import scans the configured source directory without changing
or deleting its original files. Review every reported match, mismatch, invalid
file, or server-only/client-only anomaly. The server peer set remains
authoritative; do not treat an imported file as permission to rewrite server
settings.
Choose and protect the vault password during initial vault creation. The
password protects the separate vault secret key; it does not encrypt the
server configuration, original client files, or backup archives. Losing it
prevents future reissue of stored client private keys. A backup restores the
encrypted vault but cannot recover a lost password. Store credentials under
your organization's separate secret-custody process; never place the password
in settings, a command line, logs, or evidence.
For a new client, Commit first, then complete first file delivery in the same
application session. The initial file is mandatory and can be retried at a
different destination after a publication failure while that creation session
is still active. Optional QR output represents the same configuration and is
available only after file publication. The delivered file is plaintext
WireGuard configuration, mode `0600`; treat terminal QR output as a disclosure.
If publication reports uncertain output or cleanup is required, follow the
exact manual-cleanup instruction before retrying; do not assume an error means
no plaintext file was created.
Until the saved peer set has been explicitly applied and verified, treat the
new client's access as inactive. After that session ends, each reissue requires
the vault password for that client and reads installed saved state. The
application does not use retained original plaintext files as a password-free
fallback. Overwriting a selected output file requires confirmation.
## Commit, backups, restore and restart
Keep the three states distinct:
1. **Draft** is the current session's proposed edit. Cancel preserves it in
that session; exiting does not save it.
2. **Saved** is what Commit installed on disk. Commit creates a mandatory
pre-Commit backup, then saves settings, server configuration, internal state
and encrypted vault. Commit never reloads, syncs, or restarts WireGuard.
3. **Applied** is saved state verified against the running interface after an
explicit restart. A historical verification marker after reopen is not a
live observation.
Every Commit, restore and restart first creates a complete mandatory backup.
If that backup fails, the protected operation stops before changing saved files
or submitting a restart. Backups are uncompressed archives, not encrypted
archives. They can contain the server's private configuration and encrypted
vault records. Restrict them to root, include them in the site's protected
backup custody, and remember that a backup beside the executable is lost with
that disk. The application has no automatic retention or remote-copy feature.
Deleting a client from the current vault does not erase old backups.
Restore is a previewed, confirmed full replacement of saved state. It restores
the settings and opaque vault without a vault password, and it does not change
the running interface. A restored archive's old verification marker is
historical; runtime state becomes unknown. To apply the restored state, start a
separate explicit restart and inspect its result. Never describe restore or
Commit as a VPN rollback.
Before confirming restart, review the configured interface/service and the
saved-versus-applied differences. The application creates a pre-restart backup,
submits the configured systemd job, waits for its terminal result, compares the
complete observed peer and route sets, then journals verification state. A
successful systemd job alone does not establish that the expected peers are
active. A pre-restart backup captures saved files and is not necessarily a
known-good working VPN state. Deleted-client addresses remain reserved until
verification and its saved-state write both succeed.
If restart reports failure, timeout, a runtime mismatch, or a persistence
failure, read the stage and whether submission occurred or may still complete.
A timeout does not cancel a submitted job. Do not immediately issue repeated
restarts while a submitted job may still be running. No automatic restart,
retry, VPN rollback, or reservation release occurs on these failure paths.
Preserve the pre-restart backup and inspect the service and configured
interface through the host's normal, independently controlled administration
path. Do not infer success from log text alone. When the application remains
usable, select a known-good archive through its preview/confirmation flow to
restore saved files, then decide separately whether to restart and verify them.
## Saved-file journal recovery
The persistence journal recovers interrupted file replacement; it is not
runtime rollback. On reopen, an uncommitted prepared journal rolls saved files
back to the recorded prior bytes, while a committed journal is cleaned up. If
recovery or cleanup fails, the application returns `RecoveryRequired` and
rejects mutations. Correct the underlying storage problem (for example,
available space or directory access), preserve the journal, and retry opening
the application so its recovery can run. Do not delete, hand-edit, or rename
journal files to force startup.
If a journal remains invalid or recovery continues to fail, stop making
changes. Preserve the complete data directory and operation backups for a
careful, separately reviewed recovery. A known-good archive is the documented
saved-state recovery source when the application can safely preview and restore
it; it does not repair the runtime automatically. After saved state is recovered,
inspect the live interface and use a separate explicit restart only when its
target is understood. If the application cannot open to perform a safe
previewed restore, do not improvise by copying partial archive members over
installation paths.
Never remove old archives or original client files as an automatic response to
an error. Keep secret-bearing files and working backups out of source control,
support tickets, screenshots, logs, and evidence reports.
+85
View File
@@ -0,0 +1,85 @@
# Release readiness and qualification matrix
This is a scope-qualified M9.4 record, not a release approval. M1–M8 are
accepted in [`PLANS.md`](../PLANS.md); M9 real-system acceptance remains
incomplete because no explicitly selected disposable systemd VM is available.
No production host, VPN, service, key, or configuration was accessed. The
operator procedure is in [`OPERATIONS.md`](OPERATIONS.md).
## Current host release candidate
The following native artifact was built from the current Rust sources with the
pinned compiler and locked dependency set. Its `--help` and `--version` paths
were executed on the development host. This assessment does not establish
startup in a guest or supported compatibility with another distribution.
| Property | Observed value |
| --- | --- |
| Source Git HEAD | `f4a6540df9621c9c5e5d1430243f92e749356a59` |
| Working-tree state at build | Pre-existing dirty documentation only: `PLANS.md` modified and `docs/VM_ACCEPTANCE.md` untracked; no source, manifest or lockfile changes |
| Relevant input fingerprint | SHA-256 `02ace329ca2ed17aa0b47112a3fcaa090b7d9a7eaa334f293aea9792e35e668a` over sorted `src/`, `Cargo.toml`, and `Cargo.lock` file hashes |
| Compiler and target | rustc 1.97.1, host `x86_64-unknown-linux-gnu`; Cargo 1.97.1 |
| Build command | `cargo build --locked --release` — exit 0 |
| Artifact | `target/release/wireguard-admin`, x86-64 ELF PIE, dynamically linked |
| Artifact SHA-256 | `d52d43cd02332352ffbd09c97e3b8095a710359128260c1286006e1c76c1b283` |
| Runtime requirements observed | ELF interpreter `/lib64/ld-linux-x86-64.so.2`; `libc.so.6` (GLIBC symbols through `GLIBC_2.39`) and `libgcc_s.so.1`; `ldd` resolved these on the development host |
| Artifact smoke | `./target/release/wireguard-admin --help` and `--version` — both exit 0; no settings, terminal, root management, service or interface exercised |
Commands used to inspect the artifact were `sha256sum target/release/wireguard-admin`,
`file target/release/wireguard-admin`, `readelf -h`, `readelf -l`, `readelf -d`,
`readelf --version-info`, and `ldd target/release/wireguard-admin`. The linked
GLIBC requirement is an observed lower symbol-version bound, not proof that a
distribution satisfying it is otherwise compatible. No installer, package,
signature, reproducible-build claim, or supported distribution baseline is
provided. This native GNU artifact is not an Alpine/musl artifact.
The source-input fingerprint was calculated from the repository root with:
```sh
rg --files src Cargo.toml Cargo.lock | sort | xargs sha256sum | sha256sum
```
## Specification-to-evidence matrix
“Accepted” refers to the repository's existing milestone result. It does not
mean every production platform or dual-stack behavior has been qualified. For
M1–M7, the concise milestone scope, review outcome and commands are in the
corresponding accepted sections of [`PLANS.md`](../PLANS.md). M8's detailed
source, artifact, run and scope record is
[`evidence/milestone-8.md`](evidence/milestone-8.md).
| Requirement area | Existing evidence | Qualification and limit |
| --- | --- | --- |
| Settings validation, executable-relative paths, draft/saved/applied model | M1 accepted record in `PLANS.md`; M7 startup/workflow record there | Synthetic/unit and TUI workflow coverage; no guest startup or production configuration. |
| Server configuration authority, client identity by public key, byte preservation and metadata | M2 accepted record in `PLANS.md`; specification in `SPEC.md` | Fixture/behavioral evidence for preserving non-client bytes and comments; no real server file was used. |
| Secret-safe keys, encrypted vault, read-only imports and originals | M3 accepted record in `PLANS.md`; M8 import/reissue scenario in `evidence/milestone-8.md` | Synthetic import plus accepted Alpine lifecycle scenario; source originals were fixture files and retained. Vault password loss remains unrecoverable by design. |
| Backups, mandatory backup boundary, journaled Commit, recovery and previewed restore | M4 accepted record in `PLANS.md`; M8 restore and backup-failure runs in `evidence/milestone-8.md` | Saved-state operations and test-fixture behavior covered. Restore did not apply runtime changes; real-VM manual recovery remains unverified. Archives are not encrypted. |
| Read-only runtime observations, restart orchestration, deadline and failure reporting | M5 accepted record in `PLANS.md`; runtime section of `docs/RUNTIME.md`; M8 Rustix observations | Fake-adapter and real Alpine WireGuard reader/IPv4 observations exist. No actual systemd/D-Bus job or `Application::restart` against real systemd has been run. |
| Delivery, first issue, password-per-action reissue, QR and private output | M6 accepted record and M7 synthetic PTY/workflow records in `PLANS.md`; `docs/DELIVERY.md` | Synthetic behavior covers file publication/reissue and QR rendering/terminal handling. No user device scan or actual client device qualification is recorded. |
| Native TUI, root/startup boundary and exit/terminal cleanup | M7 accepted record in `PLANS.md`, including synthetic PTY and workflow checks | Not a real guest root/non-root startup smoke. The release binary was only exercised for help/version on the build host. |
| Alpine real WireGuard lifecycle and IPv4 traffic | M8.7 in `evidence/milestone-8.md` | Accepted M8 used the pinned Alpine 3.22.7 fixture and selected disposable Docker daemon; repeated fresh integrated IPv4 scenarios passed on the recorded source fingerprint. Fixture Apply is a test-only callback, not systemd evidence. |
| Dual-stack behavior | M1–M7 synthetic client/configuration tests; M8 scope declaration | Production IPv4+IPv6 creation/full-tunnel requirements remain in force. Real IPv6 capability and traffic were excluded from M8 and are unverified; an IPv4 result does not qualify IPv6. |
| Real systemd job, alias/fast completion, failure/timeout, verified apply, restore and release-worker startup | No M9 VM result exists; `docs/VM_ACCEPTANCE.md` is a reviewed safety/scenario contract | **Unverified / M9 incomplete.** No selected disposable guest or VM run. Do not infer this from fake service adapters, Alpine or the host `--help` check. |
| Native artifact identity and dynamic dependencies | This document's host release-candidate record | Host-only assessment. Guest compatibility and startup are unverified; no universal Linux support claim. |
## Qualification status and next action
- M1–M8 remain accepted at their recorded scope. Later M9 documentation does
not change or broaden their evidence.
- M9.1 has a reviewed, non-runnable VM safety/scenario contract. M9.2–M9.3
real-system work and M9.5–M9.6 review/acceptance remain incomplete.
- The release candidate was built and its ELF dependencies inspected on the
host, but has not been copied to or started in a guest. Actual guest
architecture/distribution compatibility, root management startup,
systemd/D-Bus behavior and service recovery are unverified.
- Real IPv6 capability and tunnel traffic are unverified. No M8 IPv4 or
synthetic dual-stack test is a substitute for that qualification.
- No release, installation, production action, Git commit, or deployment is
authorized by this report.
Next action: select and explicitly identify a disposable VM/guest satisfying
[`VM_ACCEPTANCE.md`](VM_ACCEPTANCE.md), then complete its target guard and
real-system scenarios before independent M9 review. Keep systemd/guest results
separate from the existing Alpine and synthetic evidence. Any final release
statement must retain the real IPv6 qualification gap unless that coverage is
explicitly authorized and completed.
+12 -7
View File
@@ -138,11 +138,14 @@ the runtime or service adapters.
Application unit tests use private fake adapters and an injected private
monotonic clock. Public integration tests exercise cancellation, preview,
restore, and saved-setting selection without confirming a host restart. These
are synthetic fixture results. The Linux transport has not been validated
against a real interface; that check belongs to the isolated Alpine suite at
M8. The real zbus/systemd path and end-to-end restart acceptance require the
disposable systemd VM at M9. The TUI and management startup wiring are not
implemented.
are synthetic fixture results. M7 implemented the TUI and management startup
wiring, with synthetic workflow and PTY coverage. M8 also validated the Linux
Rustix transport against a real WireGuard interface in the isolated Alpine
fixture, including IPv4 peer, route, handshake, and counter observations (see
[M8 evidence](evidence/milestone-8.md)). That fixture does not exercise the
real zbus/systemd path: systemd/D-Bus behavior and end-to-end restart acceptance
still require the disposable VM at M9. Real IPv6 capability and traffic remain
unverified.
## Bounded Linux transport
@@ -164,5 +167,7 @@ errors, timeouts, and limit excess remain separate, redacted categories.
Receive buffers are zeroized on every return path because device replies may
include private and preshared key attributes, although the decoder borrows and
does not retain those attributes. Runtime observation does not itself perform
saved-peer comparison or mark state applied. Real-interface behavior remains
subject to the isolated Alpine integration milestone.
saved-peer comparison or mark state applied. M8 exercised this Rustix reader
against a real Alpine WireGuard interface for IPv4 observations; real IPv6
capability and traffic remain unverified. See [M8 evidence](evidence/milestone-8.md)
for the accepted scope.
+392
View File
@@ -0,0 +1,392 @@
# Disposable systemd VM acceptance contract
Status: Milestone 9 is paused at the user's request. The exact disposable VM
was powered off after a guarded first start and an installer progress screen;
read-only `list runningvms` was empty afterward. Debian installation completion,
guest provisioning, reset, cleanup, and systemd acceptance are unverified.
This contract supplements [Testing and Acceptance](TESTING.md) and the M9
criteria in [PLANS.md](../PLANS.md). Product behavior is governed by
[SPEC.md](SPEC.md), [Runtime observation and verification](RUNTIME.md), and
[ADRs 0001–0003](adr/).
## Selected target
The selected target is exactly one new VirtualBox VM:
| Field | Required value |
| --- | --- |
| Provider | Oracle VirtualBox, local `VBoxManage` 7.2.8_Gentoo |
| VM name | `wga-m9-debian137-20261025` |
| Guest base | Official Debian 13.7.0 (Trixie) amd64 netinst ISO |
| ISO path | `/media/data/wm/Virtualbox/wga-m9-lab/debian-13.7.0-amd64-netinst.iso` |
| ISO SHA-256 | `a7ef94ac2fb9a7fec454552abd629b7cc9d5155c886165a45649f5ce6167e355` |
| VirtualBox OS type | `Debian13_64` |
| Lab root | `/media/data/wm/Virtualbox/wga-m9-lab` (owner `sasa`, mode `0700`) |
| Networking | One NAT NIC only, no forwarding; all other NICs disabled |
| VM UUID | `b14c850b-a21e-4a56-acca-785590f0c4ac`, pinned in the mode-`0600` identity manifest |
| Guest marker | `/etc/wga-m9-lab-marker`, exact content `wga-m9-debian137-20261025` after provisioning |
The ISO is present, has the stated SHA-256, and was obtained from Debian's
[official amd64 netinst directory](https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/).
VBoxManage command forms were checked against the local 7.2.8 help and Oracle's
[VirtualBox 7.2 VBoxManage manual](https://docs.oracle.com/en/virtualization/virtualbox/7.2/user/vboxmanage.html).
Read-only `VBoxManage unattended detect --iso=... --machine-readable` reported
`OSTypeId=Debian13_64`, `OSVersion=13.7.0 Trixie`, and
`IsInstallSupported=on`, but then exited 1 with `NS_ERROR_NOT_IMPLEMENTED`.
The later bounded `unattended install --dry-run --start-vm=none` succeeded for
this exact VM with private synthetic password files, a fully qualified `.invalid`
hostname, and the local Guest Additions ISO. The dry run made no VM changes;
actual unattended preparation and guest boot remain unverified. VirtualBox's
dry-run output contains plaintext password fields, so the wrapper suppressed
them and displayed only redacted output. Any actual unattended step needs a
reviewed exact-target procedure and post-prepare media inspection before start.
The approved read-only inventory outside the sandbox found `/dev/vboxdrv`
available and `VBoxManage` 7.2.8 working. It initially listed Windows 7 and
Windows 11 VMs; neither is a test target or was modified. The create-only run
registered only the new Debian target and stopped before configuration. The
default sandbox does not expose the host driver/device, so host inventory and
later VBoxManage operations must use the approved outside-sandbox path. The
guard matches the exact new name/UUID and does not inspect existing VMs'
per-VM files.
## Guarded host operations
[`tests/vm/guard.py`](../tests/vm/guard.py) is a narrow standard-library Python
guard, not a general VirtualBox wrapper. Its default `check`, `start`, and
`authorize-guest` actions are outside Cargo's default test targets. The guard:
- Requires the exact lab root to be a real directory owned by the current user
with mode `0700`, and `target-identity.json` to be a regular current-user file
with mode `0600`.
- Requires the manifest to pin the exact VM name, current owner UID, one valid
UUID, and host marker `<VM name>:<UUID>`.
- Uses `VBoxManage list vms` to require that exact name occur once with the pinned
UUID, then `showvminfo <UUID> --machinereadable` to compare UUID/name/marker.
- Requires the VM configuration and its one VDI to reside below the lab root,
the VDI to be named exactly `wga-m9-debian137-20261025.vdi`, and any mounted
installer ISO to be the pinned Debian ISO in that root.
- Requires a complete inventory of all eight NIC slots, with only NIC 1 in NAT
mode. NAT forwarding and all VirtualBox shared folders are refused.
- Performs every identity, path, and isolation check before `startvm`. A missing,
duplicate, malformed, or mismatched identity fails closed. The only start
target is the pinned UUID, in the GUI frontend for console installation.
- Before guest authorization, repeats all host checks, requires a current-user
password file with mode `0600`, and reads only the guest marker via
`VBoxManage guestcontrol ... run /usr/bin/cat` with `--wait-stdout` and a
15-second guest process timeout. Host stdout/stderr capture is also capped at
1 MiB while the process runs. The password is supplied using `--passwordfile`;
it is never placed in argv. Marker output is compared exactly and is not
echoed. This read is unprivileged inside the guest.
The host marker is a pre-install bootstrap identity: it binds the created
VirtualBox object and protected manifest before the first start. It is not proof
of guest identity. The guest marker is installed only after Debian setup and is
checked separately after boot. Do not perform root provisioning, run application
acceptance, or access the guest system bus until `authorize-guest` succeeds for
the exact UUID. The guest marker check is deliberately read-only; it does not
grant the caller a general guest command runner.
The exact current focused checks are:
```sh
python3 -m unittest discover -s tests/vm -p 'test_*.py' -v
python3 -m py_compile tests/vm/guard.py tests/vm/test_guard.py
git diff --check
```
They use a temporary synthetic VM inventory and fake VBoxManage responses. They
do not call VBoxManage, start a guest, contact a host service, or read any
registered VM configuration. The behavioral cases cover normal exact identity,
missing/duplicate/wrong UUID, wrong name, manifest/root permissions, storage
escape, NIC mode and incomplete inventory, NAT forwarding, shared folders,
host/guest marker mismatch, no `startvm` on refusal, and exact UUID start scope.
The guard supports these later commands once the selected VM has been created,
its manifest and marker have been recorded, and the coordinator has reviewed the
mutation step:
```sh
python3 tests/vm/guard.py check
python3 tests/vm/guard.py start
python3 tests/vm/guard.py authorize-guest --password-file /path/to/private/guest-password
```
The placeholder password path above must name a private file owned by the
current user with mode `0600`. Do not put a password value in the command line,
environment, terminal history, or output. The `start` command is an implemented
guarded operation but has not been run. Running it requires the approved
outside-sandbox path because the default sandbox does not expose the host
VirtualBox device.
## Create-only procedure (ran once; do not rerun)
[`tests/vm/create.py`](../tests/vm/create.py) exposes only the fixed create
operation; it accepts no target, path, or action override. After confirming the
lab root owner/mode, exact ISO digest, absence of both the identity manifest and
the exact target VM path (using `lstat`, so dangling symlinks also count as
occupied), and a well-formed VirtualBox inventory without the selected name, it
issues bounded VBoxManage calls to:
1. Register `wga-m9-debian137-20261025` as `Debian13_64` below the lab root.
2. Configure 1024 MiB RAM, one CPU, NIC 1 in NAT mode, NICs 2–8 disabled, audio
disabled, USB disabled, and DVD then disk boot order.
3. Create one dynamically allocated standard 8192 MiB VDI, attach it on SATA
port 0, and attach the pinned Debian ISO on SATA port 1.
4. Write the actual VM UUID and `<name>:<UUID>` host marker in
`target-identity.json` with mode `0600`, set the same marker as the VirtualBox
description, then run the existing read-only host guard.
Run this exact command only after coordinator review of the current host state
and the scoped mutations:
```sh
python3 tests/vm/create.py
```
The command must run through the approved outside-sandbox path because the
default sandbox cannot access the VirtualBox host driver. Its effects are VM
registration, storage/network/device/boot configuration, ISO attachment, and
the protected host identity manifest. It does not start the VM, install Debian,
use guest control, create snapshots, or delete/clean up resources. VBoxManage
calls have 30-second timeouts and 1 MiB per-stream output limits. Errors omit
VBoxManage output; after registration they report the exact target name and
known UUID. A failed procedure retains any partial target and its identity for
inspection; it performs no automatic rollback or broad cleanup. The final guard
must pass before the script reports success. The first run registered the UUID
above, then stopped when strict parsing rejected VirtualBox's indented
`rec_screen0` recording marker. The parser was narrowly repaired and
independently re-reviewed. The target path is now occupied, so `create.py` must
not be rerun; use the reviewed resume procedure below.
## Resume the registered partial target (ran once; do not rerun)
The earlier create attempt registered the selected target before stopping at
the initial machine-readable parse. The exact retained target is
`wga-m9-debian137-20261025` with UUID
`b14c850b-a21e-4a56-acca-785590f0c4ac`; it is powered off and has only its
`.vbox` configuration file. Its observed NIC inventory was NIC 1 in NAT with
NICs 2–8 disabled. It had no identity manifest or VDI. That prior state was
the sole input accepted by [`tests/vm/resume.py`](../tests/vm/resume.py).
The resume-only procedure is fixed to that UUID and lab root. Before its first
mutation it checks the private root, exact pinned ISO path and SHA-256, absent
manifest and target VDI, unique registered name/UUID identity, exact `.vbox`
path, powered-off state, empty host description, default memory/CPU settings,
the observed NIC inventory (NIC 1 in NAT and NICs 2–8 disabled), no attached
media or storage controller, no shared folders, and no NAT forwarding. It
refuses an unexpected adapter or preexisting state.
After preflight it configures memory/CPU, NAT-only networking, disabled audio
and USB, boot order, one standard 8192 MiB VDI, and the pinned ISO; then it
writes the protected host manifest, sets the host marker, and runs the existing
read-only guard. It does not create, start, unregister, delete, or clean up a
VM. A failure after mutation retains the exact target and reports its pinned
UUID for inspection; no rollback is attempted.
The focused command is:
```sh
python3 -m unittest discover -s tests/vm -p 'test_resume.py' -v
```
It uses only a temporary synthetic lab and fake VBoxManage responses. It does
not call VBoxManage or establish host/VM evidence. The implementation command
was `python3 tests/vm/resume.py`; it ran once via the approved outside-sandbox
path. It configured the exact target and wrote the host marker/manifest. Its
final guard call initially refused observed VirtualBox storage metadata;
focused repairs and independent re-review closed those parser and guard
findings. A subsequent read-only `python3 tests/vm/guard.py check` passed
against the actual powered-off target. Do not rerun the resume command on this
occupied state.
Do not infer `unregistervm --delete`, snapshot restore, guest-control, or broad
cleanup commands from the CLI help. No provisioning command is documented
because Debian installation, Guest Additions/management channel, the unprivileged
`wga` account, and guest marker placement have not been validated. No reset or
deletion command is documented because a baseline snapshot and exact owned
resource IDs do not yet exist. Existing registered VMs must remain untouched.
## Unattended preparation (ran once; do not rerun)
[`tests/vm/install_prepare.py`](../tests/vm/install_prepare.py) is pinned to the
VM UUID above and accepts no target, path, credential, or action overrides. It
first checks the current user's `/tmp/wga-m9-private` directory is a real mode
`0700` directory and its nonempty `wga-password` and `root-password` files are
current-user-owned regular mode-`0600` files. These are synthetic install
credentials only; password contents never enter argv or output. It verifies the
pinned Debian ISO and `/usr/share/virtualbox/VBoxGuestAdditions.iso` SHA-256
digests, then calls the existing exact-target read-only guard and additionally
requires the VM to be powered off, with the expected VDI at `SATA-0-0`, the
pinned installer ISO at `SATA-1-0`, and every other reported SATA attachment
slot set to `none` or `emptydrive`. This accepts VirtualBox 7.2.8's real
`showvminfo` inventory (`SATA-2-0` through `SATA-29-0` are `none`) while
rejecting any additional attached media. It refuses before mutation if any
check fails.
The script runs a bounded `VBoxManage unattended install <pinned UUID>` dry run
first with `--iso=<pinned Debian ISO>` and
`--admin-password-file=/tmp/wga-m9-private/root-password`, using the synthetic
`wga` account and password file, `.invalid` FQDN, `en_US`, `UTC`, US country,
minimal package adjustment, and the pinned Guest Additions ISO. The root
credential option is `--admin-password-file`; `--root-password-file` is not a
valid option for the local VBoxManage 7.2.8. Only a successful dry run permits
the same bounded command without `--dry-run`. Both commands request
`--start-vm=none`; the script has no start, cleanup, or retry operation.
VBoxManage output is captured privately because its dry-run output contains
plaintext password fields; errors and status output omit captured text. The
command has a 120-second deadline and 1 MiB per-stream capture limit.
After independent review, the command ran once through the approved
outside-sandbox VBoxManage path:
```sh
python3 tests/vm/install_prepare.py
```
The command exited 0 and left the VM powered off. Read-only `showvminfo`
inspection found the exact VDI at SATA 0 and a VirtualBox-generated auxiliary
`.viso` at SATA 1, both in the private target folder. The generated installer
files are mode `0600`; the target folder is mode `0700`. The normal host guard
refuses the `.viso`, as designed, so the first start requires a separate guard
for this exact installer state and independent review. Do not rerun preparation
on the occupied state. Guest boot, Guest Additions setup, and guest provisioning
are not established by this result or the synthetic tests.
The focused behavioral tests use only temporary synthetic files and a fake
VBoxManage runner:
```sh
python3 -m unittest discover -s tests/vm -p 'test_install_prepare.py' -v
```
## First start of the prepared installer (ran once; VM stopped)
[`tests/vm/install_boot.py`](../tests/vm/install_boot.py) adds a separate,
fixed-target `check`/`start` command for the one powered-off installer state.
The ordinary [`guard.py`](../tests/vm/guard.py) deliberately continues to
reject the generated `.viso`; this installer guard does not loosen that normal
guard. It reuses the strict machine-readable parser and checks the exact
current-user-owned mode-`0600` identity manifest, pinned UUID and matching host
description, exact target name/configuration path, and current-user-owned
mode-`0700` target folder. It requires the VDI at SATA 0 and exactly
`Unattended-b14c850b-a21e-4a56-acca-785590f0c4ac-aux-iso.viso` at SATA 1;
SATA ports 2–29 must all be `none`. The `.viso` and any generated
`isolinux`/`preseed`/`postinstall` files are checked as current-user-owned
mode-`0600` regular files with exactly one hard link. The VM configuration and
VDI are also required to be regular current-user-owned files with exactly one
hard link. The guard never opens or prints installer-file contents.
The VM must remain powered off during preflight, have only NIC 1 in NAT mode,
and have no forwarding rules or shared folders. Symlinked paths, extra media,
unknown attachment metadata, and incomplete or mismatched identity refuse
before start.
The exact commands are:
```sh
python3 tests/vm/install_boot.py check
python3 tests/vm/install_boot.py start
```
`check` only reads the protected manifest, filesystem metadata, and bounded
VirtualBox inventory; it does not change VM state. `start` repeats all checks,
then issues only `VBoxManage startvm b14c850b-a21e-4a56-acca-785590f0c4ac
--type=headless`. It accepts no UUID, root, media, credential, or action
override. Each VBoxManage call uses the existing guard's 15-second deadline and
1 MiB per-stream output cap. Output is not copied into errors. If the start call
fails or times out, the result explicitly says the VM may have started and
requires a separate read-only inspection; there is no retry, cleanup, or
rollback. A successful command means only that VirtualBox accepted the start
request. It does not prove guest boot, Debian installation, or Guest Additions.
Focused fake-runner coverage is:
```sh
python3 -m unittest discover -s tests/vm -p 'test_install_boot.py' -v
```
These tests create synthetic private files and never call VBoxManage. Both
commands ran once against the actual VM after independent review. `check`
passed; `start` returned success; VirtualBox listed only the pinned target as
running. A private console screenshot showed Debian's base-system installer at
6%. The user then stopped Milestone 9, so the exact target was powered off and
read-only `list runningvms` returned no running VMs. Do not restart it without
a new explicit request. Installation completion and guest boot remain
unverified.
After installation, provision only synthetic lab packages,
fixed reviewed service units, synthetic WireGuard keys/configuration, and the
guest-local marker. Keep all server/client data, outputs, and working backups
under a guest-only root-owned `0700` directory; secret files are `0600`. The
guest-local marker must be readable by the unprivileged `wga` account for the
read-only marker gate. This work belongs to the next task after target creation
and guard review; it is not claimed as implemented here.
## Isolation and acceptance boundary
- Use synthetic server/client keys, configuration, settings, and vault contents
generated inside the guest. Preserve imported originals. Do not copy or read
production keys, configuration, backups, or credentials.
- The VM may have only one NAT management adapter, with no forwarding rules;
do not use bridged, host-only, internal, NAT-network, USB network, or physical
adapters. NAT-only VM egress still traverses the host's ordinary network; do
not claim host-independent network isolation. No host D-Bus, `/etc`, `/run`,
`/var`, WireGuard interface, service control, or general shared folder may be
mounted or forwarded into the guest.
- A guest system D-Bus and PID 1 must be guest-local. Keep console management
independent of the VPN under test. Tunnel probes target only synthetic
addresses reachable through the test tunnel, never the NAT management path.
- Provision only fixed, reviewed lab units/drop-ins and fixture hooks. Imported
configuration text remains data. Preserve administrator settings and prove
preflight refusal using synthetic fixtures rather than silently rewriting
`SaveConfig`.
- Exercise the public `Application::restart` flow with the real zbus service
adapter and Rustix observer. A test seam may inject persistence failures only
where already established; it may not replace service submission or runtime
observation as evidence of systemd behavior. Alpine fixture apply remains
separate evidence.
- Run the actual release executable separately from any `cfg(test)` driver.
Capture source/dirty identity, compiler/locked dependencies, target, artifact
SHA-256, and dynamic-library requirements for both artifacts. Prove each
executable starts in the guest.
- Keep secrets out of argv, environment variables, image metadata, logs,
transcripts, and evidence. Use bounded typed control, private files/channels,
monotonic timeouts, and bounded output. A timeout after service submission
means it may still complete; it is not cancellation.
- Before every scenario, reset to a recorded clean guest state and verify both
the pinned host identity and guest marker. Afterward, stop only resources
created by that run and verify their exact IDs. If cleanup is uncertain,
preserve IDs and report them; never use broad host cleanup. Preserve the
original failure if cleanup also fails.
## Required scenario evidence
For every run, record UTC start/end, guest and base identities, kernel/systemd/
D-Bus/WireGuard versions, both marker results, source/artifact identities, exact
commands, exit status and bounded redacted outcomes, backup/archive identities,
reset/cleanup result, and the acceptance criteria supported. Keep raw console
output private; evidence must contain no secrets or complete client config.
Record each scenario independently so failures cannot be masked by unrelated
fixture behavior.
| Scenario | Required observable evidence |
| --- | --- |
| Successful apply and fast job | Existing synthetic client has positive tunnel-only IPv4 traffic. Through one `Application`, create and Commit a client; show saved/runtime divergence and that Commit did not submit a service job. Cancel preview with no backup/job/mutation. Confirm restart: pre-restart backup completes first, real zbus job for the resolved primary unit reaches `done`, Rustix observes the expected interface and complete matching peer/routes, and durable verification is written. Verify new tunnel traffic, reopen and show verification is historical. Repeat with fast completion and a configured alias resolving to the primary unit; retain the completion signal. Include application outcomes, not journal inference alone. |
| Deletion and reservations | Establish positive traffic before deleting/Committing. Show the client remains in runtime and its address remains reserved until successful real restart and complete verification. Then show it absent, tunnel-only traffic fails within a bounded probe, durable reservation releases, and old backups remain. |
| Failure and deadline | Using fixed guest-only units/drop-ins, show terminal job failure and submitted-job deadline expiry with bounded results, accurate submitted/may-still-complete status, no automatic retry/rollback, no false Verified state, and retained reservations. Also show unresolved unit fails before submission and backup failure prevents restart. After late completion, explicitly observe and reset the guest; timeout is not cancellation. |
| Job succeeds, verification fails | A dedicated fixed fixture service completes while leaving the interface absent or peers mismatched. Show the real adapter reports verification failure and reservations remain. Separately inject saved-state write failure only through the established persistence seam after real restart/observation; show runtime changed without durable Verified state and `RecoveryRequired` blocks unsafe mutations. |
| Restore and manual recovery | Preview and confirm a known-good archive without vault password. Show precautionary backup, full saved settings/config/vault replacement, and unchanged runtime before explicit restart. Then run real restart/verification and expected tunnel check. Demonstrate documented manual recovery; no hidden rollback. Confirm permissions and reopen behavior. |
| Release executable smoke | In guest console/PTY, prove identified release binary starts from its private directory and settings resolve beside it. Exercise current-client/statistics view and one controlled Commit/restart through the real worker path; confirm exit/cancel restores terminal. Help/version work as non-root without settings; non-root management is rejected. Record output/backup permissions. |
The final M9 evidence must map these observations to M9-A1–A9 and the
specification-to-evidence matrix. State the tested scope, including IPv4 tunnel
coverage, synthetic dual-stack tests, unverified real IPv6 behavior, and
QR/device interoperability limits. A mocked bus, Alpine fixture apply, missing
prerequisite, skipped test, or guest boot alone does not satisfy real systemd
acceptance.
## Next action
None while Milestone 9 is paused by the user. The disposable VM is powered off;
do not resume installation, provision it, or run acceptance without a new
explicit request. No M9-A1–A9 check is complete.
+68
View File
@@ -0,0 +1,68 @@
# Milestone 9 diagnostics
Current execution state and acceptance are in [PLANS.md](../../PLANS.md). This
file records bounded host preparation failures, not systemd acceptance evidence.
All timestamps are UTC; the VM is disposable and contains synthetic data only.
## 2026-10-25T18:28:55Z — M9.1-R8, initial create parse
`python3 tests/vm/create.py` registered only
`wga-m9-debian137-20261025` (UUID
`b14c850b-a21e-4a56-acca-785590f0c4ac`) and stopped before VM configuration.
Cause: actual VirtualBox 7.2.8 machine-readable output contains an indented
`rec_screen0` recording section marker. The strict parser rejected it. Read-only
inventory showed the target powered off with only its `.vbox` file; the existing
Windows VMs were unchanged. A narrow marker parser repair and synthetic test
passed independent re-review. Creation cannot be rerun because the target path
is occupied; a UUID-pinned resume procedure was reviewed instead.
## 2026-10-25T18:35:00Z — M9.1-R5/R8, resume final guard
`python3 tests/vm/resume.py` verified the exact partial target, then configured
the VDI, pinned installer ISO, NAT-only NICs, and private host marker/manifest.
Its final read-only guard refused actual quoted SATA attachment keys and
per-port metadata. `python3 tests/vm/guard.py check` first reported an
unrecognized machine-readable key; after quoted-key parsing it reported an
unrecognized attachment-shaped metadata key. Read-only `showvminfo` identified
`SATA-IsEjected-1-0` among the actual metadata. Focused repairs accepted only
the observed key forms, required the exact SATA controller, and rejected
unrecognized/extra media and a symlinked target folder. Independent re-review
closed the findings; the integrated synthetic VM suite passed 26/26 and the
actual read-only `guard.py check` exited 0. No second resume or VM start occurred.
## 2026-10-25T18:47:20Z — M9.1 unattended prerequisite (recorded)
The first no-change `VBoxManage unattended install ... --dry-run --start-vm=none`
refused a single-label hostname. Repeating with the fully qualified synthetic
hostname `wga-m9-debian137.invalid` exited 0, including a separate dry run with
the local Guest Additions ISO. Both used current-user mode-`0600` private
synthetic password files. VirtualBox prints password values in dry-run output;
the wrapper redacted them before display. These runs created no files or VM
configuration changes. Actual installer preparation, boot, Guest Additions,
and guest systemd remain unverified.
## 2026-10-25T18:53:05Z — M9.1 installer preparation (recorded)
Independent review found that the first prepare-only script omitted the
required `--iso` and used an unsupported root-password option; its media
comparison also rejected VirtualBox's empty SATA ports. M9.1-R9/R10 repairs
passed re-review and 33/33 synthetic VM tests. The corrected
`python3 tests/vm/install_prepare.py` exited 0 outside the sandbox with
password-bearing VirtualBox output suppressed. Read-only `showvminfo` showed the
same pinned VM powered off, NAT-only, with its one VDI at SATA 0 and the private
generated `Unattended-<UUID>-aux-iso.viso` at SATA 1. Generated installer files
were mode `0600` in the mode-`0700` target folder. The normal guard intentionally
refuses `.viso`; first start awaits a separately reviewed installer-state guard.
## 2026-10-26T20:30:15Z — User stopped M9.1
The installer-state guard and start code passed independent review after a
hard-link isolation repair; the full synthetic VM suite passed 44/44. Actual
`python3 tests/vm/install_boot.py check` and `start` exited 0 against only the
pinned target. Read-only `list runningvms` listed only that VM; a private
console screenshot showed Debian base-system installation at 6%. Completion
was not observed. After the user explicitly stopped Milestone 9, the exact VM
was powered off; a subsequent read-only running-VM inventory was empty. Guest
installation, Guest Additions, systemd, WireGuard, and M9 acceptance remain
unverified. No existing Windows VM, production resource, Git commit, or
deployment was touched.
+42
View File
@@ -0,0 +1,42 @@
# TUI navigation repair
Validated on 2026-10-26 at 22:32 +02:00. Bounded maintenance; M9 remains
paused and unaccepted. No production, Alpine or VM execution was performed.
## Behavior and review
List Up/Down previously called the details-opening selection method. The repair
separates highlighting from opening: arrows update selection, Enter opens the
selected peer, and Esc returns to the list. Existing detail/help/message scrolling
is preserved. Unsupported peers remain inspectable without mutation identity.
Independent reviewer NAV-001 identified an initial-highlight mismatch; the repair
treats the renderer's implicit first row as current. Re-review closed NAV-001
with no remaining actionable scoped findings.
Regression coverage exercises initial Down, initial Up wrap, explicit selection,
Enter, Esc, scroll reset and unsupported peer inspection. Focused command
`cargo test --locked tui::terminal::tests::` passed 7/7.
## Integrated checks and identity
Rust/Cargo 1.97.1, native x86_64 GNU host. All commands exited 0:
- `cargo fmt --all -- --check`
- `cargo clippy --locked --all-targets -- -D warnings`
- `cargo test --locked`: 323 passed, 3 explicitly ignored, none failed,
including 2 documentation tests. Ignored real-environment tests were not run.
- `cargo build --locked --release`
- `git diff --check`
Base commit: `f4a6540df9621c9c5e5d1430243f92e749356a59`. Dirty inputs include
the navigation repair plus pre-existing documentation/VM preparation changes;
the latter were preserved. SHA256 of changed Rust files:
- `src/tui/state.rs`: `3b9ea1540c26a372c781a8194cedff74bb56320a698ae28e56ce22f38d159c5d`
- `src/tui/terminal.rs`: `368bf0be11c35202127b7ac5c95f62a312f9de9b464de6e6816cb4b7523cdb1e`
Native artifact `target/release/wireguard-admin` SHA256:
`de5b7efe9f8615fcb13d8779a86451d62e64c3f1da976ba706d6773f8a573d9e`.
This is host build and fixture evidence only; target-server deployment and
interactive SSH confirmation remain unverified. Previous release artifact
identities do not identify this build.