Introspection (the headline): detection is now probe-based. Instead of
grepping raw sectors for filename strings, we walk the ISO9660
directory tree and check whether the well-known boot files actually
exist — and the same probes run over NFS READ3 / SFTP seek-reads, so
share-hosted ISOs finally classify instead of registering as Unknown.
iso-store:
- New iso_fs module: the read-only ISO9660 walker (generalized from
http-api) over an IsoReadAt trait — local files, NFS, SFTP, and the
in-memory test images all share it. Iterative walk, 4 MiB directory
cap, strict-mastering trailing-dot normalization (VMLINUZ.;1 now
matches /vmlinuz), CachingReadAt collapses repeated directory reads
during the probe pass (~60 → ~6 round-trips per remote ISO).
- introspect.rs rewritten (INTROSPECT_REV 2): PVD label → El Torito →
/sources/boot.wim probe → verified Linux kernel+initrd probe table →
local-only 16 MiB UDF-Windows scan → filename-token fallback.
* Fixes the false-Windows bug: any Linux ISO shipping GRUB/syslinux
chainload modules contains the literal "bootmgr", so gparted-live
classified as WindowsPe. Linux probes now run first; the byte scan
only sees ISOs nothing else claimed. Local ISOs re-probe once on
startup via the rev bump — no re-upload.
* Kernel entries are emitted only when kernel+initrd verifiably
exist (no more guessed paths that 404 at boot). Debian-live /
d-i netinst / CoreOS shapes classify for the UI but keep their
working sanboot entries (their boot protocols need args we don't
render yet; CoreOS additionally needs its embedded ignition).
* Label + filename vocab extended: rhcos/coreos/openshift/okd,
gparted/clonezilla/kali/tails, almalinux/rocky, sles, manjaro.
- NFS + SFTP managers: per-ISO IsoReadAt readers (READ3-at-offset with
short-read looping / seek+read_exact), background introspection pass
after each scan — entries register instantly with a provisional
filename-based report (rev 0, optimistic sanboot preserved) and
upgrade in place as probes land (30s/ISO timeout, failures keep the
provisional). locate_in_iso() exposes the walker to the HTTP layer.
- remote_cache: introspection results persisted per protocol keyed
share/path@size and gated on INTROSPECT_REV — container restarts
re-probe only new/replaced ISOs; upgrades re-probe exactly once.
- SMB: smbclient can't seek, so SMB ISOs get the filename-token family
(rev stays 0 → sanboot entry + "awaiting introspection" label).
- IsoStore::update_external_introspection swaps in completed reports
and regenerates boot entries, preserving category/password.
http-api:
- /iso/{id}/{*path} now serves files from inside NFS/SFTP-hosted ISOs
(remote ISO9660 lookup + ranged share stream) — verified kernel
entries on remote Linux ISOs are actually bootable, end to end.
- iso_fs.rs deleted in favor of the shared iso-store module.
- full_flow fixtures build real directory trees via the shared
test-image builder (new iso-store feature) — a label-only blob no
longer earns a kernel entry, by design.
webui:
- Available images: paged 5 per page with a quiet footer pager
(Showing X–Y of N · Prev/Next), filter-then-paginate, page resets on
search input. Fifty images is five clean pages, not a scroll wall.
- Hosts/Queue profile: "Unattended file (in Storage → Advanced)" so
the picker says where the files live.
- Row badge keys on introspect_rev: probed remote ISOs read like local
ones; un-probed say "awaiting introspection".
Validation: clippy pedantic clean, fmt clean, 316 workspace tests
green (+17: walker, probe shapes incl. gparted regression + CoreOS,
filename table, cache round-trips), webui syntax-checked.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
OpenPXE
Container-native network boot & OS deployment — built in Rust.
Drag in an ISO. PXE-boot and image an entire fleet from a browser.
No iPXE scripting. No dnsmasq + tftpd + Samba glue. No glibc. No garbage collector.
OpenPXE turns bare-metal provisioning into a single container with a web UI. It's a ground-up Rust reimplementation of iVentoy (ventoy/PXE), designed for Docker/OCI and OpenShift instead of a Windows desktop — so it drops onto an Unraid box, a Linux server, or a Kubernetes cluster and just runs.
Upload .iso files (or point at a remote share), and any machine on the network boots
them — Linux installers, live tools, or stock Windows setup — with zero iPXE knowledge
required by the operator.
Status — v0.6.0, late pre-beta. The full PXE stack, web UI, remote ISO libraries (SMB/NFS/SFTP), Windows deployment, queued fleet rollout, SAML SSO, and Prometheus metrics are implemented and test-covered. The release checklist gates every tag on the full test suite +
clippy. Currently in real-hardware validation.
Why OpenPXE
Standing up network boot the traditional way means hand-wiring dnsmasq, a TFTP daemon,
hand-written iPXE menu scripts, an HTTP server, and Samba — then keeping that fragile
stack alive, and discovering none of it containerizes cleanly (kernel-mount NFS, raw
sockets, CAP_SYS_ADMIN). iVentoy solved the UX beautifully, but it's a Windows GUI app.
OpenPXE collapses that whole stack into one statically-linked binary in one container:
- A web UI does everything. iPXE is an internal implementation detail — there is no
script upload, no
.ipxeediting, no PXE jargon in the interface. - It runs anywhere a container runs. No kernel modules, no privileged mode — proxy-mode
DHCP +
NET_BIND_SERVICEis the entire requirement. Verified on Unraid, plain Docker, and OpenShift's restricted SCC. - It's air-gap native. Zero CDN assets, zero outbound calls from the server, browser, or generated boot scripts. Build the image once, run it forever, disconnected.
Highlights
Boot stack
- DHCP proxy (RFC 4578) that coexists with your existing DHCP — it never hands out IPs.
- TFTP (RFC 1350 + 2347/2348/2349/7440 option negotiation) serving arch-correct iPXE firmware.
- HTTP serving the UI, generated boot scripts, raw ISOs (with byte-range), and files inside ISOs with no prior extraction.
- Graphical iPXE boot menu built from your uploads, with a PNG background and a clean hierarchy — generated fresh on every request from current settings.
ISO management & remote libraries
- Drag-and-drop chunked uploads that don't 502 on multi-GB images.
- Automatic introspection — detects the distro family and generates the right
kernel+initrd or Windows
wimbootchain. No manual config. - Remote ISO libraries, streamed on demand (no local cache) over SMB, NFS, or SFTP — see the table below.
Fleet deployment
- Queued Deployment — clients join a queue and wait; the operator fires one image at every waiting machine simultaneously.
- Per-MAC host bindings — pin a MAC straight to a target (with optional auto hostname, auto IP, and an unattended answer file); it skips the menu and chains through.
- Unattended installs — upload Kickstart / Preseed / Autoinstall / Windows answer files; they're templated per-host (hostname / IP / MAC) and served only to booting clients.
- Windows deployment from a stock Microsoft ISO — every binary the client runs stays Microsoft-signed (details below).
Operations & access
- SAML 2.0 single sign-on (pure-Rust SP, no OpenSSL/xmlsec) alongside local accounts.
- Custom branding — light / dark / PXE-client logos and favicon.
- Notifications — Slack / Teams / Discord webhooks and SMTP email on boot events.
- Prometheus
/metrics, a built-in operator terminal, live tracing log, and/healthz·/readyzprobes. - Layered config — defaults → TOML file →
OPENPXE_*env, in that order.
Built in Rust
Rust isn't a checkbox here — it's why OpenPXE deploys the way it does:
- One static binary, ~18 MB. Compiled to
x86_64-unknown-linux-musl— no glibc, no interpreter, no sidecar runtime. The runtime image is "binary + a few CLI tools." - No garbage collector, async throughout. A Tokio runtime drives DHCP, TFTP, HTTP, and many concurrent multi-GB ISO streams on a tiny, predictable memory footprint — it idles near-zero and never GC-pauses mid-transfer.
- Memory-safe by construction.
unsafeis denied workspace-wide; the only exceptions are two small, individually-audited FFI calls (statvfsfor disk usage and a SambaSIGHUP). - OpenSSL-free, pure-Rust crypto. TLS via
rustls/ring; the SAML Service Provider does XML-DSig verification with RustCrypto — noxmlsec, nolibxml2, no C crypto to CVE-patch. Even the SMB/NFS/SFTP clients avoid C libraries. - Sub-minute, reproducible container builds. Cross-compiled with
cargo-zigbuild(zig as the linker) — a full image builds in well under a minute on a warm cache, with no QEMU emulation.
Quick start
Run the container
# Build the self-contained image (iPXE binaries are fetched + built inside the Dockerfile).
docker build -f deploy/docker/Dockerfile -t openpxe:0.5.5 .
# Run it on the box plugged into your PXE network. Host networking is required in
# proxy mode so the container sees DHCPDISCOVER broadcasts; set PUBLIC_IP to this
# host's LAN address so advertised boot URLs are reachable.
docker run -d --name openpxe --network host \
-e OPENPXE_PUBLIC_IP=10.0.0.5 \
-e OPENPXE_DHCP_MODE=proxy \
-v $PWD/data/isos:/var/lib/openpxe/isos \
-v $PWD/data/work:/var/lib/openpxe/work \
openpxe:0.5.5
# Open the UI and drop an ISO in.
open http://10.0.0.5
On macOS/Windows, Docker runs inside a Linux VM, so "host network" means the VM — use the
openpxe-devservice indocker-compose.ymlfor API-only testing on a laptop:OPENPXE_PUBLIC_IP=127.0.0.1 docker compose up openpxe-dev.
Build from source
./scripts/fetch-ipxe.sh # populate assets/ipxe/ (embedded at compile time)
cargo run --release # needs root or CAP_NET_BIND_SERVICE for :80/:69
Pre-seed ISOs from a directory
docker run --rm \
-v /my/iso-library:/seed:ro \
-v openpxe-data:/var/lib/openpxe/isos \
-e OPENPXE_PUBLIC_IP=10.0.0.5 \
openpxe:0.5.5 seed --from /seed # add --dry-run to preview
Remote ISO libraries
Point OpenPXE at a NAS and boot ISOs straight off it — read on demand, no local copy,
so a 50-ISO library costs zero disk on the OpenPXE host. All three clients are userspace
(no kernel mounts, no CAP_SYS_ADMIN); pick whichever your storage speaks.
| Protocol | Implementation | Auth | HTTP Range¹ |
|---|---|---|---|
| NFS (v3) | Pure-Rust in-process client | Client-IP (server export list) | ✅ |
| SFTP (SSH) | Pure-Rust in-process client (russh) |
Password or SSH key · host-key TOFU | ✅ |
| SMB / CIFS | Userspace smbclient |
Guest or username/password | — |
¹ Range support lets clients seek into a multi-GB ISO without downloading what comes
before it — needed for kernel/initrd extraction and httpdisk-style boots. NFS and SFTP
expose explicit offsets; the SMB CLI streams sequentially, so SMB-sourced ISOs serve whole-file.
Supported client architectures
| DHCP option 93 | Architecture | Firmware served |
|---|---|---|
0x0000 |
Legacy x86 BIOS | undionly.kpxe |
0x0006 |
IA32 UEFI | snponly-i386.efi |
0x0007 / 0x0009 |
x86_64 UEFI | snponly.efi |
0x000B |
ARM64 UEFI | snponly-arm64.efi |
UEFI firmware that advertises HTTPClient (option 60) skips TFTP entirely and is handed an HTTP URL.
The boot menu, on a real client
OpenPXE — network boot menu
------------------------- Default -------------------------
Boot from Local HDD
----------------------- Installers ------------------------
Linux Installers >
Windows Installers > (only if enabled in Settings)
-------------------------- Tools --------------------------
Tools > Utilities / OpenPXE Shell / NIC Info / Reboot
---------------------- Queued Deployment ------------------
Queued Deployment (join queue)
Linux/Windows submenus list images iVentoy-style with sizes:
OpenPXE — Linux Installers
[ 4699 MB] ubuntu-22.04.2-desktop-amd64
[ 2002 MB] Fedora-Workstation-Live-x86_64-38-1.6
< Back to main menu
The entire hierarchy is generated from what you upload and toggle — iPXE never surfaces.
Windows deployment
Enable Windows ISO support in Settings, then upload a stock, unmodified Microsoft ISO:
- On upload, OpenPXE uses
wimlib-imagexto inject exactly two plain-text files into the WinPE image (winpeshl.ini+startnet.cmd) — no drivers, no certificates. - The container's Samba
smbdserves the extracted install tree on:445. - The client chainloads
wimboot→ patched WinPE → Windows Setup running off the share.
Every executable the client runs is stock Microsoft-signed. OpenPXE never ships
drivers, never installs certificates into the client trust store, and never recommends
bcdedit /set testsigning on. The SMB approach is adapted (re-implemented, not copied)
from Bootimus (Apache-2.0). Port 445 must be
directly reachable from clients; Windows 10/11 client SKUs are the tested target.
Configuration
All settings have defaults and layer defaults → TOML (--config / OPENPXE_CONFIG) →
OPENPXE_* env. The common knobs:
| Var | Default | Meaning |
|---|---|---|
OPENPXE_PUBLIC_IP |
auto-detect | IP advertised to clients. Startup fails if unset and auto-detect yields loopback. |
OPENPXE_DHCP_MODE |
proxy |
proxy or disabled |
OPENPXE_HTTP_PORT |
80 |
Web UI + boot-script HTTP port |
OPENPXE_TFTP_PORT |
69 |
TFTP port |
OPENPXE_DHCP_PORT |
67 |
DHCP server-side port |
OPENPXE_ISO_DIR |
/var/lib/openpxe/isos |
Uploaded ISOs |
OPENPXE_WORK_DIR |
/var/lib/openpxe/work |
Scratch, settings, share + branding state |
OPENPXE_LOG |
info,openpxe=info |
tracing filter |
OpenShift
oc apply -f deploy/openshift/
oc -n openpxe get route openpxe -o jsonpath='{.spec.host}'
The bundled openpxe-scc grants exactly hostNetwork (CNI overlays don't deliver L2
broadcast into pod netns) and NET_BIND_SERVICE (to bind ports <1024) — nothing else.
No raw sockets, no privileged mode. The Route covers 80/TCP; PXE clients reach UDP
67/69/4011 on the node's host IP directly.
Health & observability
| Endpoint | Purpose |
|---|---|
/healthz |
Liveness — always 200 if the HTTP stack is up. |
/readyz |
Readiness — 200 only once iPXE firmware is bundled and the ISO dir is reachable. |
/api/status |
Full JSON: version, assets, counts, live settings, share + SMB state. |
/metrics |
Prometheus text format — DHCP replies by arch, TFTP/HTTP counts, queue gauges, uptime. |
Architecture
Workspace of focused crates — core, dhcp-proxy, tftp, http-api, iso-store,
ipxe-assets, webui, and the openpxe binary. See
docs/architecture.md for the protocol stack, crate layout, and
the full decision log.
License
Dual-licensed under MIT OR Apache-2.0 — use whichever fits your project.