-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.
+
OpenPXE
-> **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.
+
+ Container-native network boot & OS deployment — built in Rust.
+
-## Design non-negotiables
+
+ 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.
+
-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.
+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.
-### Architectures supported on day one
+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.**
-| 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` |
+> **Status — v0.5.5, 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.
-UEFI firmware that sends `HTTPClient` in option 60 is handled too — we
-skip TFTP and respond with an HTTP URL.
+## Why OpenPXE
-## Quick start — MVP container (recommended)
+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.
+- **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.
+
+## 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
-# 1. Pull bundled iPXE binaries (~2 MB, one-time).
-./scripts/fetch-ipxe.sh
+# Build the self-contained image (iPXE binaries are fetched + built inside the Dockerfile).
+docker build -f deploy/docker/Dockerfile -t openpxe:0.5.5 .
-# 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 \
+# 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.4.1
+ openpxe:0.5.5
-# 4. Open the UI and drop an ISO in.
+# 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.
+> 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`.
-### Quick start — docker compose
+### Build from source
```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
+./scripts/fetch-ipxe.sh # populate assets/ipxe/ (embedded at compile time)
+cargo run --release # needs root or CAP_NET_BIND_SERVICE for :80/:69
```
-### 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):
+### 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.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
+ openpxe:0.5.5 seed --from /seed # add --dry-run to preview
```
-### Environment overrides
+## Remote ISO libraries
-| 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 |
+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.
-## What the boot menu looks like on a real client
+| 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
+ OpenPXE — network boot menu
------------------------- Default -------------------------
Boot from Local HDD
- ----------------------- Installers -----------------------
+ ----------------------- Installers ------------------------
Linux Installers >
Windows Installers > (only if enabled in Settings)
-------------------------- Tools --------------------------
- Tools > Utilities / Shell /
- NIC Info / Reboot /
- Exit and continue BIOS
+ Tools > Utilities / OpenPXE Shell / NIC Info / Reboot
---------------------- Queued Deployment ------------------
Queued Deployment (join queue)
```
-Linux/Windows submenus show file sizes iVentoy-style:
+Linux/Windows submenus list images iVentoy-style with sizes:
```
- OpenPXE - Linux Installers
+ 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
+ [ 2002 MB] Fedora-Workstation-Live-x86_64-38-1.6
< 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.
+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 |
## 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 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.
-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.
+## Health & observability
-### 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: \\\ /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.
+| 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
-See [`docs/architecture.md`](docs/architecture.md) for the protocol stack,
-crate layout, and the full decision log.
+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.
-## Licence
+## License
-MIT OR Apache-2.0.
+Dual-licensed under **MIT OR Apache-2.0** — use whichever fits your project.
diff --git a/docs/openpxe-logo.svg b/docs/openpxe-logo.svg
new file mode 100644
index 0000000..52e1664
--- /dev/null
+++ b/docs/openpxe-logo.svg
@@ -0,0 +1,21 @@
+