1. Overview

NETSetup uses a custom Windows PE (WinPE) boot image to bootstrap the OS deployment process. The WinPE boots from an ISO or USB, launches NETSetup, which then drives the unattended Windows installation.

The WinPE boot.wim and related boot loader files live in src/NETSetup/WinPE/ (the git source of truth). The create-iso command copies these into NETSetupDEV/BOOT/ and packages them into NETSetup.iso.

2. Boot Chain

The full boot sequence from power-on to NETSetup execution:

UEFI Firmware
  |
  +-- Reads ISO El Torito boot image (efisys.bin)
  +-- Loads efi/boot/bootx64.efi (= bootmgr.efi, Microsoft-signed)
  |
  v
Windows Boot Manager (bootmgr.efi)
  |
  +-- Reads BCD from efi/microsoft/boot/bcd
  +-- BCD must be in WinPE mode (NOT Windows Setup mode)
  +-- Loads sources/boot.wim Index 1 into RAM (ramdisk -> X:)
  |
  v
WinPE Kernel (winload.efi -> ntoskrnl)
  |
  +-- Boot-start drivers load (storvsc, vmbus, vioscsi, viostor, stornvme, etc.)
  +-- winpeshl.exe starts (WinPE shell host)
  +-- No winpeshl.ini -> runs: cmd.exe /k startnet.cmd
  |
  v
startnet.cmd (inside boot.wim at X:\Windows\System32\startnet.cmd)
  |
  +-- wpeinit (initializes network, PnP, assigns drive letters)
  +-- Scans all drives A-Z for AutoExecute.ps1
  +-- Finds AutoExecute.ps1 on the ISO/USB partition (e.g. D:\)
  |
  v
AutoExecute.ps1 (on ISO/USB at BOOT\AutoExecute.ps1, NOT inside boot.wim)
  |
  +-- cd NETSetup
  +-- .\NETSetup.exe install
  +-- powershell (fallback interactive shell if NETSetup exits)
  |
  v
NETSetup.exe install
  |
  +-- Detects WinPE environment (host.IsOnWinPE())
  +-- Loads customer config (matches machine by serial number)
  +-- Backs up existing Windows (Users + ProgramData + drivers) to USB, if any
  +-- Clears disk (GPT), makes ESP (S:) + Windows (W:) partitions
  +-- Resolves image (prebuilt on boot medium, else download to W:)
  +-- DISM /Apply-Image install.wim -> W:\   (NOT setup.exe)
  +-- Injects Proxmox VirtIO boot drivers OFFLINE into W:\ (vioscsi + NetKVM), VM only
  +-- bcdboot from the applied image -> UEFI boot on S:
  +-- Copies unattend.xml to W:\Windows\Panther, copies NETSetup to W:\NETSetup
  +-- Reboots into Windows installation
Note

NETSetup no longer calls setup.exe /unattend. Win11 25H2 setup.exe fails in WinPE, so deployment is DISM /Apply-Image + bcdboot driven from MainWinPE.InstallOperatingSystem. The unattend.xml is stripped to specialize + oobeSystem only (no DiskConfiguration - NETSetup partitions the disk itself). See Stages/2-WinPE/MainWinPE.cs.

3. ISO File Layout

The create-iso command produces an ISO with this structure:

BOOT/                              <-- ISO root (oscdimg/xorriso builds from here)
+-- bootmgr                        <-- BIOS boot manager (from ISO, matches boot.wim version)
+-- bootmgr.efi                    <-- UEFI boot manager (from ISO, matches boot.wim version)
+-- boot/
|   +-- bcd                        <-- BIOS BCD (NETSetup-specific, NEVER from ISO)
|   +-- boot.sdi                   <-- System Deployment Image
|   +-- fonts/                     <-- Boot menu fonts
+-- efi/
|   +-- boot/bootx64.efi           <-- UEFI entry point (from ISO)
|   +-- microsoft/boot/
|       +-- bcd                    <-- UEFI BCD (NETSetup-specific, NEVER from ISO)
|       +-- efisys.bin             <-- El Torito UEFI boot image
|       +-- (NO cipolicies/)       <-- Must NOT exist
+-- sources/
|   +-- boot.wim                   <-- WinPE image (~545 MB, built from ADK)
+-- AutoExecute.ps1                <-- NETSetup launcher script
+-- NETSetup/
|   +-- NETSetup.exe               <-- Main binary (win-x64)
|   +-- NETSetup                   <-- Linux binary (linux-musl-x64)
|   +-- Config/                    <-- Customer configs (offline mode only; reusable + online = fetched by serial)
|   +-- Images/                    <-- install.wim (offline + reusable mode)
|   +-- Drivers/                   <-- VirtIO etc. (offline + reusable mode)
+-- linux/
    +-- grub/                      <-- GRUB bootloader for Linux PE
    +-- vmlinuz, initramfs         <-- Void Linux PE kernel

