Initial commit: PXEForge Phases 1-4

Container-native PXE boot server in Rust, designed as a clean-room
alternative to iVentoy that never touches the client OS trust store.
This is the first commit of the project; it lands the full output of
Phases 1, 2, 3, and 4 in one shot.

## Phase 1 — protocol stack

- 8-crate workspace (core, dhcp-proxy, tftp, http-api, iso-store,
  ipxe-assets, webui, pxeforge bin).
- DHCP proxy (RFC 4578): replies with boot info only, never leases —
  sidesteps CAP_NET_RAW. Architecture-aware bootfile selection from
  option 93 (BIOS, IA32, x64-UEFI alias 0x0007/0x0009, ARM64).
- TFTP server with full OACK negotiation: blksize, tsize, windowsize.
  Without it a 1 MiB iPXE binary takes 2000 packets and unusably long.
- Two-stage iPXE chain: firmware PXE -> TFTP iPXE binary -> iPXE
  re-DHCPs with user-class iPXE -> HTTP /boot.ipxe -> kernel+initrd.
- HTTP server (axum) with byte-Range ISO streaming and an in-place
  ISO9660 lookup so kernel/initrd are served from inside the ISO
  without ever extracting it to disk.
- Linux ISOs boot via kernel+initrd extraction (memdisk/sanboot fail
  for >1-2 GiB modern distros). Distro-family detection drives the
  cmdline (Debian/Ubuntu, RHEL/Fedora, openSUSE, Arch, Alpine).

## Phase 2 — UX + Windows

- Hierarchical PXE menu (Default / Installers / Tools / Gated
  Deployment) generated from settings — no hand-written .ipxe paths
  surface in the UI. Number-key + letter hotkeys, BIOS+UEFI variants
  for some RHEL ISOs.
- Gated Deployment "horse-race" queue: clients join, operator picks
  one ISO, every gate launches simultaneously via tokio::sync::Notify.
- Bootimus-pattern Windows: WimPatcher injects a CRLF startnet.cmd
  into boot.wim so vanilla WinPE net-uses an SMB share and runs
  setup.exe. All Microsoft-signed; no test certs, no testsigning,
  no httpdisk.sys. SmbManager supervises smbd start/stop/SIGHUP.
- Netbox-style dark UI, fully offline (no CDN, no external fonts).

## Phase 3 — MVP hardening

- TFTP retransmit rewrite with explicit window tracking — UEFI SNP
  clients no longer hang on files that end mid-window. 4 new tests.
- DHCP broadcast-flag honored per RFC 2131 §4.1.
- Multi-arch container (linux/amd64 + linux/arm64). Entrypoint chowns
  bind-mounts as root then drops to uid 10001 via gosu.
- /healthz + /readyz split from /api/status — readyz fails if no
  iPXE binaries are bundled.
- pxeforge seed --from <path> CLI: same pipeline as web upload (slug,
  sha256, introspection, boot-entry).
