Files
homelab-infra/apps/netboot/WINDOWS.md
T
panxiao81 88a02ababa
lint / yaml (push) Has been cancelled
lint / ansible (push) Has been cancelled
lint / terraform (push) Has been cancelled
Establish clean homelab infrastructure baseline
Reorganize the brownfield repository, remove retired and generated artifacts, harden ignore rules, and record the GitOps/IaC redesign.
2026-09-09 16:47:20 +00:00

16 KiB
Raw Blame History

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:

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:

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.

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):
    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:

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. 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):

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):

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:

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):

for /r X:\Drivers %%i in (*.inf) do drvload "%%i"   REM filename-agnostic; non-matching INFs fail harmlessly
wpeinit
wpeutil InitializeNetwork
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:

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.