Files
OpenPXE/README.md
T
Miles Ward 2c1c80a7ca v0.4.1: harden ISO uploads and beta UI polish
Add browser-safe chunked ISO uploads with progress, partial-file visibility, offset validation, and abort cleanup while keeping the legacy multipart endpoint for API clients.

Record host-log validation coverage, keep the queue/status UI copy clean, move release docs to 0.4.1, and tighten the dark theme to a near-black Netbox-style palette.
2026-05-24 13:45:35 -04:00

13 KiB
Raw Permalink Blame History

OpenPXE

Container-native PXE boot server. A Rust reimplementation of iVentoy (ventoy/PXE), designed from scratch for Docker/OCI and OpenShift. Upload .iso files via the web UI; network clients PXE-boot them.

Status: v0.4.1 / pre-beta. Phases 15 complete: full PXE stack, Queued Deployment queue, NFS-share ISO sources, live tracing log + an operator terminal, per-MAC host bindings, Prometheus /metrics, light/dark theme toggle, animated OpenPXE imaging-progress widget, chunked ISO uploads, and per-ISO boot passwords. The test suite and clippy are part of the release checklist. Ready for real-hardware validation.

Design non-negotiables

  1. Fully offline / air-gap deployable. Zero CDN assets. Zero external HTTP calls from the server, the browser, or the generated iPXE scripts. Build the container once, run forever disconnected.
  2. iPXE is a backend implementation detail. No .ipxe upload path, no manual script editing, no iPXE terminology in the UI. Every knob in the web UI maps to a specific script-generation behavior inside the binary.
  3. The client trust store is off-limits. No test-signed drivers, no bcdedit /set testsigning on, no certificates injected into WinPE or the target OS.

What it does

  1. DHCP proxy (RFC 4578). Coexists with your existing DHCP server — never assigns IPs. Listens on UDP 67 + UDP 4011.
  2. TFTP server (RFC 1350 + RFC 2347/2348/2349/7440 option negotiation) that serves architecture-specific iPXE binaries to firmware PXE ROMs.
  3. HTTP server that serves the web UI, the generated iPXE boot scripts, raw ISOs (with Range), and files inside ISOs without prior extraction.
  4. ISO introspection: auto-detects the distro family and generates the appropriate kernel+initrd or wimboot chain. No manual config.
  5. Hierarchical PXE menu mirroring the Phase 2 spec:
    Default        > Boot from Local HDD
    Installers     > Linux Installers  / Windows Installers
    Tools          > Utilities / OpenPXE Shell / Network Card Info
    Queued Deployment
    
  6. Queued Deployment queue — the coordinated launch flow. A client that selects Queued Deployment gets a numbered position and waits. The operator picks an ISO in the web UI and fires it to every waiting client simultaneously.
  7. Web UI (Netbox-style): sidebar nav (Dashboard / Network / Queue / Storage / Hosts / Terminal / About), light + dark themes (toggle top-right or press T), animated OpenPXE progress widget when devices are imaging. All assets served from the binary — no external requests.
  8. Per-MAC host bindings. Pin a MAC to a boot target and the client skips the menu, chains straight through.
  9. Prometheus metrics at /metrics — DHCP replies by arch, TFTP transfer counts and bytes, HTTP request counts by route, queue / imaging gauges, uptime, build info. Plain text exposition format, no external metrics framework dependency.
  10. Settings API lets you change the default boot-menu timeout (default 600s), the timeout action (stay / Local HDD / Queued Deployment), and feature toggles like Windows ISO support. The iPXE scripts regenerate on every request using current settings.

Architectures supported on day one

DHCP option 93 Architecture Binary 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 sends HTTPClient in option 60 is handled too — we skip TFTP and respond with an HTTP URL.

# 1. Pull bundled iPXE binaries (~2 MB, one-time).
./scripts/fetch-ipxe.sh

# 2. Build the container image (~3 min first time).
docker buildx build -f deploy/docker/Dockerfile -t openpxe:0.4.1 --load .

# 3. Run it on the box plugged into your PXE network. Set PUBLIC_IP to
#    this host's LAN address so advertised iPXE 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.4.1

# 4. Open the UI and drop an ISO in.
open http://10.0.0.5

Host networking is required in proxy mode so the container sees DHCPDISCOVER broadcasts from the PXE VLAN. On macOS/Windows hosts Docker runs in a Linux VM, so "host" means the VM — use openpxe-dev in docker-compose.yml for API-only testing on a laptop.

Quick start — docker compose

# MVP / API testing on a laptop (no DHCP, high ports):
OPENPXE_PUBLIC_IP=127.0.0.1 docker compose up openpxe-dev
# Real PXE deployment on a Linux host (host network, DHCP proxy on):
OPENPXE_PUBLIC_IP=10.0.0.5  docker compose up openpxe

Multi-arch build + push

For deploying to x86_64 servers, build both arches in one manifest:

# One-time: bootstrap a multi-arch builder.
docker buildx create --name openpxe-multi --driver docker-container --use

# Build + push both linux/amd64 and linux/arm64 under one tag.
docker buildx build --builder openpxe-multi \
  --platform linux/amd64,linux/arm64 \
  -t ghcr.io/YOUR-ORG/openpxe:0.4.1 \
  --push \
  -f deploy/docker/Dockerfile .

On an Apple Silicon host, the amd64 stage runs under QEMU emulation (~10-15 min for a cold cache). On a Linux x86_64 host, both arches build natively at normal speed. CI runners on GitHub Actions with docker/build-push-action@v5 handle this cleanly.