4. Source of Truth

Component Path

boot.wim (WinPE image)

src/NETSetup/WinPE/sources/boot.wim

Boot loader binaries

src/NETSetup/WinPE/bootmgr, bootmgr.efi

BCD configuration

src/NETSetup/WinPE/boot/bcd, efi/microsoft/boot/bcd

EFI boot loader

src/NETSetup/WinPE/efi/boot/bootx64.efi

AutoExecute.ps1

src/NETSetup/NETSetupDEV/SRC/AutoExecute.ps1

ISO build script (Windows)

src/NETSetup/NETSetupDEV/SRC/UPDATE.ps1

ISO build script (Linux)

src/NETSetup/NETSetupDEV/UPDATE.sh

Copies (not authoritative):

  • C:\NETSetupDEV\BOOT\ — local working copy, rebuilt by create-iso

  • %USERPROFILE%\osisa Dropbox\NETSetup\BOOT\ — synced for all dev machines

5. Building boot.wim

5.1. Rule: Always use ADK, never ISO boot.wim

Starting with Windows 11 24H2 (build 26100), the ISO’s boot.wim has self-integrity checks in winload.efi. When DISM adds packages or drivers, it breaks these checks, causing 0xc0000487 on \windows\system32\boot\winload.efi at boot. This was NOT an issue with 22H2 (build 22621).

The ADK’s winpe.wim is specifically designed for DISM customization and has no integrity enforcement.

ADK base image: C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment\amd64\en-us\winpe.wim

5.2. WinPE packages

21 packages added via DISM /Add-Package:

WinPE-WMI, WinPE-NetFx, WinPE-Scripting, WinPE-PowerShell, WinPE-DismCmdlets, WinPE-SecureStartup, WinPE-SecureBootCmdlets, WinPE-EnhancedStorage, WinPE-StorageWMI, WinPE-Setup, WinPE-Setup-Client, WinPE-Setup-Server, WinPE-WDS-Tools, WinPE-HTA, WinPE-MDAC, WinPE-FMAPI, WinPE-PlatformId, WinPE-Dot3Svc, WinPE-PPPoE, WinPE-RNDIS, WinPE-WinReCfg

5.3. startnet.cmd

@echo off
wpeinit
for %%a in ( a b c d e f g h i j k l m n o p q r s t u v w x y z) do (
    if exist "%%a:\AutoExecute.ps1" cd /d "%%a:" && goto :exec
)
:exec
powershell -ep bypass .\AutoExecute.ps1

5.4. Drivers

Drivers must be injected via dism /Add-Driver (not just present as .sys files). Without proper registration in the WinPE SYSTEM registry hive, drivers silently fail to load.

5.4.1. VirtIO (Proxmox/QEMU) — not inbox, must inject

Driver Source

vioscsi (VirtIO SCSI)

C:\NETSetupDEV\Drivers\Proxmox\vioscsi\w11\amd64

viostor (VirtIO Block)

C:\NETSetupDEV\Drivers\Proxmox\viostor\w11\amd64

NetKVM (VirtIO Network)

C:\NETSetupDEV\Drivers\Proxmox\NetKVM\w11\amd64

Balloon (VirtIO Memory)

C:\NETSetupDEV\Drivers\Proxmox\Balloon\w11\amd64

Use /ForceUnsigned for VirtIO drivers.

5.4.2. Hyper-V — files exist in ADK but NOT registered, must explicitly add

Driver Source

wstorvsc (Hyper-V SCSI)

Host DriverStore: wstorvsc*

wnetvsc (Hyper-V Network)

Host DriverStore: wnetvsc*

wvmbus (Hyper-V VMBus)

Host DriverStore: wvmbus*

Source: C:\Windows\System32\DriverStore\FileRepository{pattern}

5.4.3. Already inbox (no action needed)

  • stornvme (NVMe)

  • storahci (SATA/AHCI)

6. Updating Boot Loader Files

When the boot.wim version changes (e.g. 22621 → 26100), the boot loader binaries must also be updated.

6.1. What to update from the Windows 11 ISO

  • bootmgr — BIOS boot manager binary

  • bootmgr.efi — UEFI boot manager binary

  • efi/boot/bootx64.efi — UEFI entry point

  • boot/fonts/ — boot menu fonts

  • boot/memtest.exe, boot/boot.sdi — utilities

