Ease-of-use pass inspired by Bootimus (Dnsmasq-PXE is a manual dnsmasq setup guide — nothing to adopt; OpenPXE already replaces that stack). Add ISO by URL: - New http-api `fetch` module: a small FetchJobs registry + a background streaming download (reqwest) that pipes a remote .iso through the same UploadHandle + introspection path as an upload, so a URL-fetched image classifies and gains boot entries identically. Progress is polled by the Storage view and rendered as rows, mirroring uploads. - Routes POST/GET/DELETE /api/isos/fetch. http/https only; .iso-only filename derived from Content-Disposition / URL basename with path traversal stripped; 16 GiB cap; cancel; credential-stripped URL display. Operator-gated, no boot-time outbound — offline boot is untouched. - Storage upload card gains an "Or add by URL" field with progress + cancel. Zero-touch admin bootstrap: - OPENPXE_ADMIN_USERNAME + OPENPXE_ADMIN_PASSWORD (or _PASSWORD_FILE for Docker/K8s secrets) auto-create the admin on first run, so a fresh container is usable with no setup wizard. Seeds the first run only — a lingering env var can't reset a rotated password. Tests: URL parse / filename / Content-Disposition unit tests + a wiremock end-to-end fetch-into-store integration test. clippy/fmt/node clean. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
279 lines
13 KiB
Markdown
279 lines
13 KiB
Markdown
<p align="center">
|
|
<img src="docs/openpxe-logo.svg" alt="OpenPXE" width="104" height="104" />
|
|
</p>
|
|
|
|
<h1 align="center">OpenPXE</h1>
|
|
|
|
<p align="center">
|
|
<strong>Container-native network boot & OS deployment — built in Rust.</strong>
|
|
</p>
|
|
|
|
<p align="center">
|
|
Drag in an ISO. PXE-boot and image an entire fleet from a browser.<br/>
|
|
No iPXE scripting. No <code>dnsmasq</code> + <code>tftpd</code> + Samba glue. No glibc. No garbage collector.
|
|
</p>
|
|
|
|
<p align="center">
|
|
<img alt="release" src="https://img.shields.io/badge/release-v0.6.0-2874d7" />
|
|
<img alt="license" src="https://img.shields.io/badge/license-MIT%20%7C%20Apache--2.0-59824f" />
|
|
<img alt="rust" src="https://img.shields.io/badge/built%20with-Rust-fb8841?logo=rust&logoColor=white" />
|
|
<img alt="container" src="https://img.shields.io/badge/container--native-OCI%20%C2%B7%20OpenShift-2496ED?logo=docker&logoColor=white" />
|
|
<img alt="binary" src="https://img.shields.io/badge/static-musl%20%C2%B7%20~18MB-330f1f" />
|
|
</p>
|
|
|
|
---
|
|
|
|
OpenPXE turns bare-metal provisioning into a single container with a web UI. It's a
|
|
ground-up Rust reimplementation of [iVentoy (ventoy/PXE)](https://github.com/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 `.ipxe` editing, no PXE jargon in the interface.
|
|
- **It runs anywhere a container runs.** No kernel modules, no privileged mode — proxy-mode
|
|
DHCP + `NET_BIND_SERVICE` is 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 — drop several at once
|
|
and they upload concurrently.
|
|
- **Add by URL** — paste an ISO link and the server streams it straight into storage and
|
|
auto-detects it, with progress; no download-then-reupload.
|
|
- **Automatic introspection** — detects the distro family and generates the right
|
|
kernel+initrd or Windows `wimboot` chain. 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` · `/readyz` probes.
|
|
- **Layered config** — defaults → TOML file → `OPENPXE_*` env, in that order.
|
|
- **Zero-touch first run** — set `OPENPXE_ADMIN_USERNAME` + `OPENPXE_ADMIN_PASSWORD` and a
|
|
fresh container comes up with the admin already created, no setup wizard. Automate the rest
|
|
from scripts/Postman with a per-install **API key** (`x-api-key`), shown in Settings → Advanced.
|
|
|
|
## 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.** `unsafe` is **denied workspace-wide**; the only
|
|
exceptions are two small, individually-audited FFI calls (`statvfs` for disk usage and a
|
|
Samba `SIGHUP`).
|
|
- **OpenSSL-free, pure-Rust crypto.** TLS via `rustls`/`ring`; the SAML Service Provider
|
|
does XML-DSig verification with RustCrypto — no `xmlsec`, no `libxml2`, 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
|
|
|
|
```bash
|
|
# 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-dev` service in `docker-compose.yml` for API-only testing on a laptop:
|
|
> `OPENPXE_PUBLIC_IP=127.0.0.1 docker compose up openpxe-dev`.
|
|
|
|
### Build from source
|
|
|
|
```bash
|
|
./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
|
|
|
|
```bash
|
|
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:
|
|
|
|
1. On upload, OpenPXE uses `wimlib-imagex` to inject exactly two plain-text files into the
|
|
WinPE image (`winpeshl.ini` + `startnet.cmd`) — no drivers, no certificates.
|
|
2. The container's Samba `smbd` serves the extracted install tree on `:445`.
|
|
3. 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](https://github.com/garybowers/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 |
|
|
| `OPENPXE_ADMIN_USERNAME` | — | First-run only: with `OPENPXE_ADMIN_PASSWORD`, auto-creates the admin so no setup wizard is needed. Ignored once an admin exists. |
|
|
| `OPENPXE_ADMIN_PASSWORD` | — | First-run admin password. Use `OPENPXE_ADMIN_PASSWORD_FILE` to read it from a file (Docker/K8s secret). |
|
|
|
|
## OpenShift
|
|
|
|
```bash
|
|
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`](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.
|