Build from source (no container)

./scripts/fetch-ipxe.sh
cargo run --release            # needs NET_BIND_SERVICE or root for :80/:69

Container health probes

Endpoint Purpose
/healthz Liveness — HTTP stack alive. Always 200.
/readyz Readiness — 200 only if iPXE binaries bundled + ISO dir OK.
/api/status Full JSON status: versions, assets, counts, live settings, SMB state.

Pre-seeding ISOs from a directory

For CI, pre-baked homelab deployments, or a fresh PVC, the binary has a seed subcommand that imports every *.iso from a host path through the same pipeline the web UI uses (introspection + boot-entry generation):

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.4.1 seed --from /seed

# Dry run first to see what would be imported:
docker run --rm -v /my/iso-library:/seed:ro openpxe:0.4.1 seed --from /seed --dry-run

Environment overrides

Var Default Meaning
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_DHCP_MODE proxy proxy or disabled
OPENPXE_PUBLIC_IP auto-detect Advertised IP for clients. Startup fails if unset and auto-detect returns loopback.
OPENPXE_ISO_DIR /var/lib/openpxe/isos Where uploaded ISOs live
OPENPXE_WORK_DIR /var/lib/openpxe/work Scratch + runtime settings
OPENPXE_LOG info,openpxe=debug tracing filter

What the boot menu looks like 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 / Shell /
                                     NIC Info / Reboot /
                                     Exit and continue BIOS
 ---------------------- Queued Deployment ------------------
 Queued Deployment   (join queue)

Linux/Windows submenus show file sizes iVentoy-style:

                   OpenPXE - Linux Installers

 [  4376 MB]  CentOS-7-x86_64-DVD-1810
 [  2002 MB]  Fedora-Workstation-Live-x86_64-38-1.6
 [  4699 MB]  ubuntu-22.04.2-desktop-amd64
            < Back to main menu

iPXE never appears in the UI — the whole hierarchy above is generated from ISOs you upload via drag-and-drop in the web UI plus toggles in Settings.

OpenShift

oc apply -f deploy/openshift/
oc -n openpxe get all
oc -n openpxe get route openpxe -o jsonpath='{.spec.host}'

Why a custom SCC?

The default restricted-v2 blocks hostNetwork and all capabilities. PXE cannot work without host network (CNI overlays don't deliver L2 broadcast into pod netns), and we need NET_BIND_SERVICE to bind <1024. The custom openpxe-scc grants exactly those two and nothing else. No raw sockets, no privileged mode — proxy-mode DHCP sidesteps the usual requirements.

What's on host ports

Port Proto Purpose
67 UDP DHCP server (proxy replies)
69 UDP TFTP
4011 UDP PXE Boot Server discovery
80 TCP Web UI + HTTP boot assets

The OpenShift Route only covers 80/TCP. Clients on the PXE network talk to the node's host IP directly for UDP.

Windows support

Enabled by toggling Windows ISO support under Settings. The flow:

  1. Upload a stock Microsoft Windows install ISO (vanilla, no pre-processing).
  2. On upload, OpenPXE extracts the ISO and uses wimlib-imagex to rewrite image index 2 (WinPE) of sources/boot.wim. It injects exactly two plain-text files:
    • Windows/System32/winpeshl.ini — tells WinPE to run startnet.cmd.
    • Windows/System32/startnet.cmd — runs wpeinit, waits for the SMB host to be reachable, net use Z: \\<server>\<share> /user:guest, then Z:\setup.exe.
  3. The container's Samba smbd serves the extracted install tree on :445.
  4. The client gets chainloaded into wimboot → patched WinPE → Windows Setup running off the SMB share. Every binary the client executes is stock Microsoft-signed.

What we never do

  • Ship drivers — signed, test-signed, or otherwise — that load on the client.
  • Install certificates into the target's trust store or WinPE boot policy.
  • Recommend bcdedit /set testsigning on or any equivalent signing-policy weakening.

Credit & limitations

The SMB-based approach is adapted from Bootimus (Apache-2.0). Re-implemented in Rust; no code was copied verbatim. Known operational constraints inherited from the design:

  • Port 445 must be directly reachable from PXE clients. net use ignores alternate ports. In OpenShift this means hostPort: 445 on the deployment; on a host that already runs SMB it will collide.
  • Windows 10/11 client SKUs are the tested target. Server SKUs untested.
  • Hardware with NICs/storage controllers missing from WinPE's bundled drivers will need a driver-pack injection step (not yet implemented).

Queued Deployment

The coordinated launch flow, end to end:

  1. A client boots and picks Queued Deployment in the PXE menu (or falls through on timeout with the default timeout_action).
  2. The client joins the queue, gets a numbered queue position, and enters a long-poll loop (25s per request, auto-renewed).
  3. In the web UI's Queued Deployment tab, the operator sees each waiting client with its MAC, IP, arch, and position.
  4. The operator selects an image and clicks Launch for all waiting. The server broadcasts the assignment to every queued client via a tokio::sync::Notify; each client's next poll returns the boot script for the chosen image.
  5. Every client chains the same image at effectively the same moment. The queue stays visible until the operator releases entries, which keeps a useful audit trail during hardware testing.

No user-facing iPXE anywhere in this flow. The client only ever runs scripts we generate; the operator only interacts with the web UI.

Architecture

See docs/architecture.md for the protocol stack, crate layout, and the full decision log.

Licence

MIT OR Apache-2.0.