6.2. What to NEVER replace

Important

BCD files (boot/bcd and efi/microsoft/boot/bcd) must NEVER be replaced with ISO versions.

The ISO’s BCD is configured for "Windows Setup" mode, which auto-launches setup.exe and bypasses the NETSetup boot chain (startnet.cmdAutoExecute.ps1NETSetup.exe). The NETSetup BCD files are configured for WinPE command-line mode.

If accidentally replaced, restore from git: git checkout <pre-update-commit> — src/NETSetup/WinPE/boot/bcd src/NETSetup/WinPE/efi/microsoft/boot/bcd

Important

CI policies (efi/microsoft/boot/cipolicies/) must be removed after copying from the ISO. Windows 24H2 CI policies block DISM-customized WinPE images from booting (0xc0000487).

7. Driver Mechanism (Runtime, Not boot.wim)

The section above is about drivers baked into boot.wim so WinPE itself can see disks. This section is the SEPARATE mechanism: drivers NETSetup installs into the TARGET OS during deployment. Root helper is Helpers/DriverHelper2.cs.

7.1. Where drivers live

Tree What

src/NETSetup/Drivers/<manufacturer>/<model>

Per-machine driver packs (laptops, desktops)

src/NETSetup/Drivers/<manufacturer>

Manufacturer-only fallback (used when model has no folder)

src/NETSetup/Drivers/Proxmox/<category>/<os>/amd64

OS-aware VirtIO tree (vioscsi, NetKVM, viostor, Balloon, …​)

Resolution: DriverHelper2.TryInstallDriversWindows calls ResolveDriverPath(driversFolder, host.NormalizedManufacturer(), host.Model.Value) - prefers manufacturer/model, falls back to manufacturer, else warns and skips. The OS folder for the Proxmox tree is mapped by ResolveVirtIOFolderName (Windows 11 → w11, Server 2019 → 2k19).

7.2. Two install paths - ONLINE vs OFFLINE

Path Method When

ONLINE (against running OS)

InstallInfDrivers / InstallProxmoxDrivers (pnputil)

Stage3, OS is booted. Non-boot drivers (NIC, balloon, GPU, per-model packs).

OFFLINE (into applied image before first boot)

DISM /Add-Driver via InjectProxmoxDriversOffline

WinPE, image applied but NOT yet booted. Boot-critical storage driver (vioscsi).

7.3. How the Proxmox tree ships

The VirtIO drivers are NOT in git as loose files. They ship as Drivers/Proxmox.7z (Dropbox remote source). S2RemoteSource.CopyDrivers extracts them to BOOT/NETSetup/Drivers/Proxmox when the ISO build’s includeDrivers list contains "Proxmox". So the tree only exists on the boot medium for Proxmox-targeted ISOs; bare-metal / Hyper-V ISOs do not bundle it, and the offline injection below no-ops there.

8. Offline Boot-Driver Injection (vioscsi 0x7B Fix)

8.1. The bug it kills

Proxmox VMs use a virtio-scsi system disk. Windows has NO inbox vioscsi driver, so a freshly applied image cannot reach its own system disk on first boot and bugchecks INACCESSIBLE_BOOT_DEVICE (0x7B) in a reboot loop. Injecting vioscsi into the boot.wim (build section above) fixes WinPE seeing the disk, but does NOTHING for the installed OS - the applied image needs its own copy.

8.2. The fix

DriverHelper2.InjectProxmoxDriversOffline stages vioscsi (boot-critical) + NetKVM (so the node has network on first boot) into the applied W:\ image with offline DISM:

dism /Image:"W:\\" /Add-Driver /Driver:"<Drivers\Proxmox\vioscsi\w11\amd64>" /Recurse /ForceUnsigned

Wiring: MainWinPE.InstallOperatingSystem step 5b, Windows branch only, BETWEEN DISM /Apply-Image and bcdboot. Gated on host.IsVirtualMachine() AND the Drivers/Proxmox tree being present on the boot medium. If vioscsi cannot be staged the install HARD-FAILS (ExitFailure) - shipping a node that 0x7B-loops is worse than aborting. Non-boot drivers only warn on failure.

8.3. GOTCHA: trailing backslash breaks the DISM command line

dism /Image:"W:\" looks right but the closing quote after \ escapes to \", mangling the whole command line → DISM error 1639 "missing a required servicing command". Fix: DOUBLE the trailing backslash so the splitter parses it back to W:\:

/Image:"W:\\"      <-- correct
/Image:"W:\"       <-- WRONG, DISM error 1639

