- 1. Overview
- 2. Boot Chain
- 3. ISO File Layout
- 4. Source of Truth
- 5. Building boot.wim
- 6. Updating Boot Loader Files
- 7. Driver Mechanism (Runtime, Not boot.wim)
- 8. Offline Boot-Driver Injection (vioscsi 0x7B Fix)
- 9. Standalone Windows Node Reachability
- 10. Reusable Driver-Fixed ISO (Online Config) - IMPLEMENTED
- 11. Troubleshooting
- 12. Version History
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 |
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) |
|
Boot loader binaries |
|
BCD configuration |
|
EFI boot loader |
|
AutoExecute.ps1 |
|
ISO build script (Windows) |
|
ISO build script (Linux) |
|
Copies (not authoritative):
-
C:\NETSetupDEV\BOOT\— local working copy, rebuilt bycreate-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) |
|
viostor (VirtIO Block) |
|
NetKVM (VirtIO Network) |
|
Balloon (VirtIO Memory) |
|
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: |
wnetvsc (Hyper-V Network) |
Host DriverStore: |
wvmbus (Hyper-V VMBus) |
Host DriverStore: |
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 ( The ISO’s BCD is configured for "Windows Setup" mode, which auto-launches If accidentally replaced, restore from git:
|
|
Important
|
CI policies ( |
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 |
|---|---|
|
Per-machine driver packs (laptops, desktops) |
|
Manufacturer-only fallback (used when model has no folder) |
|
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) |
|
Stage3, OS is booted. Non-boot drivers (NIC, balloon, GPU, per-model packs). |
OFFLINE (into applied image before first boot) |
DISM |
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 ( |
Sets IP from |
|
Registry: |
Enable inbound File and Printer Sharing (445 + admin shares) |
|
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.isoalready 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)andvm.SerialNumber = ProxmoxNodeIdentity.DeriveSerialNumber(vmid)so the guest-computed serial equals itsConfig/<serial>#N.mapkey.
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: |
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 |
BSOD: |
vioscsi not staged into the APPLIED image (virtio-scsi disk, no inbox driver) |
Offline inject vioscsi into W:\ ( |
DISM error 1639 "missing a required servicing command" |
Quoted |
Double the trailing backslash |
Standalone node: |
|
Use firewall group ID |
DISM Error 50 "not supported" |
Stale DISM mount from previous failure |
Run |
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): |