# PXEForge Container-native PXE boot server. A Rust reimplementation of [iVentoy (ventoy/PXE)](https://github.com/ventoy/PXE), designed from scratch for Docker/OCI and OpenShift. Upload `.iso` files via the web UI; network clients PXE-boot them. > **Status:** Phase 3 MVP. Container image builds and runs, gate flow > validated end-to-end (two clients join queue → operator assigns in UI → > both wake within 1 s with the correct boot script). 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 / PXEForge Shell / Network Card Info Gated Deployment ``` 6. **Gated Deployment queue** — the "horse race gate" flow. A client that selects *Gated 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 / Clients / Gated Deployment / Images / Settings / About), top tabs, dark theme, teal accents. All assets served from the binary — no external requests. 8. **Settings API** lets you change the default boot-menu timeout (default 600s), the timeout action (stay / Local HDD / Gated 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. ## Quick start — MVP container (recommended) ```bash # 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 pxeforge:0.1.0 --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 pxeforge \ --network host \ -e PXEFORGE_PUBLIC_IP=10.0.0.5 \ -e PXEFORGE_DHCP_MODE=proxy \ -v $PWD/data/isos:/var/lib/pxeforge/isos \ -v $PWD/data/work:/var/lib/pxeforge/work \ pxeforge:0.1.0 # 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 `pxeforge-dev` in `docker-compose.yml` for API-only testing on a laptop. ### Quick start — docker compose ```bash # MVP / API testing on a laptop (no DHCP, high ports): PXEFORGE_PUBLIC_IP=127.0.0.1 docker compose up pxeforge-dev # Real PXE deployment on a Linux host (host network, DHCP proxy on): PXEFORGE_PUBLIC_IP=10.0.0.5 docker compose up pxeforge ``` ### Multi-arch build + push For deploying to x86_64 servers, build both arches in one manifest: ```bash # One-time: bootstrap a multi-arch builder. docker buildx create --name pxeforge-multi --driver docker-container --use # Build + push both linux/amd64 and linux/arm64 under one tag. docker buildx build --builder pxeforge-multi \ --platform linux/amd64,linux/arm64 \ -t ghcr.io/YOUR-ORG/pxeforge:0.1.0 \ --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) ```bash ./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): ```bash docker run --rm \ -v /my/iso-library:/seed:ro \ -v pxeforge-data:/var/lib/pxeforge/isos \ -e PXEFORGE_PUBLIC_IP=10.0.0.5 \ pxeforge:0.1.0 seed --from /seed # Dry run first to see what would be imported: docker run --rm -v /my/iso-library:/seed:ro pxeforge:0.1.0 seed --from /seed --dry-run ``` ### Environment overrides | Var | Default | Meaning | |------------------------|-----------------------------|----------------------------------------| | `PXEFORGE_HTTP_PORT` | `80` | Web UI + boot script HTTP port | | `PXEFORGE_TFTP_PORT` | `69` | TFTP port | | `PXEFORGE_DHCP_PORT` | `67` | DHCP server-side port | | `PXEFORGE_DHCP_MODE` | `proxy` | `proxy` or `disabled` | | `PXEFORGE_PUBLIC_IP` | auto-detect | Advertised IP for clients. Startup **fails** if unset and auto-detect returns loopback. | | `PXEFORGE_ISO_DIR` | `/var/lib/pxeforge/isos` | Where uploaded ISOs live | | `PXEFORGE_WORK_DIR` | `/var/lib/pxeforge/work` | Scratch + runtime settings | | `PXEFORGE_LOG` | `info,pxeforge=debug` | `tracing` filter | ## What the boot menu looks like on a real client ``` PXEForge - 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 ---------------------- Gated Deployment ------------------ Gated Deployment (join queue) ``` Linux/Windows submenus show file sizes iVentoy-style: ``` PXEForge - 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 ```bash oc apply -f deploy/openshift/ oc -n pxeforge get all oc -n pxeforge get route pxeforge -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 `pxeforge-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, PXEForge 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: \\\ /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](https://github.com/garybowers/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). ## Gated Deployment The "horse race gate" flow, end to end: 1. A client boots and picks **Gated Deployment** in the PXE menu (or falls through on timeout with the default `timeout_action`). 2. The client joins the queue, gets a numbered gate position, and enters a long-poll loop (25s per request, auto-renewed). 3. In the web UI's **Gated 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 gated 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 gate opens and the horses run together. 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`](docs/architecture.md) for the protocol stack, crate layout, and the full decision log. ## Licence MIT OR Apache-2.0.