Encapsulated in DismCommand.QuoteWinPath (Stages/2-WinPE/DismCommand.cs) so callers never hand-format the path. Do not re-introduce raw quoting.

9. Standalone Windows Node Reachability

Standalone (workgroup) Windows nodes have NO domain controller and NO router reservation serving them. Stage3 client branch runs an extra block ONLY when !joinsDomain (netSetupConfig.DirectoryService.Get<IActiveDirectoryServer>().Any() is false). Domain clients deliberately stay on DHCP and skip all three steps below. Code: Stages/3-NETSetupWindows/Stage3Windows.cs + WindowsServerMethods.cs.

9.1. The three steps that make c$ reachable

Step Why

Static IPv4 (SetStandaloneClientIPv4)

Sets IP from computer.IP, gateway from config.GatewayIP (fallback IRouter), primary DNS from the Nix Dns VM (else the gateway). NO domain DNS pinning - a workgroup node has no AD to register into. Domain nodes get DHCP; a fixed-IP standalone node needs this pin.

LocalAccountTokenFilterPolicy=1

Registry: HKLM...\Policies\System. Lets a LOCAL admin keep its full token over the network. Without it, UAC remote token filtering strips admin rights and blocks c$ / admin$.

Enable inbound File and Printer Sharing (445 + admin shares)

Enable-NetFirewallRule -Group '@FirewallAPI.dll,-28502'.

9.2. GOTCHA: firewall group must be the language-independent ID

Use the group ID @FirewallAPI.dll,-28502, NOT -DisplayGroup 'File and Printer Sharing'. The display name is LOCALIZED (German nodes show "Datei- und Druckerfreigabe"), so -DisplayGroup matches ZERO rules on a non-English node and 445 stays blocked. The numeric group ID is locale-independent. Verified live: \\<ip>\c$ reachable by design (proven on nsnode02 @ 192.168.3.69).

10. Reusable Driver-Fixed ISO (Online Config) - IMPLEMENTED

Old way: each node ISO baked THAT node config PLUS a MapFile keyed by serial. Every node need own ISO build. Now shipped: build ONE driver-fixed NETSetup.iso, reuse for every node, config move OFF the medium and get fetched ONLINE by serial. The offline vioscsi injection above already make one ISO boot on any Proxmox VM - that is the prerequisite that make this viable. It does NOT change the driver mechanism or the offline vioscsi fix.

Code: Stages/0-Iso/MainISO.cs (CreateReusableProductionISO + shared BuildIsoWithBakedImages), CLI/Commands/Build/CreateIsoCommand.cs (--reusable), CLI/Commands/Build/PublishConfigCommand.cs (publish-config), Helpers/ProxmoxNodeIdentity.cs, Entities/NETSetupConfig.cs (UseReusableIso), Stages/5-NETSetupLinux/ProvisionWindowsVmOnProxmox.cs (the switch), Config/ConfigMethods.cs (FetchConfigOnline).

10.1. Three steps of the flow

10.1.1. 1. Build once - the shared ISO

netsetup create-iso --reusable --images <os-list> --drivers Proxmox

Produces a SINGLE reusable NETSetup.iso (no ticket suffix) with images + drivers baked in but NO customer config and NO MapFile. --reusable is mutually exclusive with --offline and needs NEITHER --ticket NOR --hostname. Internally CreateReusableProductionISO shares BuildIsoWithBakedImages with the test ISO; the only difference is it strips the stale Config/#VirtualMachines.json so a VM never wrongly falls back to a test config when the online map lookup should drive it. On a Proxmox host the finished ISO is copied to iso storage as NETSetup.iso.

10.1.2. 2. Register each node - publish-config (no ISO build)

