Establish clean homelab infrastructure baseline
lint / yaml (push) Has been cancelled
lint / ansible (push) Has been cancelled
lint / terraform (push) Has been cancelled

Reorganize the brownfield repository, remove retired and generated artifacts, harden ignore rules, and record the GitOps/IaC redesign.
This commit is contained in:
2026-09-09 16:47:20 +00:00
commit 88a02ababa
418 changed files with 50579 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
# Installing Windows 9x (95 / 98 / ME) over the network
9x is DOS-based, not NT — none of the NT5/NT6 methods apply. There are two routes; the second
is much better for netboot.
## Route A — the "real DOS way" (not recommended)
PXE → `memdisk` boots a DOS floppy/ISO (FreeDOS or MS-DOS) → load a **real-mode NIC packet
driver** + network redirector → copy the `WIN98` CABs from a share → run `setup.exe`. It works
(netboot.xyz already has a FreeDOS entry to bootstrap from), but real-mode DOS packet drivers
are a per-NIC nightmare and the CAB copy is slow. Only worth it for authenticity.
## Route B — win98-quickinstall (recommended)
<https://github.com/oerg866/win98-quickinstall>
The important thing: **its installer is Linux-based** (a minimal Linux env + a writer called
`lunmercy`). It does **not** run DOS-era Setup — it streams a pre-made image ("MercyPak",
designed to be read once, sequentially) onto the disk, then you reboot into a working Win98.
It bundles driver libraries (NIC/sound/video/storage incl. USB/NVMe) that can be injected at
install time — so it also solves 9x's driver problem.
Because the installer is Linux, **it netboots exactly like any Linux distro** — which iPXE does
natively — with no DOS, no packet drivers, and no mid-install media dependency (it's a
single-pass writer, so unlike XP it survives the process fine).
```
iPXE → kernel vmlinuz + initrd (HTTP) → lunmercy writes the image → reboot into Win98
```
### Building the image (on Linux)
QuickInstall images are **derived from an already-set-up Win98 install**, so you either:
- grab a **prebuilt release ISO** from the repo (fastest way to test the pipeline), or
- `git clone` + `./build.sh` against your **Win98 SE ISO** to produce a custom image + boot
media (see the repo's `BUILDING.md`).
Output includes bootable **ISO / USB / floppy** images and the underlying **kernel + initrd**.
### Wiring it into netboot — two ways
Host the files under the appliance's assets dir so they're on `:8080`:
`/home/panxiao81/services/apps/netboot/assets/win98qi/` → `http://192.168.10.127:8080/win98qi/`
**B1. Quick path — `sanboot` the ISO** (good first test). Works here *because* it's a
one-shot Linux writer (the reboot-mid-install problem that kills XP sanboot doesn't apply):
```ipxe
#!ipxe
sanboot http://192.168.10.127:8080/win98qi/win98-quickinstall.iso
```
**B2. Proper path — `kernel`/`initrd` over HTTP.** Extract `vmlinuz` + `initrd` from the ISO
and boot them directly:
```ipxe
#!ipxe
kernel http://192.168.10.127:8080/win98qi/vmlinuz
initrd http://192.168.10.127:8080/win98qi/initrd.gz
imgargs vmlinuz <any source/args lunmercy needs>
boot
```
> **Verify first:** whether `lunmercy` can read the MercyPak image from the **network**
> (HTTP/NFS) via a kernel-cmdline source. If yes → serve the image over HTTP (its sequential
> read design is ideal). If it only reads from the boot medium → either bake the image into
> the `initrd`, or just use the `sanboot`-ISO path (B1), which keeps the image on the virtual CD.
### Adding it to the netboot.xyz menu
netboot.xyz supports a **custom menu**: set `custom_url` in `config/menus/local-vars.ipxe` to a
dir that serves a `custom.ipxe`, and the menu gains a custom entry that chains it.
```ipxe
# in local-vars.ipxe
set custom_url http://192.168.10.127:8080
```
Then host `assets/custom.ipxe` (→ `:8080/custom.ipxe`) containing a menu that chains the B1 or
B2 snippet above. For a one-off test you can also just drop to the **iPXE shell** (menu →
"iPXE shell") and paste the `sanboot`/`kernel` lines directly.
## Test in a VM
Use the `drive-vm` skill (BIOS, IDE disk — 9x is BIOS-only and wants IDE):
```bash
V=~/.claude/skills/drive-vm/scripts/vmctl.sh
# either boot the ISO directly to validate the installer itself:
$V start --name w98 --mem 512 --disk /path/blank.qcow2 --iso /path/win98qi.iso --boot d
# or netboot it (bridge) once the menu/custom entry is wired:
$V start --name w98 --mem 512 --disk /path/blank.qcow2 --bridge br0 --netboot
```
Then screenshot/keydrive through it. 9x is happy with **512 MB RAM** (more can upset it),
a **BIOS** machine, and an **IDE** disk.
## Gotchas
- **BIOS/CSM only** — no UEFI. Legacy boot for both the installer and the target.
- **9x RAM ceiling** — >512 MB–1 GB can cause "insufficient memory" errors; cap the VM/target.
- **Disk geometry** — FAT32; keep the system partition sane (<127 GB is safest for 9x).
- Drivers are handled by quickinstall's driver libs at install time — no F6/packet-driver pain.
- The MercyPak-over-network question (above) is the one thing to confirm before committing to
the pure `kernel`/`initrd`+HTTP path; `sanboot`-ISO always works as a fallback.
+151
View File
@@ -0,0 +1,151 @@
# Installing NT5 (Windows 2000 / XP / Server 2003) over the network
NT5 predates WinPE/`install.wim`/`setup.exe`, so the modern pipeline in `WINDOWS.md` does
**not** apply. It also can't be `sanboot`ed (setup reboots mid-install and loses the virtual
CD). The workable network method that reuses our infra (SMB share + picker) is:
**boot an x86 WinPE → mount the share → run `winnt32.exe /makelocalsource` from the NT5 source.**
`/makelocalsource` copies the whole `i386` tree to the target disk first, so the share isn't
needed after the reboot into the real setup.
```
iPXE (Windows menu) → x86 WinPE (HTTP/wimboot)
└─ startnet.cmd: drvload NIC → net use Z: \\192.168.10.127\win
└─ winnt32.exe /syspart /tempdrive /makelocalsource /unattend:winnt.sif /noreboot
└─ reboot → NT5 text-mode setup → GUI setup (all from local disk)
```
## Hard prerequisite: an x86 WinPE
`winnt32.exe` is **32-bit**. Our built WinPE is **x64**, which has no 32-bit support unless
`WinPE-WoW64` is added (an ADK/Windows step). So you need one of:
- **An x86 WinPE** — built the same way as the x64 one (see `WINDOWS.md` step 1), but from a
**32-bit `boot.wim`**, i.e. a **Win10 x86** (or Win7 x86) ISO. Place it at
`assets/WinPE/x86/` (the netboot.xyz Windows menu's arch toggle switches `${win_arch}` to
`x86`). *No x86 Windows source is on this host yet — this is the missing ingredient.*
- **or** x64 WinPE + `dism /add-package WinPE-WoW64.cab` (Windows box).
Everything else below is editable on Linux (the source tree + answer file live on the share).
## 1. Put the NT5 source on the share
One folder per version under `/mnt/pool/win`, containing the extracted `i386` tree:
```
/mnt/pool/win/win2k/
├── i386/ ← extracted from the Win2000/XP/2003 ISO
├── $OEM$/ ← driver integration (see §3)
│ ├── Textmode/ ← mass-storage F6 driver(s) + txtsetup.oem
│ └── $1/Drivers/ ← PnP drivers (NIC/GPU/chipset) → copied to C:\Drivers
└── winnt.sif ← unattended answer file (§2)
```
```bash
sudo mount -o loop,ro "Windows 2000 ....iso" /mnt/iso
mkdir -p /mnt/pool/win/win2k
cp -a /mnt/iso/I386 /mnt/pool/win/win2k/i386 # case as the ISO presents it
sudo umount /mnt/iso
```
The picker (`menu.cmd`) lists folders with a `setup.exe`; NT5 has no `setup.exe`, so either
add a tiny launcher or just run winnt32 by hand (below).
## 2. `winnt.sif` answer file (driver integration + unattend)
Skeleton — drop it in the version folder. Fill in the `[MassStorageDrivers]` /
`[OEMBootFiles]` from your controller's F6 package's `txtsetup.oem`:
```ini
[Data]
AutoPartition = 0
MsDosInitiated = 0
UnattendedInstall = Yes
[Unattended]
UnattendMode = FullUnattended
OemPreinstall = Yes ; required for $OEM$ processing
OemSkipEula = Yes
FileSystem = LeaveAlone ; or ConvertNTFS
OemPnPDriversPath = Drivers\NIC;Drivers\Chipset ; under C:\Drivers (from $OEM$\$1\Drivers)
[MassStorageDrivers]
"Intel(R) SATA AHCI Controller" = "OEM" ; the exact string from txtsetup.oem
"IDE CD-ROM (ATAPI 1.2)/PCI IDE Controller" = "RETAIL" ; keep inbox IDE too
[OEMBootFiles]
txtsetup.oem
iaahci.inf
iaahci.sys
iaahci.cat
[GuiUnattended]
AdminPassword = *
TimeZone = 210 ; 210 = China Standard Time
OEMSkipRegional = 1
OemSkipWelcome = 1
[UserData]
ProductKey = XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
FullName = "user"
OrgName = "home"
ComputerName = *
[Identification]
JoinWorkgroup = WORKGROUP
[Networking]
InstallDefaultComponents = Yes
```
## 3. Drivers — two separate problems
**a) NIC for WinPE** (so the PE can reach the share): bake it into the x86 `boot.wim` and
`drvload` it — no Windows tooling:
```bash
wimlib-imagex update assets/WinPE/x86/sources/boot.wim 1 --command="add /path/to/nicdrv /Drivers/nic"
# startnet.cmd, BEFORE net use: drvload X:\Drivers\nic\<driver>.inf
```
Use a **PE-compatible** driver (a Win10/7 *x86* NIC driver for an x86 PE) — *not* the XP one.
**b) Storage controller for the TARGET** (the `0x7B` BSOD): NT5 text-mode setup has no inbox
AHCI/NVMe/RAID. Put the controller's **F6 package** in `$OEM$\Textmode\` and reference it from
`[MassStorageDrivers]`/`[OEMBootFiles]` above. All editable on the Linux-hosted share.
**Easiest dodge (recommended where possible):**
- **VM → IDE disk** (`vmctl … --disk` uses `if=ide`) → inbox driver, **skip §3b entirely**.
- **Retro physical → BIOS SATA = IDE/Legacy/Compatibility** → inbox driver. (NT5 is BIOS-only;
targets are old hardware that usually offers this.)
**c) Installed-OS drivers** (NIC/GPU/chipset for the running XP, distinct from the PE's NIC):
put XP-era drivers in `$OEM$\$1\Drivers\…` and list them in `OemPnPDriversPath` (§2). They get
copied to `C:\Drivers` and PnP-installed during GUI setup.
## 4. Run it (inside the x86 WinPE)
The target partition must exist, be **formatted (FAT32/NTFS)** and marked **active**
(`diskpart`: `create partition primary` → `format fs=ntfs quick` → `active`). Then:
```bat
net use Z: \\192.168.10.127\win
Z:\win2k\i386\winnt32 /s:Z:\win2k\i386 /unattend:Z:\win2k\winnt.sif ^
/syspart:C: /tempdrive:C: /makelocalsource /noreboot
```
`/syspart` requires `/tempdrive`. On `/noreboot` completion, reboot the target off its **local
disk** — text-mode setup runs from the copied `$WIN_NT$.~BT`/`~LS`, then GUI setup, no network.
## Gotchas
- **XP/2000/2003 are BIOS/CSM only** — no UEFI. Boot the target (and the PE) in legacy mode.
- winnt32 under WinPE wants `/makelocalsource`; without it, it expects the source to remain
reachable across the reboot (it won't be).
- Chinese media (like the Win2000 ISO here) installs fine; the answer file drives it in that
language. TimeZone 210 = CST.
- **Activation:** volume/retail keys as appropriate; NT5 activation servers are long gone —
use a VL edition/key for hands-off installs.
- If you'd rather not deal with any of this on real hardware: **install NT5 once, capture the
disk, and PXE-boot Clonezilla** (already in the netboot menu) to clone it to targets.
+109
View File
@@ -0,0 +1,109 @@
# netboot
Self-hosted [netboot.xyz](https://netboot.xyz) PXE boot server for the LAN, using the
official appliance container plus a `dnsmasq` proxyDHCP so it coexists with the existing
DHCP server (the **NEC IX router**, which is left untouched).
## Architecture
The NEC IX router keeps leasing IPs. `dnsmasq` runs in **proxyDHCP** mode and only answers
the PXE/boot part of the conversation, pointing clients at this host (`192.168.10.127`).
Client type (announced via DHCP option 60/93) → what it gets:
| Client | Transport | File |
|------------------------------------------|-----------|----------------------------------------------|
| Legacy BIOS (`arch 0`) | TFTP | `netboot.xyz.kpxe` |
| UEFI, normal PXE (`arch 7/9`) | TFTP | `netboot.xyz.efi` |
| UEFI with HTTP Boot (`vendor HTTPClient`)| HTTP | `http://192.168.10.127:8080/menus/netboot.xyz.efi` |
Fallback is automatic: a UEFI box only announces `HTTPClient` when HTTP Boot is actually
enabled/supported; otherwise it does normal PXE and lands on the TFTP `.efi` branch.
This is a **two-stage** chain, which matters for the dnsmasq config:
1. **Firmware → netboot.xyz iPXE.** Raw firmware (not iPXE) gets the binary above. proxyDHCP
*requires* `pxe-service` here — plain `dhcp-boot` produces no boot offer in proxy mode.
2. **netboot.xyz iPXE → menu.** The loaded `.efi`/`.kpxe` re-does DHCP (announcing itself via
option 175) and dnsmasq answers with `dhcp-boot=tag:ipxe,netboot.xyz.efi,,192.168.10.127`.
Two details matter, both dictated by the bootstrap **embedded in the netboot.xyz binary**:
- **The bootfile must be a *recognised binary name*** (`netboot.xyz.efi`), not `menu.ipxe`.
The embedded bootstrap only chains the menu **locally** (its `:tftpmenu` branch) when the
bootfile matches one of its own binary names; any other name skips that branch and boots
the **public** `boot.netboot.xyz` menu instead.
- **The router's DHCP `next-server` must point at `192.168.10.127`** (see below). Under
proxyDHCP the bootstrap fetches its `local-vars.ipxe` from `${next-server}` — the value
from the *real* DHCP server (the NEC IX router), **not** from dnsmasq's
`${proxydhcp/next-server}`. `local-vars.ipxe` is what sets `use_proxydhcp_settings true`
(the no-keypress switch), so if it can't be fetched the UEFI client stalls fetching from
the router, then prompts for a `p` keypress or falls back to the public menu.
The boot binaries then chain the menu **locally** over TFTP from `192.168.10.127`, so clients
boot *this* host's menu, not the public site. Only the version check and distro mirrors reach
the internet.
### Required NEC IX router setting
The router keeps leasing IPs as before, but its DHCP scope for the LAN must advertise
**`next-server 192.168.10.127`** (a.k.a. the `siaddr` / BOOTP server field) on the LAN DHCP
pool. This is the one piece of PXE config the router *does* need — it does not otherwise
PXE-boot anything, and regular (non-PXE) DHCP clients ignore `next-server`. Set it via the
DHCP-server/boot-server (`siaddr`) option of the IX DHCP profile serving the `192.168.10.0/24`
scope; leave the bootfile name unset (dnsmasq's proxyDHCP still supplies it).
## Services (all on host `192.168.10.127`)
| Port | Service | Provided by | Purpose |
|-------------|-----------|------------------------|------------------------------------------|
| `67/udp` | proxyDHCP | `dnsmasq` (host net) | PXE boot offers (no IP leasing) |
| `69/udp` | TFTP | `netbootxyz` | serves `/config/menus` (binaries + menu) |
| `8080` | HTTP | `netbootxyz` nginx | `/` = `/assets` mirror; `/menus/` = binaries (UEFI HTTP Boot) |
| `3000` | Web UI | `netbootxyz` | manage menus / download assets |
## Usage
```bash
docker compose up -d # start
docker compose logs -f dnsmasq # watch DHCP offers during a client boot
docker compose down # stop
```
Config manager (add/update distros, edit menus): <http://192.168.10.127:3000>
## Verified
Both PXE paths were tested end-to-end with QEMU VMs bridged onto `br0` (real SeaBIOS and
OVMF/UEFI firmware), booting all the way to the local netboot.xyz menu:
- **Legacy BIOS** → proxyDHCP offered `netboot.xyz.kpxe` → TFTP → local menu rendered ✅
- **UEFI x64** → proxyDHCP offered `netboot.xyz.efi` → TFTP → local menu rendered ✅
- **UEFI HTTP Boot** → configured, not yet VM-tested (hard to trigger in QEMU).
To re-test: create a tap on `br0`, run a diskless QEMU VM with `-boot n`, and watch
`docker compose logs -f dnsmasq` + `docker logs -f netbootxyz`.
## Notes / gotchas
- **Host networking is required** for `netbootxyz`: TFTP renegotiates to an ephemeral
port that Docker's bridge NAT mangles (clients get `TID mismatch`). Host net serves TFTP
straight off the LAN interface.
- `NGINX_PORT` is ignored by the image; the nginx listen port is pinned to `8080` in
`config/nginx/site-confs/default`, which also adds a `/menus/` location so UEFI HTTP Boot
can fetch the first-stage `.efi`. (If the appliance ever regenerates that file on upgrade,
re-add the `listen 8080` and `/menus/` bits.)
- `dnsmasq` binds **only `br0`** (`interface=br0` + `bind-interfaces`) so it doesn't clash
with libvirt's own dnsmasq on `virbr0`/`virbr1`.
- `config/menus/local-vars.ipxe` sets `use_proxydhcp_settings true` so proxyDHCP clients
boot without a keypress prompt.
- The NEC IX router needs **no PXE configuration** — proxyDHCP handles everything.
- Editing `dnsmasq.conf` requires `docker restart netboot-dnsmasq` (compose won't
auto-recreate on a bind-mounted file content change).
## Files
- `compose.yaml` — the two services (`netbootxyz` + `dnsmasq`)
- `dnsmasq.conf` — proxyDHCP + arch detection
- `config/` — netbootxyz appliance state (menus, nginx conf); managed by the container
- `assets/` — optional locally-mirrored distro images
- `buildout/` — leftover from an earlier manual build; **no longer used** (safe to delete)
+333
View File
@@ -0,0 +1,333 @@
# Installing Windows via netboot.xyz
Works for KVM VMs and physical machines. Windows Setup needs a real filesystem for the
~4 GB `install.wim`, so the flow is: **wimboot → WinPE (HTTP) → SMB media → setup.exe**.
> This covers **NT6+ (Vista/7/8/10/11, Server 2008–2025)**. Older Windows works completely
> differently — see **`NT5.md`** for Windows 2000/XP/Server 2003, and **`9x.md`** for
> Windows 95/98/ME (via win98-quickinstall's Linux installer).
```
iPXE Windows menu
└─ wimboot loads WinPE (boot.wim) over HTTP from this host (assets/WinPE/x64/)
└─ WinPE boots to a cmd prompt; wpeinit brings up the NIC
└─ net use → mount the SMB share with the extracted ISO
└─ setup.exe → installs Windows to the local disk
```
## Already set up on the server (done)
- **SMB share** `\\192.168.10.127\win` — **guests get read-only, passwordless** (WinPE mounts
it this way); the AD user **`panxiao81` has read-write** (`write list = DDUPAN\panxiao81`) for
staging images/media. Backed by ZFS dataset `data/win` → `/mnt/pool/win`. Holds `menu.cmd`
(the version picker) and the `winpe-build/` driver kit. Managed by the `samba_member` Ansible
role (`samba-ad/`), not a hand-edited `smb.conf`.
- **`win_base_url`** = `http://192.168.10.127:8080/WinPE` — set in both
`config/menus/local-vars.ipxe` and `config/menus/boot.cfg` (the latter covers clients whose
firmware is already iPXE and skips `local-vars`).
- **WinPE** built into `assets/WinPE/x64/` (base PE extracted from a Server 2025 ISO; NIC/
storage drivers injected with **DISM** — see step 1 / Driver notes), with a `startnet.cmd`
that brings up networking, auto-mounts the share, and launches the picker.
- Only remaining step for a real install: drop a version folder onto the share (step 2).
## Verified (PXE-tested)
Driven end-to-end in a KVM VM on `br0`: iPXE Windows menu → wimboot loaded the WinPE over
HTTP → WinPE booted → `startnet.cmd` ran `wpeinit`, mounted `\\192.168.10.127\win`, and
launched `menu.cmd`, which showed the (empty) picker. So the whole path works; adding a
version folder makes it installable. Note: give the target **≥4 GB RAM** (2 GB bugchecks the
RAM-loaded WinPE and reboots).
## What you do
### 1. Build WinPE
The WinPE at `assets/WinPE/x64/` is **already built and PXE-tested** (see "Verified" below).
Two ways to (re)build it:
**A. On Linux, no Windows box needed (how it was built here).** A Windows installation ISO's
`sources/boot.wim` *is* a modern WinPE. Extract its bare-PE image with `wimlib-imagex` and
inject a startup script that auto-mounts the share and runs the picker:
```bash
ISO=~/zh-cn_windows_server_2025_..._x64_dvd.iso # any modern Windows/Server ISO
OUT=/home/panxiao81/services/apps/netboot/assets/WinPE/x64
sudo mount -o loop,ro "$ISO" /mnt/winiso
mkdir -p "$OUT/boot" "$OUT/sources"
cp /mnt/winiso/bootmgr "$OUT/bootmgr"
cp /mnt/winiso/bootmgr.efi "$OUT/bootmgr.efi"
cp /mnt/winiso/boot/bcd "$OUT/boot/bcd"
cp /mnt/winiso/boot/boot.sdi "$OUT/boot/boot.sdi"
# export image 1 ("Windows PE") as a single bootable wim
wimlib-imagex export /mnt/winiso/sources/boot.wim 1 "$OUT/sources/boot.wim" --boot
# auto-run our startup: wpeinit + mount \\host\win + launch menu.cmd (startnet.cmd is CRLF)
wimlib-imagex update "$OUT/sources/boot.wim" 1 --command="delete --force /Windows/System32/startnet.cmd"
wimlib-imagex update "$OUT/sources/boot.wim" 1 --command="add /path/to/startnet.cmd /Windows/System32/startnet.cmd"
sudo umount /mnt/winiso
```
Use the **newest** Windows/Server ISO you have — a WinPE installs any OS at or below its
version. Drivers are injected separately with **DISM** (see **Driver notes**), so the baked-in
`startnet.cmd` no longer needs `drvload` — it just brings up networking (with a DHCP retry
loop, since a freshly-loaded NIC can be a few seconds behind the first DISCOVER) and launches
the picker:
```bat
wpeinit REM PnP auto-loads the DISM-injected NIC driver
reg add HKLM\SYSTEM\CurrentControlSet\Services\LanmanWorkstation\Parameters /v AllowInsecureGuestAuth /t REG_DWORD /d 1 /f
:netwait REM retry until a real 192.168.10.x lease appears
wpeutil InitializeNetwork
ipconfig | find "192.168.10." >nul && goto neton
ipconfig /renew >nul & ping 127.0.0.1 -n 4 >nul & goto netwait
:neton
net use Z: \\192.168.10.127\win
if exist Z:\menu.cmd call Z:\menu.cmd
cmd
```
**B. On a Windows box (Windows ADK).** `copype amd64 C:\winpe` → `MakeWinPEMedia /ISO ...`,
then copy the ISO/media contents into `assets/WinPE/x64/`. Edit `boot.wim`'s
`Windows\System32\startnet.cmd` to the same script as above. Use this if you want ADK's
optional components or a custom PE. (This same ADK box is where drivers get DISM-injected —
see Driver notes.)
Either way the tree must be:
```
assets/WinPE/x64/
├── bootmgr
├── bootmgr.efi
├── boot/bcd (BCD store — the menu also tries Boot/BCD)
├── boot/boot.sdi
└── sources/boot.wim (your WinPE image, single bootable index)
```
netboot.xyz loads exactly those five files from `${win_base_url}/x64/`. (`wimboot` itself is
fetched from public `boot.netboot.xyz` — fine as long as the host has internet.) Give WinPE
**≥4 GB RAM** on the target — the wim is RAM-loaded and 2 GB bugchecks → reboot.
### 2. Populate the SMB share with install media
Put each Windows version in **its own subfolder** under `/mnt/pool/win` — extract the ISO
*files* (not the .iso). One WinPE installs all of them; you do NOT need a WinPE per version.
```bash
sudo mount -o loop Win11_24H2.iso /mnt/iso
mkdir -p /mnt/pool/win/win11-24h2
cp -a /mnt/iso/. /mnt/pool/win/win11-24h2/
sudo umount /mnt/iso
# repeat for win10-22h2/, server2022/, server2025/, ...
```
Result:
```
/mnt/pool/win/
├── menu.cmd ← version picker (already installed)
├── win11-24h2/ ← setup.exe, sources/install.wim, ...
├── win10-22h2/
└── server2025/
```
Editions (Home/Pro/Enterprise) usually live inside one ISO's `install.wim`; `setup.exe`
lets you pick, so they don't need separate folders. See "Multiple versions" below.
### 3. Boot a target → install
1. PXE boot → **Windows** → **Load Microsoft Windows Installer** (uses `win_base_url`).
On real hardware confirm WinPE actually got a `192.168.10.x` (`ipconfig`); if the onboard
NIC won't network, use a **USB Ethernet dongle** — see **Driver notes**.
2. At the WinPE `cmd` prompt (startnet usually does this for you):
```bat
wpeinit
net use Z: \\192.168.10.127\win
Z:\menu.cmd REM pick a version; launches <folder>\sources\setup.exe
```
3. Pick a version → click through Setup → install to the local disk.
4. **After Setup's first reboot, boot the LOCAL DISK, not PXE** — otherwise it loops back into
netboot and the install looks like it "restarted." (One-time boot menu, or move the disk
above the network in the BIOS boot order.)
> **Win11 24H2/25H2 gotcha — launch `sources\setup.exe`, not the media-root `setup.exe`.** In
> 24H2+ the root `setup.exe` is the new "modern setup" front-end, meant for booting from real
> USB/DVD media or upgrading from within Windows; started from a bare WinPE prompt it **exits
> partway** ("quits in half"). The classic engine at `<folder>\sources\setup.exe` is PE-friendly.
> `menu.cmd` already prefers `sources\setup.exe` (falling back to the root one for older media).
## Multiple Windows versions
One x64 WinPE handles every x64 Windows (10/11, Server 2019–2025, all editions) — as long
as the WinPE is at least as new as the newest OS you install. Manage versions purely as the
folder library on the share; `menu.cmd` auto-lists every subfolder that contains a
`setup.exe` and launches the one you choose.
**Make it hands-off** by baking the mount + picker into WinPE so every boot lands on the
menu. When building WinPE, edit `mount\Windows\System32\startnet.cmd` (in the mounted
`boot.wim`) to:
```bat
wpeinit
rem allow passwordless (guest) SMB from WinPE
reg add HKLM\SYSTEM\CurrentControlSet\Services\LanmanWorkstation\Parameters /v AllowInsecureGuestAuth /t REG_DWORD /d 1 /f
net use Z: \\192.168.10.127\win
Z:\menu.cmd
```
**Unattended per version:** drop an `autounattend.xml` in a version folder and launch it with
`setup.exe /unattend:%~dp0autounattend.xml` (you can add per-folder entries to `menu.cmd`).
Each version can have its own answer file (edition index, product key, partitioning).
**x86 / ARM64:** only these need a second WinPE — place it in `assets/WinPE/x86/` (the Windows
menu's arch toggle switches `${win_arch}`). Rarely needed.
**Advanced — per-version entries in the iPXE menu** (choose the version *before* WinPE, e.g.
for fully automated imaging): pass a config into WinPE via extra `initrd` lines in
`windows.ipxe` so WinPE auto-installs a specific folder. See
[netbootxyz discussion #757](https://github.com/netbootxyz/netboot.xyz/discussions/757).
For interactive use, the `menu.cmd` picker is simpler and needs no iPXE changes.
## Driver notes (mainly physical machines)
WinPE must have the target's **NIC driver** (to reach the share) and Setup must have the
**storage driver** (to see the disk). VMs rarely need this; real hardware often does.
The build's reduced WinPE driver set is missing most modern **Intel** desktop NICs: it ships
`e1i`/`e1e`/`e1g` (I350/82575/8257x-era) but **not** `e1d` (I217/I218/**I219**) or `e2f`
(**I225/I226** 2.5G). Symptom: WinPE boots but `net use` fails because there is no link —
no NIC was ever loaded. Realtek onboard NICs (RTL8111/8168/8125) are likewise absent, and so
is **virtio-net** (needed for KVM installs with a virtio NIC).
### How the current image gets its drivers: DISM injection on `winadmin`
Drivers are injected into `boot.wim`'s driver store with **DISM** on the Windows ADK box
(`winadmin`, `192.168.10.6`). This is the proper method: they become real PnP drivers that load
automatically at boot — no `drvload`. A ready-to-run **build kit** lives on the share at
`\\192.168.10.127\win\winpe-build\`:
```
winpe-build/
├── boot.wim ← image to service (copy of the live one)
├── drivers/
│ ├── Intel-1G/ e1dn (I219 — but see ⚠ box), e1r (I210/211/350), v1q (82575/6/80)
│ ├── Intel-2.5G/ e2f (I225/I226) NDIS68
│ ├── virtio-NetKVM/ netkvm (virtio-net) + netkvmp.exe/netkvmco.exe
│ ├── virtio-viostor/ viostor (virtio-blk)
│ └── virtio-vioscsi/ vioscsi (virtio-scsi)
├── startnet.cmd ← no-drvload version (PnP loads drivers; DHCP retry loop)
├── build-winpe.cmd ← one-click DISM script
└── READ-ME-FIRST.txt
```
Rebuild on `winadmin` (the DISM mount dir must be **local**, not the share):
```bat
robocopy \\192.168.10.127\win\winpe-build C:\winpe-build /E
:: Start menu -> "Deployment and Imaging Tools Environment" -> Run as administrator
cd /d C:\winpe-build
build-winpe.cmd :: mounts boot.wim, drops any old \Drivers tree, DISM /add-driver, commits
copy /y C:\winpe-build\boot.wim \\192.168.10.127\win\winpe-build\boot.new.wim
```
Then on this host, back up the live image and swap it in (netboot serves it statically — no
restart, and a size change is fine, the BCD loads `boot.wim` by name):
```bash
cd assets/WinPE/x64/sources
cp -a boot.wim boot.wim.prev
cp /mnt/pool/win/winpe-build/boot.new.wim boot.wim
```
`build-winpe.cmd` is essentially:
```bat
dism /Mount-Image /ImageFile:.\boot.wim /Index:1 /MountDir:.\mount
rmdir /s /q .\mount\Drivers :: drop any old drvload tree
copy /y .\startnet.cmd .\mount\Windows\System32\startnet.cmd
dism /Image:.\mount /Add-Driver /Driver:.\drivers /Recurse /ForceUnsigned
dism /Image:.\mount /Get-Drivers :: confirm the NIC driver is listed
dism /Unmount-Image /MountDir:.\mount /Commit
```
Use the **current Win11 24H2 / 10.1.26100 ADK** — servicing a 26100 `boot.wim` with an older
DISM fails (*"image version is higher than the DISM version"*).
> **⚠ Verdict — the onboard Intel I219 does NOT work in this (build-26100) WinPE with any
> driver. Use a USB Ethernet dongle.** On a real I219 (`DEV_550B`, recent Lenovo board) all
> three Intel drivers failed to move a single frame in *either* direction (no DHCP; a static-IP
> ping gets no ARP reply — confirmed with `tcpdump` on the host, which saw nothing from the NIC's
> MAC):
> - `e1dn` v20.0.3.24 **and** the Lenovo-OEM `e1dn` v20.0.2.19 → link shows "connected", **no traffic**.
> - `e1d` v12.19.2.65 → **no traffic**, and `netsh …set interface admin=disabled` **bugchecks with
> `PNP_WATCHDOG`** (the driver can't even cleanly stop the device).
>
> PXE firmware works on the same port (its own minimal driver), so it's specifically the I219
> datapath under build-26100 WinPE — a known regression on newer PE builds. **Fix: a USB GbE
> dongle** (Realtek RTL8153/8156 is inbox in WinPE 26100 — ours worked with nothing injected).
> The onboard I219 is fine once *real* Windows is installed. An older WinPE base (Win10 22H2 /
> Server 2022, build ≤20348) *might* also work but is untested.
Intel driver sources: **Intel Wired driver 31.2**
(`downloadmirror.intel.com/921523/Wired_driver_31.2_x64.zip`), `PRO1000\Winx64\NDIS68` +
`PRO2500\Winx64\NDIS68` subfolders; the I219 `e1dn` in the current kit is the Lenovo OEM package
(`e1dn` 20.0.2.19). virtio drivers from `virtio-win-0.1.285.iso` (`~/virtio-win-0.1.285.iso`),
`<driver>/w11/amd64` folders — **keep `netkvmp.exe`**, `netkvm.inf`'s `[CopyFiles]` requires it.
**State:** the live `boot.wim` is a DISM build whose driver store (`oem*.inf`) holds `e1dn`
(OEM I219 — moot, see box), `e1r`/`v1q` (I210/211/350), `e2f` (I225/226) and
`netkvm`/`viostor`/`vioscsi` (virtio) — so it still covers other Intel NICs and KVM VMs, and the
`e1d` that `PNP_WATCHDOG`'d is deliberately excluded. The I219 machine installs via a **USB
dongle**, and Setup ran once the picker used `sources\setup.exe`. (An earlier `drvload` build was
VM-verified with a virtio-net NIC reaching the share.)
### Alternative: `drvload` at runtime (Linux build, no Windows box)
The image can also be built entirely on Linux with `wimlib-imagex`: stage the driver
`.inf`/`.sys`/`.cat` files inside `boot.wim` under `\Drivers` and `drvload` them from
`startnet.cmd` before networking. This is how it was *first* built — it's the fallback, since
`drvload` only loads into the running PE (no persistent driver store) and can load several
matching drivers at once, leaving PnP to bind whichever it ranks highest (which is how the flaky
`e1dn` got picked early on):
```bat
for /r X:\Drivers %%i in (*.inf) do drvload "%%i" REM filename-agnostic; non-matching INFs fail harmlessly
wpeinit
wpeutil InitializeNetwork
```
```bash
WIM=assets/WinPE/x64/sources/boot.wim
printf '%s\n' \
"add /path/to/stage/Drivers /Drivers" \
"delete --force /Windows/System32/startnet.cmd" \
"add /path/to/startnet.cmd /Windows/System32/startnet.cmd" \
| wimlib-imagex update "$WIM" 1
```
> **Gotcha `drvload` fails with `0x80070002` (FILE_NOT_FOUND):** the INF's `[CopyFiles]`
> references a file you trimmed (e.g. NetKVM's `netkvmp.exe`). Keep the whole driver folder —
> strip only `*.pdb`/readme.
### Storage drivers
- Storage/RAID (Intel VMD/RST) drivers usually also need loading in Setup (or slipstream into
`install.wim`). NVMe/AHCI are typically inbox.
- **KVM:** using virtio disk/NIC → the virtio drivers above are baked in; or give the VM a
**SATA disk + e1000 NIC** (both inbox) to skip driver work entirely.
## Gotcha: guest SMB from WinPE
Modern Windows blocks "insecure guest" logons by policy. WinPE usually allows it, but if
`net use` fails with **system error 1272 / 5**, enable it in the running WinPE:
```bat
reg add HKLM\SYSTEM\CurrentControlSet\Services\LanmanWorkstation\Parameters /v AllowInsecureGuestAuth /t REG_DWORD /d 1 /f
```
(To make it permanent, set the same key offline in the mounted `boot.wim`.) Alternatively,
switch the share to a real user + password — but do it in the `samba_member` role
(`samba-ad/`), **not** `/etc/samba/smb.conf` directly (Ansible regenerates that file). The
`[win]` share already grants the AD user `panxiao81` read-write via `write list`.
## Later: unattended installs
Drop an `autounattend.xml` at the root of the SMB media (or bake into WinPE via
`startnet.cmd` running `wpeinit` + `net use` + `setup.exe /unattend:...`) for zero-touch.
+81
View File
@@ -0,0 +1,81 @@
#!/bin/bash
cat << EOF
#########################################################################################################
# Create PXE bootable Proxmox image including ISO #
# #
# Author: mrballcb @ Proxmox Forum (06-12-2012) #
# Thread: http://forum.proxmox.com/threads/8484-Proxmox-installation-via-PXE-solution?p=55985#post55985 #
# Modified: morph027 @ Proxmox Forum (23-02-2015) to work with 3.4 #
#########################################################################################################
EOF
if [ ! $# -eq 1 ]; then
echo -ne "Usage: bash pve-iso-2-pxe.sh /path/to/pve.iso\n\n"
exit 1
fi
BASEDIR="$(dirname "$(readlink -f "$1")")"
pushd "$BASEDIR" >/dev/null || exit 1
[ -L "proxmox.iso" ] && rm proxmox.iso &>/dev/null
for ISO in *.iso; do
if [ "$ISO" = "*.iso" ]; then continue; fi
if [ "$ISO" = "proxmox.iso" ]; then continue; fi
echo "Using ${ISO}..."
ln -s "$ISO" proxmox.iso
done
if [ ! -f "proxmox.iso" ]; then
echo "Couldn't find a proxmox iso, aborting."
echo "Add /path/to/iso_dir to the commandline."
exit 2
fi
rm -rf pxeboot
[ -d pxeboot ] || mkdir pxeboot
pushd pxeboot >/dev/null || exit 1
echo "extracting kernel..."
if [ -x $(which isoinfo) ] ; then
isoinfo -i ../../infrastructure/proxmox.iso -R -x /boot/linux26 > linux26 || exit 3
else
7z x ../../infrastructure/proxmox.iso boot/linux26 -o/tmp || exit 3
mv /tmp/boot/linux26 /tmp/
fi
echo "extracting initrd..."
if [ -x $(which isoinfo) ] ; then
isoinfo -i ../../infrastructure/proxmox.iso -R -x /boot/initrd.img > /tmp/initrd.img
else
7z x ../../infrastructure/proxmox.iso boot/initrd.img -o/tmp
mv /tmp/boot/initrd.img /tmp/
fi
mimetype="$(file --mime-type --brief /tmp/initrd.img)"
case "${mimetype##*/}" in
"zstd"|"x-zstd")
decompress="zstd -d /tmp/initrd.img -c"
;;
"gzip"|"x-gzip")
decompress="gzip -S img -d /tmp/initrd.img -c"
;;
*)
echo "unable to detect initrd compression method, exiting"
exit 1
;;
esac
$decompress > initrd || exit 4
echo "adding iso file ..."
if [ -x $(which cpio) ] ; then
echo "../../infrastructure/proxmox.iso" | cpio -L -H newc -o >> initrd || exit 5
else
7z x "../../infrastructure/proxmox.iso" >> initrd || exit 5
fi
popd >/dev/null 2>&1 || exit 1
echo "Finished! pxeboot files can be found in ${PWD}."
popd >/dev/null 2>&1 || true # don't care if these pops fail
popd >/dev/null 2>&1 || true
+33
View File
@@ -0,0 +1,33 @@
#!ipxe
# ─────────────────────────────────────────────────────────────────────────
# Proxmox VE 9.2 install — pve1 (Intel NUC) → 192.168.10.4, target sdb
#
# The NUC's UEFI CANNOT unpack a ~1.8GB initramfs — it dies with
# "initramfs unpacking failed: write error", leaving a TRUNCATED /proxmox.iso,
# which then fails to loop-mount → "no device with valid ISO found".
# (The ISO itself is fine and the HTTP transfer completes — verified in the
# nginx log. RAM is fine too: 15881 MB. It's a firmware/early-boot limit.)
#
# So we DON'T ship the ISO in the initrd here. We boot only the kernel + a
# lean initrd augmented with e1000e (the stock installer initrd has NO network
# drivers at all). The ISO search fails, init drops to a debug shell, and you
# run ONE command:
#
# sh /netfetch.sh
#
# which brings up the NIC, streams the ISO onto /dev/sda (the 1TB HDD), and
# re-execs init — which then finds it via the normal block-device path and
# auto-installs to sdb. Nothing oversized ever goes through initramfs.
# ─────────────────────────────────────────────────────────────────────────
echo
echo Proxmox VE 9.2 — pve1 (NUC) network-staged install
echo Loading kernel + net-enabled initrd (no ISO in initramfs)...
echo
echo ">>> At the debug shell that appears, type: sh /netfetch.sh"
echo
kernel http://192.168.10.127:8080/proxmox/linux26 ro ramdisk_size=16777216 rw quiet splash=silent proxmox-start-auto-installer
initrd http://192.168.10.127:8080/proxmox/initrd-net.img
boot || goto failed
:failed
echo Proxmox netboot failed. Dropping to shell.
shell
+16
View File
@@ -0,0 +1,16 @@
#!ipxe
# ─────────────────────────────────────────────────────────────────────────
# Proxmox VE 9.2 AUTOMATED install — pve1 → 192.168.10.4 (target: sdb, the SATA SSD)
# Same --pxe-style method: lean kernel + gzip initrd, then the answer-embedded
# ISO as a second initrd (proxmox.iso). WIPES sdb (HDD sda untouched).
# NOTE: pve1 is the SSH jump host / build box — reinstall it LAST.
# ─────────────────────────────────────────────────────────────────────────
echo
echo Proxmox VE 9.2 AUTOMATED install: pve1 -> 192.168.10.4 (wipes sdb)
kernel http://192.168.10.127:8080/proxmox/linux26 ro ramdisk_size=16777216 rw quiet splash=silent proxmox-start-auto-installer
initrd http://192.168.10.127:8080/proxmox/initrd.img
initrd http://192.168.10.127:8080/proxmox/prepared-pve1.iso proxmox.iso
boot || goto failed
:failed
echo Proxmox netboot failed. Dropping to shell.
shell
+16
View File
@@ -0,0 +1,16 @@
#!ipxe
# ─────────────────────────────────────────────────────────────────────────
# Proxmox VE 9.2 AUTOMATED install — pve2 → 192.168.10.7 (target: nvme0n1)
# Same --pxe-style method as pve3: lean kernel + gzip initrd, then the
# answer-embedded ISO as a second initrd (proxmox.iso). WIPES nvme0n1.
# Reinstalling pve2 also vacates the contested 192.168.10.5.
# ─────────────────────────────────────────────────────────────────────────
echo
echo Proxmox VE 9.2 AUTOMATED install: pve2 -> 192.168.10.7 (wipes nvme0n1)
kernel http://192.168.10.127:8080/proxmox/linux26 ro ramdisk_size=16777216 rw quiet splash=silent proxmox-start-auto-installer
initrd http://192.168.10.127:8080/proxmox/initrd.img
initrd http://192.168.10.127:8080/proxmox/prepared-pve2.iso proxmox.iso
boot || goto failed
:failed
echo Proxmox netboot failed. Dropping to shell.
shell
+19
View File
@@ -0,0 +1,19 @@
#!ipxe
# ─────────────────────────────────────────────────────────────────────────
# Proxmox VE 9.2 AUTOMATED install — pve3 → 192.168.10.9 (target: nvme0n1)
# Replicates the (unreleased) `proxmox-auto-install-assistant --pxe` method:
# lean kernel + lean gzip initrd, then the answer-embedded ISO loaded as a
# SECOND initrd named proxmox.iso (iPXE's native multi-initrd = correct cpio).
# The installer finds /proxmox.iso, loop-mounts it, reads the embedded answer,
# and installs unattended (proxmox-start-auto-installer). WIPES nvme0n1.
# ─────────────────────────────────────────────────────────────────────────
echo
echo Proxmox VE 9.2 AUTOMATED install: pve3 -> 192.168.10.9 (wipes nvme0n1)
echo Loading kernel + lean initrd + ISO (as proxmox.iso)...
kernel http://192.168.10.127:8080/proxmox/linux26 ro ramdisk_size=16777216 rw quiet splash=silent proxmox-start-auto-installer
initrd http://192.168.10.127:8080/proxmox/initrd.img
initrd http://192.168.10.127:8080/proxmox/prepared-pve3.iso proxmox.iso
boot || goto failed
:failed
echo Proxmox netboot failed. Dropping to shell.
shell
+32
View File
@@ -0,0 +1,32 @@
services:
# Official netboot.xyz appliance: TFTP (:69/udp), assets nginx (:8080), web UI (:3000).
# HOST networking is required — TFTP renegotiates to an ephemeral port that Docker's
# bridge NAT mangles (clients get "TID mismatch"). Host net serves TFTP straight off
# the LAN interface. Does NOT provide DHCP (see the dnsmasq service below).
netbootxyz:
image: ghcr.io/netbootxyz/netbootxyz
container_name: netbootxyz
restart: unless-stopped
network_mode: host
environment:
TZ: Asia/Tokyo
NGINX_PORT: "8080" # assets mirror (host port, host net)
WEB_APP_PORT: "3000" # web configuration UI
volumes:
- "./config:/config"
- "./assets:/assets"
# proxyDHCP: runs ALONGSIDE the NEC IX router's DHCP. The router leases IPs; dnsmasq
# only answers the PXE/boot part and points clients at this host (192.168.10.127).
dnsmasq:
image: 4km3/dnsmasq:2.90-r3
container_name: netboot-dnsmasq
restart: unless-stopped
network_mode: host # must see LAN DHCP broadcasts (scoped to br0 in the conf)
cap_add:
- NET_ADMIN
command: ["-k", "--conf-file=/etc/dnsmasq.conf"]
volumes:
- "./dnsmasq.conf:/etc/dnsmasq.conf:ro"
depends_on:
- netbootxyz
+2
View File
@@ -0,0 +1,2 @@
429: Too Many Requests
For more on scraping GitHub and how it may affect your rights, please review our Terms of Service (https://docs.github.com/en/site-policy/github-terms/github-terms-of-service).
+219
View File
@@ -0,0 +1,219 @@
#!ipxe
:global_vars
# set site name
set site_name netboot.xyz
# set boot domain
set boot_domain boot.netboot.xyz/3.0.2
# set location of memdisk
set memdisk http://${boot_domain}/memdisk
# set location of custom netboot.xyz live assets, override in local-vars.ipxe
isset ${live_endpoint} || set live_endpoint https://github.com/netbootxyz
# default Windows install source (local WinPE); local-vars.ipxe may override
isset ${win_base_url} || set win_base_url http://192.168.10.127:8080/WinPE
# signature check enabled?
set sigs_enabled false
# disable signature checks if SecureBoot is active, as imgverify is not
# available when booting via iPXE upstream's official Secure Boot image
iseq ${efi/SecureBoot} 01 && set sigs_enabled false ||
# set location of signatures for sources
set sigs http://${boot_domain}/sigs/
# set location of latest iPXE
iseq ${platform} efi && set ipxe_disk netboot.xyz-snponly.efi || set ipxe_disk netboot.xyz-undionly.kpxe
# set default boot timeout
isset ${boot_timeout} || set boot_timeout 300000
##################
# official mirrors
##################
:mirrors
### AlmaLinux
set almalinux_mirror http://repo.almalinux.org
set almalinux_base_dir almalinux
### Alpine Linux
set alpinelinux_mirror http://dl-cdn.alpinelinux.org
set alpinelinux_base_dir alpine
### Arch Linux
set archlinux_mirror mirrors.kernel.org
set archlinux_base_dir archlinux
### CentOS Stream
set centos_mirror https://mirror.stream.centos.org
set centos_base_dir
### CentOS Stream CoreOS
set scos_mirror https://cloud.centos.org
set scos_base_dir centos/scos
### Debian
set debian_mirror http://deb.debian.org
set debian_base_dir debian
### Devuan
set devuan_mirror http://deb.devuan.org
set devuan_base_dir devuan
### Fedora
set fedora_mirror http://mirrors.kernel.org
set fedora_base_dir fedora
### Fedora CoreOS
set coreos_mirror https://builds.coreos.fedoraproject.org
set coreos_base_dir prod/streams
### FreeDOS
set freedos_mirror http://www.ibiblio.org
set freedos_base_dir pub/micro/pc-stuff/freedos/files/distributions/1.4
### IPFire
set ipfire_mirror https://downloads.ipfire.org
set ipfire_base_dir releases/ipfire-2.x
### Kali Linux
set kali_mirror http://http.kali.org
set kali_base_dir kali
### Mageia
set mageia_mirror http://mirrors.kernel.org
set mageia_base_dir mageia
### OpenBSD
set openbsd_mirror http://cdn.openbsd.org
set openbsd_base_dir pub/OpenBSD
### openEuler
set openEuler_mirror http://repo.openeuler.org
set openEuler_base_dir
### openSUSE
set opensuse_mirror http://download.opensuse.org
set opensuse_base_dir distribution/leap
### Red Hat Enterprise Linux CoreOS
set rhcos_mirror https://mirror.openshift.com
set rhcos_base_dir pub/openshift-v
### Rocky Linux
set rockylinux_mirror http://download.rockylinux.org
set rockylinux_base_dir pub/rocky
### Slackware
set slackware_mirror http://mirrors.kernel.org
set slackware_base_dir slackware
### SmartOS
set smartos_mirror https://netboot.smartos.org/os/
set smartos_base_dir /platform/i86pc/
### Ubuntu
set ubuntu_mirror http://archive.ubuntu.com
set ubuntu_base_dir ubuntu
#################################################
# determine architectures and enable menu options
#################################################
:architectures
set menu_linux 1
set menu_bsd 1
set menu_unix 1
set menu_freedos 1
set menu_live 1
set menu_pci 1
set menu_windows 1
set menu_utils 1
iseq ${arch} i386 && goto i386 ||
iseq ${arch} x86_64 && goto x86_64 ||
iseq ${arch} arm64 && goto arm64 ||
goto architectures_end
:x86_64
set menu_linux_i386 0
iseq ${platform} efi && goto efi ||
goto architectures_end
:i386
set menu_linux 0
set menu_linux_i386 1
set menu_bsd 1
set menu_unix 0
set menu_freedos 1
set menu_live 0
set menu_windows 0
set menu_utils 1
iseq ${platform} efi && goto efi ||
goto architectures_end
:arm64
set menu_linux 0
set menu_linux_arm 1
set menu_unix 0
set menu_freedos 0
set menu_live 0
set menu_live_arm 1
set menu_windows 0
set menu_utils 0
set menu_utils_arm 1
set menu_pci 0
iseq ${platform} efi && goto efi ||
goto architectures_end
:efi
set menu_bsd 1
set menu_freedos 0
set menu_unix 0
set menu_pci 0
goto architectures_end
:architectures_end
goto clouds
###################################
# set iPXE cloud provider specifics
###################################
:clouds
iseq ${ipxe_cloud_config} gce && goto gce ||
iseq ${ipxe_cloud_config} metal && goto metal ||
iseq ${ipxe_cloud_config} packet && goto metal ||
goto clouds_end
:gce
set cmdline console=ttyS0,115200n8
goto clouds_end
:metal
iseq ${arch} i386 && goto metal_x86_64 ||
iseq ${arch} x86_64 && goto metal_x86_64 ||
iseq ${arch} arm64 && goto metal_arm64 ||
goto clouds_end
:metal_x86_64
set cmdline console=ttyS1,115200n8
iseq ${platform} efi && set ipxe_disk netboot.xyz-metal-snp.efi || set ipxe_disk netboot.xyz-metal.kpxe
set menu_linux_i386 0
set menu_freedos 0
set menu_windows 0
iseq ${platform} efi && set menu_pci 0 ||
goto clouds_end
:metal_arm64
set cmdline console=ttyAMA0,115200
set ipxe_disk netboot.xyz-metal-arm64-snp.efi
set menu_bsd 1
set menu_freedos 0
set menu_live 0
set menu_windows 0
set menu_utils 0
set menu_pci 0
goto clouds_end
:clouds_end
goto end
:end
exit
+22
View File
@@ -0,0 +1,22 @@
#!ipxe
### local overrides for this self-hosted netboot.xyz instance
# Use the proxyDHCP-provided TFTP server (192.168.10.127) without prompting for a keypress
set use_proxydhcp_settings true
# Windows: where wimboot fetches WinPE from (files live in assets/WinPE/x64/,
# served by the appliance nginx on :8080). The Windows menu appends /x64/...
set win_base_url http://192.168.10.127:8080/WinPE
# ─── Proxmox VE 9.2 reinstall auto-boot (per-node MAC-match) ───
# Re-add ONE line at a time, only for the node you're actively reinstalling, then
# remove it once that node is up — this prevents a network-boot reinstall loop.
# Runs after DHCP so ${mac} is set; non-matching machines fall through (||) to the menu.
# iseq ${mac} 6c:4b:90:c9:f8:0a && chain --replace http://192.168.10.127:8080/proxmox/pve3.ipxe || # pve3 -> .9
# iseq ${mac} 6c:4b:90:c9:f8:53 && chain --replace http://192.168.10.127:8080/proxmox/pve2.ipxe || # pve2 -> .7
# pve1 (NUC) needs pve1-net.ipxe, NOT pve1.ipxe: its UEFI cannot unpack a
# ~1.8GB initramfs ("initramfs unpacking failed: write error"), so the ISO
# must be staged to a local disk over the network instead of riding along in
# the initrd. See services/netboot/assets/proxmox/pve1-net.ipxe for details.
# iseq ${mac} b8:ae:ed:ea:0f:30 && chain --replace http://192.168.10.127:8080/proxmox/pve1-net.ipxe || # pve1 -> .4
# ALL THREE NODES REINSTALLED 2026-07-25 — every line above is disarmed on purpose.
+1
View File
@@ -0,0 +1 @@
3.0.2
+26
View File
@@ -0,0 +1,26 @@
user nbxyz;
worker_processes 4;
pid /run/nginx.pid;
include /etc/nginx/modules/*.conf;
events {
worker_connections 768;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 0;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /config/log/nginx/access.log;
error_log /config/log/nginx/error.log;
gzip on;
gzip_disable "msie6";
include /config/nginx/site-confs/*;
}
daemon off;
@@ -0,0 +1,12 @@
server {
listen 8080;
location / {
root /assets;
autoindex on;
}
# menus/binaries over HTTP — for UEFI HTTP Boot (serves the first-stage .efi)
location /menus/ {
alias /config/menus/;
autoindex on;
}
}
+58
View File
@@ -0,0 +1,58 @@
# ─── netboot proxyDHCP ───────────────────────────────────────────────────
# Runs ALONGSIDE the NEC IX router's DHCP. The router leases IPs; dnsmasq only
# answers the PXE/boot half and hands out NO addresses. TFTP + HTTP + the local
# menu are all served by the netbootxyz container on this host (192.168.10.127).
port=0 # disable dnsmasq's DNS server entirely
log-dhcp # verbose boot logging (comment out once stable)
log-facility=- # send logs to stderr so `docker logs` shows them
interface=br0 # LAN bridge ONLY
bind-interfaces # don't bind virbr0/virbr1 (libvirt's dnsmasq owns :67 there)
dhcp-range=192.168.10.0,proxy # proxyDHCP on the LAN subnet — assigns no leases
# ─── client classification ───────────────────────────────────────────────
dhcp-match=set:ipxe,175 # our 2nd-stage iPXE sets option 175
dhcp-match=set:httpboot,option:vendor-class,HTTPClient # UEFI HTTP Boot firmware
dhcp-option=tag:httpboot,60,HTTPClient # echo the vendor class back
# architecture (DHCP option 93) → EFI vs legacy BIOS, so stage 2 hands an iPXE
# client a binary it can actually execute (a BIOS iPXE cannot run a UEFI .efi).
dhcp-match=set:efi,option:client-arch,6 # UEFI ia32
dhcp-match=set:efi,option:client-arch,7 # UEFI x86-64
dhcp-match=set:efi,option:client-arch,9 # UEFI x86-64 (alt)
dhcp-match=set:efi,option:client-arch,11 # UEFI arm64
# ─── stage 1: raw firmware (NOT iPXE) → hand it the netboot.xyz binary ────
# proxyDHCP REQUIRES pxe-service (not dhcp-boot) to emit a boot offer. The TFTP
# server defaults to this dnsmasq host (192.168.10.127) = the container's TFTP.
pxe-prompt="Booting netboot.xyz...",0 # 0s timeout — no keypress
pxe-service=tag:!ipxe,x86PC,"netboot.xyz (BIOS)",netboot.xyz.kpxe
# NOTE: use the SNPONLY EFI build (SNP/UNDI = firmware NIC driver) instead of the
# all-drivers netboot.xyz.efi. iPXE's NATIVE driver hangs on some NICs (Intel I219
# in the NUC froze right after "autoexec.ipxe not found"); snponly reuses the
# firmware's own driver and boots reliably. Realtek ThinkCentres work either way.
pxe-service=tag:!ipxe,BC_EFI,"netboot.xyz (UEFI)",netboot.xyz-snponly.efi
pxe-service=tag:!ipxe,X86-64_EFI,"netboot.xyz (UEFI)",netboot.xyz-snponly.efi
# UEFI HTTP Boot firmware → fetch the first-stage .efi over HTTP
dhcp-boot=tag:httpboot,tag:!ipxe,http://192.168.10.127:8080/menus/netboot.xyz-snponly.efi
# ─── stage 2: our netboot.xyz iPXE re-requests → give it OUR next-server ──
# The .efi's embedded bootstrap fetches local-vars.ipxe from ${next-server} (the
# REAL DHCP server = the NEC IX router), NOT from ${proxydhcp/next-server}. So the
# router's DHCP next-server MUST be set to 192.168.10.127 (see README) — otherwise
# local-vars is fetched from the router (no TFTP → fails) and, lacking
# use_proxydhcp_settings, the bootstrap prompts for a 'p' keypress / falls back to
# the public menu.
#
# The bootfile MUST be a name the bootstrap recognises (netboot.xyz.efi / .kpxe) so
# it reaches its :tftpmenu branch and chains menu.ipxe LOCALLY. A non-binary name
# like menu.ipxe skips :tftpmenu and boots the PUBLIC boot.netboot.xyz menu instead.
# The server field (192.168.10.127) sets ${proxydhcp/next-server}.
#
# Split by arch: the netboot.xyz bootstrap only string-matches these names (never
# execs them), but an ALREADY-iPXE client (e.g. a firmware iPXE ROM) autoboots the
# bootfile, so it must be executable on that arch — .efi for UEFI, .kpxe for BIOS.
# Both binaries re-run the bootstrap, which then reaches :tftpmenu → local menu.
dhcp-boot=tag:ipxe,tag:efi,netboot.xyz-snponly.efi,,192.168.10.127
dhcp-boot=tag:ipxe,tag:!efi,netboot.xyz.kpxe,,192.168.10.127