# OpenPXE

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:** v0.4.1 / pre-beta. Phases 1–5 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.
8. **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.

## 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 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

```bash
# 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:

```bash
# 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)

```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 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

```bash
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](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).

## 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`](docs/architecture.md) for the protocol stack,
crate layout, and the full decision log.

## Licence

MIT OR Apache-2.0.