netsetup publish-config --ticket N [--config <path-to-#N.json>]

Uploads Config/#N.json PLUS one Config/<serial>#N.map per machine to the Dropbox remote source. NO ISO is built. --config defaults to Config/#N.json under the cwd (where the customer-config workflow stages the generated JSON). This is the ONLINE counterpart to the offline baked config - run it once per ticket to register every node of that ticket. Under the hood it stages a temp Config/ (canonical JSON + GenerateMapFiles(stagingOp, "#N")) then EnsureNoDiff into the remote source Config/.

10.1.3. 3. Boot the shared ISO - guest matches config online by serial

The guest computes its own serial, then FetchConfigOnline (in ConfigMethods.cs) looks for a single matching Config/<serial>#N.map on the Dropbox remote source and downloads that map plus its #N.json. The map file name carries the serial (hosted machines prefix the vmid as <vmid>@<serial>#N.map); the ticket is parsed back out of the #N suffix to name the config. The offline baked config on the medium remains the fallback when the node is offline (or when a local map file is present on a USB stick).

10.2. Proxmox VMs - deterministic serial so the guest matches its .map

On virtio-scsi WinPE the SMBIOS serial is empty, so the guest falls back to a serial computed from its NIC MAC. ProxmoxNodeIdentity derives, FROM the vmid, both the deterministic NIC MAC (Proxmox OUI BC2411 + vmid as the lower 3 octets) and the MAC-concat serial via the SAME netbase routine the guest uses (WindowsComputerHardware.BuildSerialNumberFromMacs). So the serial the customer translator writes into the config equals the serial the guest computes at boot.

When NETSetupConfig.UseReusableIso = true, ProvisionWindowsVmOnProxmox:

  • points EVERY VM at the shared NETSetup.iso already on iso storage - NO per-VM offline ISO build (which is also why reusable mode works on Liberator/Proxmox runtime hosts that lack the source tree + dotnet SDK), and

  • pins vm.MacAddress = ProxmoxNodeIdentity.DeriveMacAddress(vmid) and vm.SerialNumber = ProxmoxNodeIdentity.DeriveSerialNumber(vmid) so the guest-computed serial equals its Config/<serial>#N.map key.

REQUIRED translator step: for each reusable-mode machine the customer translator MUST set SerialNumber = ProxmoxNodeIdentity.DeriveSerialNumber(vmid) (the vmid is the machine hosted-serial prefix). Skip it and the guest computes a serial that matches no published map, and install falls back to "assuming test installation".

UseReusableIso defaults to FALSE - the per-VM offline ISO (baked config) stays the DEFAULT.

10.3. Security - scope the baked Dropbox token

The reusable ISO ships WITHOUT a baked customer config, but it DOES carry the already-baked Dropbox token so the guest can pull its config at runtime. Because one ISO now serves every node, a leaked reusable ISO leaks that token. Use a per-customer, READ-ONLY, SCOPED Dropbox token pointed only at that customer’s Config/ subtree - NOT the full read-write image/driver token used at build time. The offline per-VM ISO (no runtime token needed) stays the default for anything sensitive.

11. Troubleshooting

Symptom Cause Fix

BSOD: winload.efi 0xc0000487

boot.wim built from ISO boot.wim (24H2 integrity checks)

Rebuild from ADK winpe.wim

BSOD: instant blue screen, VM shuts down

bootmgr.efi version mismatch with boot.wim

Update bootmgr from matching ISO

"Windows Server Setup" instead of NETSetup

BCD replaced with ISO version (Windows Setup mode)

Restore NETSetup BCD from git

"Install driver to display hardware"

Storage drivers not registered in WinPE

Inject via dism /Add-Driver (not just file copy)

BSOD: INACCESSIBLE_BOOT_DEVICE (0x7B) reboot loop after first reboot

vioscsi not staged into the APPLIED image (virtio-scsi disk, no inbox driver)

Offline inject vioscsi into W:\ (InjectProxmoxDriversOffline); ensure VM + Proxmox tree present

DISM error 1639 "missing a required servicing command"

Quoted /Image:"W:\" - trailing \ escaped the closing quote

Double the trailing backslash /Image:"W:\\" (DismCommand.QuoteWinPath)

Standalone node: \\ip\c$ refused / not reachable

-DisplayGroup 'File and Printer Sharing' matched nothing on a localized (German) node

Use firewall group ID @FirewallAPI.dll,-28502; also set LocalAccountTokenFilterPolicy=1

DISM Error 50 "not supported"

Stale DISM mount from previous failure

Run dism /Cleanup-Wim, reboot if needed

setup.exe fails at ~5% with 0xC1900108

WinPE version older than install.wim

Update WinPE to match install.wim version

12. Version History

Date Version Change

2026-04-01

10.0.26100

Rebuilt from ADK (was ISO-based 22621). Added VirtIO + Hyper-V driver injection. Updated boot loader binaries. Fixed BCD (must not use ISO version).

2026-07-07

-

Documented runtime driver mechanism, offline vioscsi injection (0x7B fix), standalone node reachability, reusable-ISO direction. Deploy path is DISM /Apply-Image + bcdboot (not setup.exe).

2026-07-07

-

Reusable driver-fixed ISO IMPLEMENTED (commit f64e1a96): create-iso --reusable builds one config-less NETSetup.iso, publish-config --ticket N registers node config + MapFiles online, guest matches config by serial via FetchConfigOnline. Proxmox VMs pin deterministic MAC + serial from vmid (ProxmoxNodeIdentity) when UseReusableIso=true. Use a scoped read-only Dropbox token.