- All timestamps RFC 3339 (browser Date couldn't parse the 9-tuple).
- Gate poll retains assignment until operator releases — clients that
  retry on transient network errors reuse the assignment instead of
  falling back to the menu.
- Custom OpenShift SCC: hostNetwork + NET_BIND_SERVICE only, no
  NET_RAW.

## Phase 4 — UI restructure + remote storage

- Web UI rebuilt around six tabs inspired by the iVentoy layout:
  Dashboard / Network / Forge Gate / Storage / Terminal / About.
  Old "Monitoring/Content/Configuration" sidebar groups are gone.
- NFS share manager (crates/iso-store/src/nfs.rs): mount NFSv3 or
  NFSv4.1 shares as ISO sources instead of uploading every file
  into the PVC. New IsoSource enum on IsoMeta lets the store resolve
  Local vs NFS lazily. Persisted to <work_dir>/nfs.json; failed
  mounts surface in the UI rather than blocking startup.
- Dockerfile gains nfs-common + iproute2; mounting NFS in-container
  also requires CAP_SYS_ADMIN. Documented in docs/architecture.md.
- LogBus + tracing layer in core: 500-line ring buffer + broadcast
  channel feed an SSE endpoint at /api/log/stream.
- Operator terminal at /api/terminal: whitelisted commands (status,
  isos, clients, gate, nfs, smb, log) — deliberately not a shell.
  Output mirrored onto the LogBus so the live tail and the terminal
  pane share one timeline.
- Network tab: read-only nic_name / subnet_mask / gateway probed
  from `ip` at startup; only DNS server is editable. Editing IP/mask
  on a hot UI would silently break PXE for every client mid-boot.
- Bootimus parity (releases v0.1.55 -> v0.1.62): amber row tint on
  un-bootable ISOs with inline reasons, dashboard "won't boot" panel.

## Tests

56 tests passing across the workspace:
- 16 core (LogBus, gate, settings, arch, client)
- 1 dhcp-proxy (raw option-93 extraction)
- 8 http-api unit (range parsing, terminal split/format)
- 13 http-api integration (gated deployment, range, settings, NFS,
  terminal, log SSE, network endpoint, ui assets, no-external-urls)
- 12 iso-store (introspect, slugify, smb, windows wim, NFS options)
- 6 tftp (RRQ parsing, plan_window edges)

cargo build --workspace and cargo clippy --workspace --all-targets
both finish clean (warnings only, no errors).
This commit is contained in:
Miles Ward
2026-04-29 02:47:00 -04:00
commit cc309da062
67 changed files with 9032 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Namespace
metadata:
name: pxeforge
labels:
# Allow privileged pods (host-network) in this namespace only. The pod
# itself still runs non-root with only NET_BIND_SERVICE — privileged
# here is about namespace pod-security, not container privileges.
pod-security.kubernetes.io/enforce: privileged
pod-security.kubernetes.io/warn: privileged
pod-security.kubernetes.io/audit: privileged
+80
View File
@@ -0,0 +1,80 @@
---
# Custom SCC for PXEForge.
#
# The default `restricted-v2` SCC blocks host network and all capabilities,
# which PXE cannot tolerate: DHCPDISCOVER is an L2 broadcast that CNI overlays
# do not deliver into pod netns. We grant the minimum set needed:
#
# - allowHostNetwork: true — required to receive broadcast DHCP
# - allowHostPorts: true — exposes 67/69/4011/80 on the node
# - requiredDropCapabilities strips the usual dangerous caps
# - allowedCapabilities:
# NET_BIND_SERVICE — bind <1024 as non-root
# - runAsUser.type: MustRunAsRange — force non-root uid mapped via setcap
# - readOnlyRootFilesystem: true — binary is in / (set by image), data
# dirs are mounted elsewhere
#
# We do NOT grant NET_RAW / NET_ADMIN / SYS_ADMIN. Proxy-mode DHCP does not
# need raw sockets (see architecture memory).
apiVersion: security.openshift.io/v1
kind: SecurityContextConstraints
metadata:
name: pxeforge-scc
annotations:
kubernetes.io/description: >-
Minimal SCC for PXEForge: host network + NET_BIND_SERVICE only, no raw
sockets, no privileged mode.
allowPrivilegedContainer: false
allowPrivilegeEscalation: false
allowHostNetwork: true
allowHostPorts: true
allowHostPID: false
allowHostIPC: false
allowedCapabilities:
- NET_BIND_SERVICE
requiredDropCapabilities:
- ALL
defaultAddCapabilities: []
readOnlyRootFilesystem: true
runAsUser:
type: MustRunAsRange
seLinuxContext:
type: MustRunAs
fsGroup:
type: MustRunAs
supplementalGroups:
type: RunAsAny
volumes:
- configMap
- downwardAPI
- emptyDir
- persistentVolumeClaim
- projected
- secret
users: []
groups: []
---
# Bind the SCC to the pxeforge service account.
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: pxeforge-scc-use
rules:
- apiGroups: ["security.openshift.io"]
resources: ["securitycontextconstraints"]
resourceNames: ["pxeforge-scc"]
verbs: ["use"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: pxeforge-scc-use
namespace: pxeforge
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: pxeforge-scc-use
subjects:
- kind: ServiceAccount
name: pxeforge
namespace: pxeforge
+35
View File
@@ -0,0 +1,35 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: pxeforge
namespace: pxeforge
---
apiVersion: v1
kind: ConfigMap
metadata:
name: pxeforge-config
namespace: pxeforge
data:
# Toggle DHCP proxy on or off. "proxy" = answer PXE clients alongside an
# existing DHCP server. "disabled" = require operator to point an external
# DHCP at us via next-server/filename.
PXEFORGE_DHCP_MODE: "proxy"
# Override if auto-detection picks the wrong NIC in multi-homed pods.
# Leave unset to auto-detect from the node's primary IPv4.
# PXEFORGE_PUBLIC_IP: "10.0.0.5"
PXEFORGE_LOG: "info,pxeforge=info"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pxeforge-isos
namespace: pxeforge
spec:
# ReadWriteOnce is fine — we deploy as a single replica since DHCP proxy
# coordination across replicas is not useful (clients hit whichever node
# hostNetwork catches their broadcast).
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 200Gi
+104
View File
@@ -0,0 +1,104 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: pxeforge
namespace: pxeforge
labels:
app.kubernetes.io/name: pxeforge
spec:
# Single replica by design (see PVC comment). If HA is needed later, split
# the HTTP/web plane (scalable, stateless) from the DHCP-proxy/TFTP plane
# (anycast / per-node daemonset).
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: pxeforge
template:
metadata:
labels:
app.kubernetes.io/name: pxeforge
spec:
serviceAccountName: pxeforge
# L2 broadcast (DHCPDISCOVER) does not cross most CNI overlays into
# pod netns. Host network is the working path.
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
securityContext:
# setcap on the binary allows non-root <1024 binding. No need to
# run as root.
runAsNonRoot: true
runAsUser: 10001
fsGroup: 10001
containers:
- name: pxeforge
image: ghcr.io/casperadmin/pxeforge:0.1.0
imagePullPolicy: IfNotPresent
ports:
- name: dhcp
containerPort: 67
hostPort: 67
protocol: UDP
- name: tftp
containerPort: 69
hostPort: 69
protocol: UDP
- name: pxe
containerPort: 4011
hostPort: 4011
protocol: UDP
- name: http
containerPort: 80
hostPort: 80
protocol: TCP
- name: smb
containerPort: 445
hostPort: 445
protocol: TCP
envFrom:
- configMapRef:
name: pxeforge-config
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10001
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
volumeMounts:
- name: isos
mountPath: /var/lib/pxeforge/isos
- name: work
mountPath: /var/lib/pxeforge/work
- name: tmp
mountPath: /tmp
readinessProbe:
httpGet:
path: /api/status
port: 80
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
httpGet:
path: /api/status
port: 80
initialDelaySeconds: 15
periodSeconds: 15
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 1000m
memory: 512Mi
volumes:
- name: isos
persistentVolumeClaim:
claimName: pxeforge-isos
- name: work
emptyDir: {}
- name: tmp
emptyDir: {}
+43
View File
@@ -0,0 +1,43 @@
---
# Service for the web UI / API. Using host network means the pod IP is the
# node IP, so this Service is mostly useful for cluster-internal ingress to
# the management UI via Route below.
apiVersion: v1
kind: Service
metadata:
name: pxeforge
namespace: pxeforge
labels:
app.kubernetes.io/name: pxeforge
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: pxeforge
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
- name: smb
port: 445
targetPort: 445
protocol: TCP
---
# Expose the web UI through an OpenShift Route. Clients on the PXE network
# still talk to the node directly on UDP 67/69/4011 — the Route only covers
# the TCP/80 management plane.
apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: pxeforge
namespace: pxeforge
spec:
to:
kind: Service
name: pxeforge
weight: 100
port:
targetPort: http
tls:
termination: edge
insecureEdgeTerminationPolicy: Redirect