This is the bulk pre-beta cleanup pass. Bumps the workspace to 0.2.0.
Test count is 56 -> 66 (+10), clippy is fully clean across the
workspace (was several dozen warnings).
## New features
**Per-MAC host bindings** (Tinkerbell smee pattern). New
`HostBindings` registry maps a MAC -> preferred boot target, persisted
to <work_dir>/hosts.json. The DHCP reply now embeds `?mac=${mac}` in
the boot.ipxe URL; iPXE substitutes the literal MAC client-side, so
the HTTP layer can short-circuit straight to the bound target instead
of rendering the menu. Reserved menu shortcuts (`_local`, `_gate`,
`_tools_menu`) are valid targets too. New /api/hosts CRUD + a Hosts
tab in the sidebar.
**Prometheus `/metrics`** endpoint. Tiny lock-free implementation —
just AtomicU64s and a Display impl, no `prometheus` / `metrics-rs`
dep. Counters: DHCP replies (per arch label), DHCP declined, TFTP
transfers (per status), TFTP bytes, HTTP requests (per route).
Gauges: ISO count, client count, gate count, gate-imaging, NFS active
mounts, uptime, build info. Plain text exposition format,
text/plain;version=0.0.4 content-type, no auth (all metric values are
non-sensitive counts).
**Light + dark themes**. CSS tokens on `:root` and
`:root[data-theme=light]`, swap by toggle button (top-right) or `T`
hotkey. Persisted in localStorage; pre-paint inline script avoids
dark<->light flash. Light palette designed against the Netbox Labs
reference screenshot — near-white surfaces, soft grey dividers,
accent unchanged for brand consistency. Terminal pane stays dark in
both themes (it's a console, that's the right read).
**Animated SVG logo + forge widget**. New `logo.svg` is a refined
silver/grey anvil. New `anvil-forge.svg` adds rising sparks and a
pulsing underglow via SMIL — pure SVG, no GIF, no JS animation loop.
Used:
- in the **forge progress** widget on Dashboard + Forge Gate, paired
with a `linear-gradient(warn -> accent)` bar with a moving sheen;
goes idle (greyscale, no sheen) at zero imaging load
- in the page-load `<div class=loader>` that replaces the old
"Loading..." text
## Code cleanup pass
`cargo clippy --workspace --all-targets` is now warning-free. Spot
fixes across the tree:
- `format!()`-into-`String` -> `std::fmt::Write::write!`
- manual reverse comparators -> `Reverse`
- `map_or(false, ...)` -> `is_some_and`
- redundant closures -> method references
- `r#"..."#` raw strings without `"` -> `r"..."`
- `std::io::Error::new(Other, ...)` -> `Error::other`
- `as i32` on `c.id()` -> `cast_signed()`
- merged identical match arms
## Windows workflow validation
New integration test synthesizes an ISO9660 with the SOURCES\\BOOT.WIM
sentinel, uploads it, asserts:
1. introspection labels it `windows_pe` with has_boot_wim=true,
2. the boot entry is `BootKind::Wimboot` with all five canonical
files (bootmgr, bootmgr.efi, bcd, boot.sdi, boot.wim),
3. the rendered iPXE script chains wimboot with `initrd --name`
entries for each file, and
4. NO trust-store strings appear in the rendered output: bcdedit,
testsigning, certutil, httpdisk, and test-signed are all
explicitly forbidden as a hard guarantee.
WinPE bootstrap (startnet.cmd) picks up the Bootimus v0.1.58 lessons:
explicit `net start Workstation` before `net use` to avoid the SMB
client lazy-init race, and surfaces errors instead of blind retries.
## Docs
architecture.md gains a "Phase 5" section explaining the host-bindings
+ metrics + theming + Windows-test work, plus a refreshed "deferred
to Phase 6" list (real-hardware integration, autounattend library,
distro profile manifest, WoL trigger, syslog receiver, IPv6).
README updates the status line, the "what it does" list, and adds
the new Hosts/Terminal tab names.
18 KiB
PXEForge architecture
Protocol stack
Client firmware PXE ROM
│
│ DHCPDISCOVER (UDP/67 broadcast, option 60 "PXEClient", option 93 arch)
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ PXEForge │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ DHCP proxy │ │ TFTP server │ │ HTTP server (axum) │ │
│ │ :67, :4011 │ │ :69 │ │ :80 │ │
│ │ (dhcproto) │ │ (custom) │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────────────┘ │
│ │ │ │ │
│ └────────────────────┼─────────────────────┘ │
│ │ │
│ ┌──────────▼────────────┐ │
│ │ IsoStore (on disk) │ │
│ │ + ClientRegistry │ │
│ └───────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
▼ reply with option 60, option 66 tftp-server, option 67 bootfile
Client firmware PXE ROM
│
│ TFTP RRQ: snponly.efi (or undionly.kpxe for BIOS)
▼
Client runs iPXE
│
│ DHCPDISCOVER with option 77 "iPXE"
▼
PXEForge sees user-class "iPXE" → replies with HTTP URL: /boot.ipxe
│
│ HTTP GET /boot.ipxe (iPXE menu, auto-generated from IsoStore)
▼
User picks entry; iPXE chains /boot/<id>.ipxe
│
│ HTTP GET kernel + initrd (or wimboot + WIM files)
▼
Kernel boots with distro-specific args pointing back at /iso/<id>.iso
Crate layout
| Crate | Responsibility |
|---|---|
pxeforge-core |
Shared types: Config, ClientArch, FirmwareClass, ClientRegistry |
pxeforge-ipxe-assets |
Embeds bundled iPXE binaries via rust-embed |
pxeforge-iso-store |
On-disk ISO store, introspection, boot-entry generation |
pxeforge-dhcp-proxy |
UDP listener + dhcproto reply builder; pure decide() unit-testable |
pxeforge-tftp |
RFC 1350 + OACK (blksize / tsize / windowsize). Serves only embedded assets — no filesystem |
pxeforge-http-api |
axum router: web UI, API, iPXE script generation, ISO streaming |
pxeforge-webui |
Single index.html served as static string |
pxeforge (bin) |
Wires everything together, runs the three servers concurrently |
Key decisions and why
DHCP proxy only, not a full DHCP server
Proxy mode (RFC 4578) replies with boot parameters (siaddr, option 66, 67)
but never sets yiaddr — it does not lease IPs. The client merges proxy
replies with its normal DHCP lease from the network's existing DHCP server.
This sidesteps needing CAP_NET_RAW or AF_PACKET. A full DHCP server has
to craft Ethernet frames to a client that doesn't yet have an IP — that
requires raw sockets and elevated privileges. A proxy replies on UDP to a
client that already has an IP (or will shortly, from the other DHCP server),
so plain SOCK_DGRAM is enough.
Two-stage iPXE chain
- Firmware PXE ROM sends DHCPDISCOVER with option 60 =
PXEClient, option 93 = arch. - PXEForge replies with TFTP server + arch-specific iPXE binary
(
undionly.kpxefor Legacy BIOS,snponly.efifor x86_64 UEFI, etc.). - Client TFTPs the iPXE binary and runs it.
- iPXE does its own DHCP, setting option 77 (user-class) to
iPXE. - PXEForge detects the user-class and this time replies with an HTTP URL
in option 67 pointing at
/boot.ipxe. - iPXE fetches and executes that script, which chains the selected OS.
The split is essential because firmware PXE ROMs only speak TFTP; they don't do HTTP. iPXE adds HTTP (and a lot more), and is small enough to fit in the TFTP hop.
TFTP option negotiation is mandatory
Default 512-byte blocks means an ~1 MiB iPXE binary is ~2000 packets. On anything less than pristine wired Ethernet this is unusably slow or outright fails. We negotiate:
blksize(RFC 2348): up to ~1468 bytes for Ethernet MTUtsize(RFC 2349): file size; some PXE ROMs require it presentwindowsize(RFC 7440): 8–16 gives order-of-magnitude throughput gains
Do not touch the client OS trust store
Whatever we do for Windows, we never:
- ship drivers signed with test/development certificates
- instruct users to enable
bcdedit /set testsigning on - install any certificate into the target's root/trust store
iVentoy's httpdisk.sys approach broke this rule. PXEForge doesn't.
Linux ISO boot uses kernel+initrd extraction, not sanboot
Loading a full ISO into RAM via memdisk or sanboot fails:
- Above ~1–2 GiB, RAM emulation is too slow or too large.
- Most modern distros can't find the emulated CD device from the initramfs.
We instead extract vmlinuz + initrd at upload time (well, look up their
locations — we serve them via ISO9660 byte-range lookup, no extraction on
disk) and pass distro-specific kernel args that point the installer back at
the HTTP-served ISO.
No iPXE UX
iPXE is an implementation detail. The web UI only accepts .iso uploads
and shows distros, not .ipxe scripts or boot targets. If someone who
knows iPXE wants to peek, they can curl /boot.ipxe — that's fine. But
the UI never surfaces it.
Unit tests
Run with cargo test --workspace --lib. Current coverage:
ClientArchalias handling (0x0009 → x86_64 UEFI)FirmwareClassclassification (iPXE wins over PXEClient echo)- Raw option-93 extraction from the wire
- TFTP RRQ parsing with options
- HTTP Range header parsing (full, open-ended, suffix, explicit)
- Distro family detection from volume label
- ISO id slugification
Phase 3 hardening
A code-review pass after Phase 2 turned up 16 issues across the protocol stack and container surface. Everything P0/P1/P2 is addressed in this release:
Protocol reliability (review P1 #10/#12):
- TFTP retransmit loop rewritten with explicit window tracking so UEFI SNP
clients no longer hang on files that end mid-window. Covered by 4 new
unit tests (
plan_window_*incrates/tftp). - DHCP proxy now honors the broadcast-flag bit (RFC 2131 §4.1) when choosing unicast vs broadcast reply destination.
Container posture (review P1 #2/#13/#14/#15):
- Multi-arch container (linux/amd64 + linux/arm64) via
buildx. - New entrypoint (
deploy/docker/entrypoint.sh) chowns data dirs as root, then drops to uid 10001 viagosu— fixes the "bind-mount comes up root-owned" problem that breaks ISO upload on standard Docker hosts. /healthzand/readyzsplit from/api/status— readyz fails if no iPXE binaries are bundled, giving K8s probes a real signal.- Startup aborts with a clear error if
PXEFORGE_PUBLIC_IPcan't be auto-detected (no more silent127.0.0.1advertisement). scripts/fetch-ipxe.shfails non-zero if zero binaries download; the Dockerfile uses arch-scoped paths (x86_64-efi/snponly.efietc.).
Windows boot plumbing (new):
pxeforge-iso-store::smb::SmbManagersupervisessmbdon the Windows toggle:start→ spawn + writesmb.conf;reconcile→ SIGHUP on share changes;stop→ SIGTERM.SmbStatesurfaced to the UI for visibility.extract_windows_isoshells out to7z(orbsdtarfallback) to unpack the ISO tree intosmb_dir/<slug>/as the SMB share root.WimPatcher(from Phase 2) is still in place forboot.wiminjection.- Guardrail: flipping the Windows toggle in
/api/settingsis rejected with a 400 ifwimbootisn't bundled.
iVentoy/Bootimus parity (P2):
- ISO sizes in menu labels (
[ 4376 MB]), iVentoy format. Reboot Computer+Exit and continue BIOS bootin Tools menu.- Number-key hotkeys (1..9) on boot entries, letter hotkeys on tools.
- Clients tab cross-joins the gate queue so an operator sees "at gate #2" or "assigned: ubuntu-linux" status inline.
Developer ergonomics (new):
pxeforge seed --from <path>CLI to import ISOs from a directory. Same pipeline as web upload (slug, sha256, introspection, boot-entry).docker-compose.ymlwithpxeforge(host network, real PXE) andpxeforge-dev(published ports, DHCP disabled, for API testing).
API cleanliness:
- All timestamps now serialized as RFC 3339 strings (the
timecrate's default 9-tuple broke browserDateparsing). - Gate poll retains assignment until the operator releases it; if the client's chain fails, it reuses the assignment instead of falling back to the menu.
Phase 4 — UI restructure + remote storage
The web UI was rebuilt around six tabs (Dashboard / Network / Forge Gate / Storage / Terminal / About) inspired by the iVentoy layout the user attached and Netbox Labs's compact-card pattern. The old hierarchical "Monitoring / Content / Configuration" sidebar grouping is gone — every tab is one click from the brand bar.
NFS share manager (crates/iso-store/src/nfs.rs):
- Operators add a remote share via Storage → NFS shares; PXEForge mounts
it under
<work_dir>/nfs/<id>/and walks it for*.isofiles. - Each ISO found is registered with
IsoStore::register_externalusing a newIsoSource::Nfs { mount_id, relative_path }variant. The store resolves these to disk lazily viaset_nfs_root, so adding NFS required exactly one new field onIsoMeta(with a serde default for forward compatibility with oldmeta.jsonfiles). - Versions:
vers=3andvers=4.1only. v3 also getsnolocksince appliances commonly disablelockd. All mounts usesoft,timeo=100so a dead server surfaces as a UI error rather than wedging iPXE. - State persists to
<work_dir>/nfs.json. On startup the manager re-attempts every spec; failures are logged per-mount and surfaced in the UI rather than blocking startup. - Container requirement:
mount.nfsbinary plusCAP_SYS_ADMIN. The default Dockerfile bundlesnfs-common. OpenShift operators must swap to a more permissive SCC or use a CSI driver.
Live log + operator terminal (crates/core/src/log_bus.rs,
crates/http-api/src/{log_stream,terminal}.rs):
- A
LogBusLayerfeeds every tracing event into a 500-line ring buffer plus atokio::sync::broadcastchannel. /api/log/streamis an SSE endpoint that emits the recent buffer followed by live updates. Slow clients see alaggedevent rather than dropping the stream./api/terminalaccepts a single command line and dispatches to a whitelist (status,isos,clients,gate {list,assign,release},nfs {list,mount,unmount,scan},smb {status,start,stop,reload},log {clear,tail}). Output is mirrored onto the LogBus so reading the live tail tells the same story as scrolling the terminal pane.- The whitelist exists deliberately — exposing a raw shell to the web would be an RCE endpoint.
Network tab:
/api/networkexposes auto-detectednic_name,subnet_mask, andgateway(parsed fromip route/ip addrat startup). These are read-only by design — silently changing the public IP on a hot UI would break PXE for every client mid-boot.- The only writable network field is
dns_server, an optional informational hint stored inSettings. PXEForge does not run a DNS server; the field exists so operators don't have to dig out the upstream DNS at 3 AM.
Bootimus parity nicks (Bootimus v0.1.55 → v0.1.62):
- Storage table tints amber for ISOs that won't boot with current settings (Windows ISO when Windows is disabled, Linux ISO with no detected kernel + over the sanboot size threshold). Each row carries an inline reason — same affordance as Bootimus's "Image Properties" warning.
- Dashboard surfaces a "Images that won't boot" panel reusing the same predicate, so the operator sees the problem before they pick the ISO in the gate.
- Streaming uploads are already in place via axum multipart; the v0.1.62 fix to "502 on big upload" doesn't apply.
Phase 5 — pre-beta hardening
Per-MAC host bindings (crates/core/src/host_bindings.rs):
- New
HostBindingsregistry maps a MAC → preferredBootEntry::id(or one of the reserved menu shortcuts_local,_gate,_tools_menu). - Persisted to
<work_dir>/hosts.json. LikeSettingsStore, in-memory is authoritative — disk corruption falls back to empty rather than failing startup. - Inspired by Tinkerbell
smee's MAC-prepended URL pattern. The DHCP reply now embeds?mac=${mac}in the boot.ipxe URL; iPXE substitutes the literal MAC client-side, so the HTTP layer can short-circuit past the menu when a binding exists. /api/hostsGET / POST / DELETE drives the Hosts tab.
Prometheus metrics (crates/core/src/metrics.rs):
- Lock-free
AtomicU64-backed counters + gauges. Noprometheus/metrics-rsdep — they bring a registry, runtime, and complexity we don't need for a fixed set of metric families. - Counters: DHCP replies (per arch label), DHCP declined, TFTP transfers (per status label), TFTP bytes, HTTP requests (per route label).
- Gauges: ISO count, client count, gate count, gate-imaging count, NFS active mounts, uptime, build info.
- Exposed as plain Prometheus text at
/metrics.
Code cleanup pass: clippy --workspace --all-targets is now
warning-free. Replaced format!()-into-String with
std::fmt::Write::write!, switched manual reverse comparators to
Reverse, fixed map_or(false, …) → is_some_and, and a handful of
other idiom fixes.
UI overhaul for the v0.2.0 pre-beta milestone:
- Light + dark themes via
:root[data-theme=light]token swap. Toggled by a top-right button or theTkey. Persisted in localStorage; pre-paint inline script avoids dark→light flash. - New SVG logos: a refined anvil (
logo.svg) and a SMIL-animatedanvil-forge.svg(rising sparks + pulsing underglow). Pure SVG — no GIFs, no CSS keyframes for the sparks. - "Forge progress" widget on the Dashboard and Forge Gate: animated
anvil paired with a
linear-gradient(warn → accent)progress bar with a moving sheen. Goes idle (greyscale, no sheen) at zero imaging load. - Loader replaced "Loading…" text with the same anvil.
- Sidebar gains a Hosts tab.
Windows boot validation:
- New integration test synthesizes an ISO9660 with the
SOURCES\BOOT.WIMsentinel, uploads it, and asserts:- introspection labels it
windows_pewithhas_boot_wim=true, - the boot entry is
BootKind::Wimbootwith all five canonical files (bootmgr,bootmgr.efi,bcd,boot.sdi,boot.wim), - the rendered iPXE script chains wimboot with
initrd --nameentries for each, and - no trust-store strings appear:
bcdedit,testsigning,certutil,httpdisk,test-signedare all explicitly forbidden in the rendered output.
- introspection labels it
- WinPE bootstrap (
startnet.cmd) now picks up Bootimus v0.1.58 fixes: explicitnet start Workstationbeforenet use, surfaces errors instead of blind retries.
Test count: 66 → up from 56 in v0.1.0.
What's deferred to Phase 6
- Full ISO9660 + Joliet + Rock Ridge parser (current lookup is plain ISO9660 — Debian ISOs with Rock Ridge extensions may miss some paths).
- Real-hardware Windows boot validation (plumbing tested; no MS ISO pushed through the full pipeline yet).
- Real-hardware NFS validation (mount manager tested; no real NAS pushed through the full pipeline yet).
- UEFI HTTP Boot protocol (option 60 =
HTTPClient) untested on real firmware. - Raspberry Pi netboot quirks (option 43 vendor-specific, per-MAC prefixes).
- Multi-replica / daemonset deployment model (single replica for now).
- Pure-Rust SMB server (replace smbd) — slim image, no Samba.
- Auto-install / autounattend file library (Bootimus v0.1.58 pattern).
- Per-client / per-group menus (Bootimus v0.1.16 pattern).
- Real-hardware integration: at minimum a Linux ISO booted on a real BIOS box, a Windows ISO booted via wimboot on a real UEFI box, and a Pi 4 booting from an NFS-mounted Raspberry Pi OS ISO.
- Distro profile manifest (Bootimus v0.1.27 pattern) — currently introspection logic is hard-coded; could become data-driven so an operator can add a new distro profile from the UI without rebuilding.
- Wake-on-LAN trigger (Bootimus v0.1.16 pattern) — power-on a host then imaging starts unattended via a per-MAC binding.
- Syslog receiver (smee feature) — capture client-side install syslog for diagnostic visibility.
- IPv6 PXE / DHCPv6 — currently IPv4 only.