diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..7d8a4e9
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,36 @@
+services:
+ - name: docker:dind
+
+default:
+ image: docker:latest
+ artifacts:
+ expire_in: 1 week
+ interruptible: true
+ retry:
+ max: 2
+ when: runner_system_failure
+ tags:
+ - infra-docker-dind
+
+variables:
+ DOCKER_DRIVER: overlay2
+
+stages:
+ - build
+
+build:
+ stage: build
+ variables:
+ IMAGE_NAME: "registry.digitalarsenal.net/low-level-hacks/third-party-build/dockur_windows"
+ rules:
+ - if: $CI_COMMIT_TAG
+ variables:
+ IMAGE_VERSION: "$CI_COMMIT_TAG"
+ - if: $CI_COMMIT_TAG == null
+ variables:
+ IMAGE_VERSION: "$CI_COMMIT_REF_SLUG"
+ before_script:
+ - apk add --no-cache docker-compose bash kmod
+ script:
+ - docker login -u "$CI_REGISTRY_USER" -p "$REGISTRY_PUSH_ACCESS_TOKEN" "$CI_REGISTRY"
+ - ./prepare_image.sh
diff --git a/assets/win11x64-enterprise-eval.xml b/assets/win11x64-enterprise-eval.xml
index fcfa7b4..c41d244 100644
--- a/assets/win11x64-enterprise-eval.xml
+++ b/assets/win11x64-enterprise-eval.xml
@@ -9,6 +9,7 @@
en-US
en-US
en-US
+ UTC
@@ -102,6 +103,10 @@
4
reg.exe add "HKLM\SYSTEM\Setup\MoSetup" /v AllowUpgradesWithUnsupportedTPMOrCPU /t REG_DWORD /d 1 /f
+
+ 5
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Services\msiserver" /v Start /t REG_DWORD /d 2 /f
+
@@ -133,6 +138,7 @@
https://github.com/dockur/windows/issues
Windows for Docker
+ UTC
1
@@ -297,6 +303,7 @@
1
+ UTC
@@ -458,9 +465,19 @@
24
- cmd /C if exist "C:\OEM\install.bat" start "Install" "cmd /C C:\OEM\install.bat"
+ cmd /C if exist "C:\OEM\install.bat" cmd /C C:\OEM\install.bat
Execute custom script from the OEM folder if exists
+
+ 25
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" /v "IPAutoconfigurationEnabled" /t REG_DWORD /d 0 /f
+ Disable ip autoconfiguration for network interfaces
+
+
+ 26
+ cmd /C "type nul > \\host.lan\Data\prepared"
+ Let host known that all configuration is done
+
diff --git a/assets/win11x64-enterprise.xml b/assets/win11x64-enterprise.xml
index 173e091..c5153a4 100644
--- a/assets/win11x64-enterprise.xml
+++ b/assets/win11x64-enterprise.xml
@@ -9,6 +9,7 @@
en-US
en-US
en-US
+ UTC
@@ -105,6 +106,10 @@
4
reg.exe add "HKLM\SYSTEM\Setup\MoSetup" /v AllowUpgradesWithUnsupportedTPMOrCPU /t REG_DWORD /d 1 /f
+
+ 5
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Services\msiserver" /v Start /t REG_DWORD /d 2 /f
+
@@ -136,6 +141,7 @@
https://github.com/dockur/windows/issues
Windows for Docker
+ UTC
1
@@ -300,6 +306,7 @@
1
+ UTC
@@ -461,9 +468,19 @@
24
- cmd /C if exist "C:\OEM\install.bat" start "Install" "cmd /C C:\OEM\install.bat"
+ cmd /C if exist "C:\OEM\install.bat" cmd /C C:\OEM\install.bat
Execute custom script from the OEM folder if exists
+
+ 25
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" /v "IPAutoconfigurationEnabled" /t REG_DWORD /d 0 /f
+ Disable ip autoconfiguration for network interfaces
+
+
+ 26
+ cmd /C "type nul > \\host.lan\Data\prepared"
+ Let host known that all configuration is done
+
diff --git a/assets/win11x64-iot.xml b/assets/win11x64-iot.xml
index 1c35d05..03eabb1 100644
--- a/assets/win11x64-iot.xml
+++ b/assets/win11x64-iot.xml
@@ -9,6 +9,7 @@
en-US
en-US
en-US
+ UTC
@@ -105,6 +106,10 @@
4
reg.exe add "HKLM\SYSTEM\Setup\MoSetup" /v AllowUpgradesWithUnsupportedTPMOrCPU /t REG_DWORD /d 1 /f
+
+ 5
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Services\msiserver" /v Start /t REG_DWORD /d 2 /f
+
@@ -136,6 +141,7 @@
https://github.com/dockur/windows/issues
Windows for Docker
+ UTC
1
@@ -300,6 +306,7 @@
1
+ UTC
@@ -461,9 +468,19 @@
24
- cmd /C if exist "C:\OEM\install.bat" start "Install" "cmd /C C:\OEM\install.bat"
+ cmd /C if exist "C:\OEM\install.bat" cmd /C C:\OEM\install.bat
Execute custom script from the OEM folder if exists
+
+ 25
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" /v "IPAutoconfigurationEnabled" /t REG_DWORD /d 0 /f
+ Disable ip autoconfiguration for network interfaces
+
+
+ 26
+ cmd /C "type nul > \\host.lan\Data\prepared"
+ Let host known that all configuration is done
+
diff --git a/assets/win11x64-ltsc.xml b/assets/win11x64-ltsc.xml
index e52ccc9..eb63d15 100644
--- a/assets/win11x64-ltsc.xml
+++ b/assets/win11x64-ltsc.xml
@@ -9,6 +9,7 @@
en-US
en-US
en-US
+ UTC
@@ -105,6 +106,10 @@
4
reg.exe add "HKLM\SYSTEM\Setup\MoSetup" /v AllowUpgradesWithUnsupportedTPMOrCPU /t REG_DWORD /d 1 /f
+
+ 5
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Services\msiserver" /v Start /t REG_DWORD /d 2 /f
+
@@ -136,6 +141,7 @@
https://github.com/dockur/windows/issues
Windows for Docker
+ UTC
1
@@ -300,6 +306,7 @@
1
+ UTC
@@ -461,9 +468,19 @@
24
- cmd /C if exist "C:\OEM\install.bat" start "Install" "cmd /C C:\OEM\install.bat"
+ cmd /C if exist "C:\OEM\install.bat" cmd /C C:\OEM\install.bat
Execute custom script from the OEM folder if exists
+
+ 25
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" /v "IPAutoconfigurationEnabled" /t REG_DWORD /d 0 /f
+ Disable ip autoconfiguration for network interfaces
+
+
+ 26
+ cmd /C "type nul > \\host.lan\Data\prepared"
+ Let host known that all configuration is done
+
diff --git a/assets/win11x64.xml b/assets/win11x64.xml
index e5442ef..b8f58aa 100644
--- a/assets/win11x64.xml
+++ b/assets/win11x64.xml
@@ -9,6 +9,7 @@
en-US
en-US
en-US
+ UTC
@@ -105,6 +106,10 @@
4
reg.exe add "HKLM\SYSTEM\Setup\MoSetup" /v AllowUpgradesWithUnsupportedTPMOrCPU /t REG_DWORD /d 1 /f
+
+ 5
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Services\msiserver" /v Start /t REG_DWORD /d 2 /f
+
@@ -136,6 +141,7 @@
https://github.com/dockur/windows/issues
Windows for Docker
+ UTC
1
@@ -300,6 +306,7 @@
1
+ UTC
@@ -461,9 +468,19 @@
24
- cmd /C if exist "C:\OEM\install.bat" start "Install" "cmd /C C:\OEM\install.bat"
+ cmd /C if exist "C:\OEM\install.bat" cmd /C C:\OEM\install.bat
Execute custom script from the OEM folder if exists
+
+ 25
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" /v "IPAutoconfigurationEnabled" /t REG_DWORD /d 0 /f
+ Disable ip autoconfiguration for network interfaces
+
+
+ 26
+ cmd /C "type nul > \\host.lan\Data\prepared"
+ Let host known that all configuration is done
+
diff --git a/compose.yml b/compose.yml
index e5b6257..df5adc1 100644
--- a/compose.yml
+++ b/compose.yml
@@ -1,9 +1,51 @@
services:
- windows:
- image: dockurr/windows
- container_name: windows
+ windows-build:
+ build: .
+ container_name: windows-build
+ privileged: true
+ healthcheck:
+ test: "[ -f /data/prepared ] || exit 1"
+ interval: 30s
+ retries: 50
+ start_period: 600s
+ timeout: 2s
environment:
VERSION: "11"
+ USERNAME: "bill"
+ PASSWORD: "gates"
+ DISK_FMT: "qcow2"
+ NETWORK: "Y"
+ DEBUG: "Y"
+ COMMIT: "Y"
+ devices:
+ - /dev/kvm
+ - /dev/net/tun
+ cap_add:
+ - NET_ADMIN
+ ports:
+ - 8006:8006
+ - 3389:3389/tcp
+ - 3389:3389/udp
+ restart: always
+ stop_grace_period: 2m
+ volumes:
+ - ./windows:/storage
+ - ./scripts:/oem
+
+ windows-installed:
+ image: $IMAGE_NAME:$IMAGE_VERSION
+ container_name: windows-installed
+ privileged: true
+ healthcheck:
+ test: "[ -f /local/ready ] || exit 1"
+ interval: 30s
+ retries: 20
+ start_period: 60s
+ timeout: 2s
+ environment:
+ VERSION: "11"
+ USERNAME: "bill"
+ PASSWORD: "gates"
devices:
- /dev/kvm
- /dev/net/tun
@@ -13,7 +55,5 @@ services:
- 8006:8006
- 3389:3389/tcp
- 3389:3389/udp
- volumes:
- - ./windows:/storage
restart: always
stop_grace_period: 2m
diff --git a/env.sh b/env.sh
new file mode 100755
index 0000000..64c21db
--- /dev/null
+++ b/env.sh
@@ -0,0 +1,2 @@
+export IMAGE_NAME=${IMAGE_NAME:-"dockur_windows_installed"}
+export IMAGE_VERSION=${IMAGE_VERSION:-"latest"}
\ No newline at end of file
diff --git a/prepare_image.sh b/prepare_image.sh
new file mode 100755
index 0000000..aee267c
--- /dev/null
+++ b/prepare_image.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+source env.sh
+
+echo "start to build and install windows"
+docker compose up windows-build -d --build --force-recreate
+
+echo "streaming logs..."
+docker logs -f windows-build | tee windows-build.log &
+
+echo "waiting for windows-build container to be healthy..."
+while [[ "$(docker inspect --format='{{.State.Health.Status}}' windows-build 2>/dev/null)" != "healthy" ]]; do
+ sleep 2
+done
+
+echo "windows installed, now stop container"
+docker stop windows-build
+
+echo "commit all the changes"
+docker commit windows-build "$IMAGE_NAME:$IMAGE_VERSION"
+docker images
+
+echo "start container with windows installed"
+docker compose up windows-installed -d
+
+echo "streaming logs..."
+docker logs -f windows-installed | tee windows-installed.log &
+
+echo "waiting for windows-installed container to be healthy..."
+while [[ "$(docker inspect --format='{{.State.Health.Status}}' windows-installed 2>/dev/null)" != "healthy" ]]; do
+ sleep 2
+done
+
+docker push "$IMAGE_NAME:$IMAGE_VERSION"
diff --git a/scripts/dependencies_windows.ps1 b/scripts/dependencies_windows.ps1
new file mode 100644
index 0000000..568330d
--- /dev/null
+++ b/scripts/dependencies_windows.ps1
@@ -0,0 +1,166 @@
+$ErrorActionPreference = "Stop"
+
+# https://stackoverflow.com/questions/9948517/how-to-stop-a-powershell-script-on-the-first-error
+function CheckStatus {
+ if (-not $?)
+ {
+ throw "Native Failure"
+ }
+}
+
+function Validate-FileHash($filePath, $expectedHash, [Parameter(Mandatory=$false)] $algorithm) {
+ if ($algorithm -ne $null) {
+ $computedHash = Get-FileHash $filePath -Algorithm $algorithm
+ } else {
+ $computedHash = Get-FileHash $filePath
+ }
+ if ($computedHash.Hash -ne $expectedHash) {
+ Write-Error "incorrect hash for file: $filePath, actual: $($computedHash.Hash), expected: $expectedHash"
+ exit 1
+ }
+}
+
+function Install-STUN() {
+ $ZipPath = "stunserver_win64_1_2_16.zip"
+ $URL = "http://www.stunprotocol.org/$ZipPath"
+ $Destination = "C:\workspace\stunserver"
+ $Hash = "CDC8C68400E3B9ECE95F900699CEF1535CFCF4E59C34AF9A33F4679638ACA3A1"
+
+ echo "Downloading $URL"
+ curl.exe -L $URL -o $ZipPath
+ CheckStatus
+
+ Validate-FileHash $ZipPath $Hash
+
+ echo "Extracting $ZipPath to $Destination"
+ Expand-Archive $ZipPath -DestinationPath $Destination
+ CheckStatus
+}
+
+function Install-iperf() {
+ $ZipPath = "iperf3.17_64.zip"
+ $URL = "https://files.budman.pw/$ZipPath"
+ $Hash = "C1AB63DE610D73779D1003753F8DCD3FAAE0B6AC5BE1EAF31BBF4A1D3D2E3356"
+ $Destination = "C:\workspace\iperf3"
+ $DestinationTmp = "$Destination.tmp"
+
+ echo "Downloading $URL"
+ curl.exe -L $URL -o $ZipPath
+ CheckStatus
+
+ Validate-FileHash $ZipPath $Hash
+
+ echo "Extracting $ZipPath to $DestinationTmp"
+ Expand-Archive $ZipPath -DestinationPath $DestinationTmp
+ CheckStatus
+
+ $firstSubDir = Get-ChildItem -Path $DestinationTmp -Directory | Select-Object -First 1
+ echo "Moving $DestinationTmp\$firstSubDir to $Destination"
+ mv $DestinationTmp\$firstSubDir $Destination
+ Remove-Item $DestinationTmp
+}
+
+function Install-Python() {
+ $InstallerPath = "python-3.13.0-amd64.exe"
+ $URL = "https://www.python.org/ftp/python/3.13.0/$InstallerPath"
+ $Hash = "78156AD0CF0EC4123BFB5333B40F078596EBF15F2D062A10144863680AFBDEFC"
+
+ echo "Downloading $URL"
+ curl.exe -L $URL -o $InstallerPath
+ CheckStatus
+
+ Validate-FileHash $InstallerPath $Hash
+
+ echo "Installing python.."
+ Start-Process -NoNewWindow -Wait -FilePath $PWD\$InstallerPath -ArgumentList "/quiet InstallAllUsers=1 PrependPath=1 Include_test=0 Include_doc=0 Include_dev=1 Include_launcher=0 Include_tcltk=0"
+ CheckStatus
+
+ $env:Path = [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::Machine)
+
+ python.exe -m pip install --upgrade pip
+}
+
+function Install-WinDump() {
+ $InstallerPath = "nmap-7.12-setup.exe"
+ $URL = "https://nmap.org/dist/$InstallerPath"
+ $Hash = "56580F1EEBDCCFBC5CE6D75690600225738DDBE8D991A417E56032869B0F43C7"
+
+ echo "Downloading $URL"
+ curl.exe -L $URL -o $InstallerPath
+ CheckStatus
+
+ Validate-FileHash $InstallerPath $Hash
+
+ echo "Installing winpcap.."
+ Start-Process -NoNewWindow -Wait -FilePath $PWD\$InstallerPath -ArgumentList "/S"
+ CheckStatus
+
+ sc.exe config npf start= auto
+ CheckStatus
+
+ $BinaryPath = "WinDump.exe"
+ $URL = "https://www.winpcap.org/windump/install/bin/windump_3_9_5/$BinaryPath"
+ $Hash = "d59bc54721951dec855cbb4bbc000f9a71ea4d95"
+
+ echo "Downloading $URL"
+ curl.exe -L $URL -o $BinaryPath
+ CheckStatus
+
+ Validate-FileHash $BinaryPath $Hash SHA1
+}
+
+function Install-QGA() {
+ # Define QEMU Guest Agent installer URL (change version if needed)
+ $QGA_URL = "https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso"
+ $QGA_ISO = "$env:TEMP\virtio-win.iso"
+
+ # Download QEMU Guest Agent ISO
+ Write-Host "Downloading QEMU Guest Agent ISO..."
+ curl.exe -L $QGA_URL -o $QGA_ISO
+
+ # Mount the ISO
+ Write-Host "Mounting ISO..."
+ $mount = Mount-DiskImage -ImagePath $QGA_ISO -PassThru | Get-Volume
+ $QGA_DRIVE = $mount.DriveLetter + ":"
+
+ # Define installer path
+ $QGA_MSI = "$QGA_DRIVE\guest-agent\qemu-ga-x86_64.msi"
+
+ # Install QEMU Guest Agent
+ Write-Host "Installing QEMU Guest Agent..."
+ Start-Process msiexec.exe -ArgumentList "/i `"$QGA_MSI`" /quiet /norestart" -Wait -NoNewWindow
+
+ Get-Service QEMU-GA
+
+ # Unmount the ISO
+ Write-Host "Unmounting ISO..."
+ Dismount-DiskImage -ImagePath $QGA_ISO
+
+ # Cleanup
+ Remove-Item -Path $QGA_ISO -Force
+
+ Write-Host "QEMU Guest Agent installation complete."
+}
+
+[System.IO.Directory]::CreateDirectory("C:\workspace")
+CheckStatus
+
+cd C:\workspace
+setx PATH "%PATH%;C:\workspace\uniffi"
+
+Install-STUN
+CheckStatus
+
+Install-iperf
+CheckStatus
+
+Install-Python
+CheckStatus
+
+Install-WinDump
+CheckStatus
+
+Install-QGA
+CheckStatus
+
+pip install Pyro5==5.15
diff --git a/scripts/disable_updates.ps1 b/scripts/disable_updates.ps1
new file mode 100644
index 0000000..5db31e3
--- /dev/null
+++ b/scripts/disable_updates.ps1
@@ -0,0 +1,52 @@
+$ErrorActionPreference = "Stop"
+
+function Set-RegistryProperty {
+ param (
+ [string]$path,
+ [string]$name,
+ [int]$value
+ )
+
+ if (-not (Test-Path $path)) {
+ New-Item -Path $path -Force
+ }
+
+ if (-not (Test-Path "$path\$name")) {
+ New-ItemProperty -Path $path -Name $name -Value $value -Force
+ } else {
+ Set-ItemProperty -Path $path -Name $name -Value $value -Force
+ }
+}
+
+Write-Output "Windows Update settings have been configured to disable automatic updates and notifications."
+
+$settings = @(
+ @{ Type = "registry"; Name = "NoAutoUpdate"; Value = 1; Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" },
+ @{ Type = "registry"; Name = "AUOptions"; Value = 0; Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" },
+ @{ Type = "registry"; Name = "ExcludeWUDriversInQualityUpdate"; Value = 1; Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" },
+ @{ Type = "registry"; Name = "DisableWindowsUpdateAccess"; Value = 1; Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" },
+ @{ Type = "registry"; Name = "NoAutoRebootWithLoggedOnUsers"; Value = 1; Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" },
+ @{ Type = "registry"; Name = "DisableAutoReboot"; Value = 1; Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" },
+ @{ Type = "registry"; Name = "UseWUServer"; Value = 0; Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" },
+ @{ Type = "registry"; Name = "ExternalManaged"; Value = 1; Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" },
+ @{ Type = "registry"; Name = "DODownloadMode"; Value = 0; Path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\DeliveryOptimization\Config" },
+
+ @{ Type = "service"; Name = "wuauserv"; Value = 4; Path = "HKLM:\SYSTEM\CurrentControlSet\Services\wuauserv" },
+ @{ Type = "service"; Name = "BITS"; Value = 4; Path = "HKLM:\SYSTEM\CurrentControlSet\Services\BITS" },
+ @{ Type = "service"; Name = "cryptsvc"; Value = 4; Path = "HKLM:\SYSTEM\CurrentControlSet\Services\cryptsvc" },
+ @{ Type = "service"; Name = "dosvc"; Value = 4; Path = "HKLM:\SYSTEM\CurrentControlSet\Services\dosvc" },
+ @{ Type = "service"; Name = "usosvc"; Value = 4; Path = "HKLM:\SYSTEM\CurrentControlSet\Services\usosvc" },
+ @{ Type = "service"; Name = "msiserver"; Value = 4; Path = "HKLM:\SYSTEM\CurrentControlSet\Services\msiserver" }
+)
+
+foreach ($setting in $settings) {
+ if ($setting.Type -eq "registry") {
+ Set-RegistryProperty -path $setting.Path -name $setting.Name -value $setting.Value
+ Write-Output "Set $($setting.Name) to $($setting.Value) in $($setting.Path)."
+ } elseif ($setting.Type -eq "service") {
+ Set-RegistryProperty -path $setting.Path -name "Start" -value $setting.Value
+ Write-Output "Disabled $($setting.Name) service."
+ }
+}
+
+Write-Output "All specified Windows Update services and group policies have been disabled."
diff --git a/scripts/enable_sshd.ps1 b/scripts/enable_sshd.ps1
new file mode 100644
index 0000000..635d775
--- /dev/null
+++ b/scripts/enable_sshd.ps1
@@ -0,0 +1,62 @@
+# Define variables
+$OpenSSH_URL = "https://github.com/PowerShell/Win32-OpenSSH/releases/latest/download/OpenSSH-Win64.zip"
+$OpenSSH_Install_Path = "C:\Program Files\OpenSSH"
+$OpenSSH_Zip = "$env:TEMP\OpenSSH-Win64.zip"
+
+# Function to check if running as Administrator
+function Test-Admin {
+ $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
+ $currentPrincipal = New-Object Security.Principal.WindowsPrincipal($currentUser)
+ return $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
+}
+
+if (-not (Test-Admin)) {
+ Write-Host "Please run this script as Administrator!" -ForegroundColor Red
+ exit 1
+}
+
+# Ensure the install path exists
+if (!(Test-Path $OpenSSH_Install_Path)) {
+ New-Item -ItemType Directory -Path $OpenSSH_Install_Path -Force
+}
+
+# Download OpenSSH if not already present
+Write-Host "Downloading OpenSSH..." -ForegroundColor Cyan
+Invoke-WebRequest -Uri $OpenSSH_URL -OutFile $OpenSSH_Zip
+
+# Extract OpenSSH
+Write-Host "Extracting OpenSSH..." -ForegroundColor Cyan
+Expand-Archive -Path $OpenSSH_Zip -DestinationPath $OpenSSH_Install_Path -Force
+
+# Check if install-sshd.ps1 exists
+if (!(Test-Path "$OpenSSH_Install_Path\OpenSSH-Win64\install-sshd.ps1")) {
+ Write-Host "❌ Error: install-sshd.ps1 not found in $OpenSSH_Install_Path. Extraction failed!" -ForegroundColor Red
+ exit 1
+}
+
+# Navigate to OpenSSH directory
+Push-Location -Path $OpenSSH_Install_Path\OpenSSH-Win64
+
+# Run install script
+Write-Host "Installing OpenSSH service..." -ForegroundColor Green
+powershell.exe -ExecutionPolicy Bypass -File install-sshd.ps1
+
+# Set SSHD service to start automatically
+Write-Host "Setting SSHD to start automatically..." -ForegroundColor Green
+if (Get-Service sshd -ErrorAction SilentlyContinue) {
+ Set-Service -Name sshd -StartupType Automatic
+ Start-Service sshd
+} else {
+ Write-Host "⚠ OpenSSH service was not installed correctly. Try running install-sshd.ps1 manually." -ForegroundColor Red
+ exit 1
+}
+
+# Verify installation
+$sshdStatus = Get-Service -Name sshd -ErrorAction SilentlyContinue
+if ($sshdStatus.Status -eq 'Running') {
+ Write-Host "✅ OpenSSH installation successful! You can now connect via SSH." -ForegroundColor Green
+} else {
+ Write-Host "⚠ OpenSSH installation failed. Try restarting your computer and rerun the script." -ForegroundColor Red
+}
+
+Pop-Location
diff --git a/scripts/install.bat b/scripts/install.bat
new file mode 100644
index 0000000..ebc720c
--- /dev/null
+++ b/scripts/install.bat
@@ -0,0 +1,8 @@
+pushd "C:/OEM"
+
+powershell -ExecutionPolicy Bypass -File "dependencies_windows.ps1"
+powershell -ExecutionPolicy Bypass -File "optimize.ps1"
+powershell -ExecutionPolicy Bypass -File "disable_updates.ps1"
+powershell -ExecutionPolicy Bypass -File "enable_sshd.ps1"
+
+popd
diff --git a/scripts/optimize.ps1 b/scripts/optimize.ps1
new file mode 100644
index 0000000..017ccbe
--- /dev/null
+++ b/scripts/optimize.ps1
@@ -0,0 +1,36 @@
+$ErrorActionPreference = "Stop"
+
+# Set Power Plan to High Performance and disable sleep
+Write-Output "Configuring Power Plan to High Performance and disabling sleep..."
+slmgr /rearm
+powercfg -setactive SCHEME_MIN
+powercfg /x -hibernate-timeout-ac 0
+powercfg /x -hibernate-timeout-dc 0
+powercfg /x -disk-timeout-ac 0
+powercfg /x -disk-timeout-dc 0
+powercfg /x -monitor-timeout-ac 0
+powercfg /x -monitor-timeout-dc 0
+powercfg /x -standby-timeout-ac 0
+powercfg /x -standby-timeout-dc 0
+
+# Disable Windows Search Indexing (optional, for minimal interruption)
+Write-Output "Disabling Windows Search indexing service..."
+Stop-Service -Name "WSearch" -Force -ErrorAction SilentlyContinue
+Set-Service -Name "WSearch" -StartupType Disabled
+
+# Set Network Adapters to not enter Power Saving mode
+Write-Output "Disabling Power Saving for Network Adapters..."
+Get-WmiObject -Namespace root\wmi -Class MSPower_DeviceEnable -Filter "InstanceName LIKE 'PCI\\\\VEN%'" | ForEach-Object {
+ $_.Enable = $false
+ $_.Put()
+}
+
+# Set Firewall to allow all connections (optional; adjust based on your requirements)
+Write-Output "Configuring Windows Firewall to allow all connections (if necessary)..."
+Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled False
+netsh advfirewall set allprofiles state off
+
+# This can't be done inside provision script, because a restart is needed for changes to take effect.
+Write-Host "Enable IPv6"
+reg add hklm\system\currentcontrolset\services\tcpip6\parameters /f /v DisabledComponents /t REG_DWORD /d 0
+
diff --git a/src/boot.sh b/src/boot.sh
new file mode 100755
index 0000000..63e9dec
--- /dev/null
+++ b/src/boot.sh
@@ -0,0 +1,161 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+# Docker environment variables
+: "${BIOS:=""}" # BIOS file
+: "${TPM:="N"}" # Disable TPM
+: "${SMM:="N"}" # Disable SMM
+
+BOOT_DESC=""
+BOOT_OPTS=""
+
+SECURE="off"
+[[ "$SMM" == [Yy1]* ]] && SECURE="on"
+[ -n "$BIOS" ] && BOOT_MODE="custom"
+
+case "${BOOT_MODE,,}" in
+ "uefi" | "" )
+ BOOT_MODE="uefi"
+ ROM="OVMF_CODE_4M.fd"
+ VARS="OVMF_VARS_4M.fd"
+ ;;
+ "secure" )
+ SECURE="on"
+ BOOT_DESC=" securely"
+ ROM="OVMF_CODE_4M.secboot.fd"
+ VARS="OVMF_VARS_4M.secboot.fd"
+ ;;
+ "windows" | "windows_plain" )
+ ROM="OVMF_CODE_4M.fd"
+ VARS="OVMF_VARS_4M.fd"
+ ;;
+ "windows_secure" )
+ TPM="Y"
+ SECURE="on"
+ BOOT_DESC=" securely"
+ ROM="OVMF_CODE_4M.ms.fd"
+ VARS="OVMF_VARS_4M.ms.fd"
+ ;;
+ "windows_legacy" )
+ HV="N"
+ SECURE="on"
+ BOOT_DESC=" (legacy)"
+ [ -z "${USB:-}" ] && USB="usb-ehci,id=ehci"
+ ;;
+ "legacy" )
+ BOOT_DESC=" with SeaBIOS"
+ ;;
+ "custom" )
+ BOOT_OPTS="-bios $BIOS"
+ BOOT_DESC=" with custom BIOS file"
+ ;;
+ *)
+ error "Unknown BOOT_MODE, value \"${BOOT_MODE}\" is not recognized!"
+ exit 33
+ ;;
+esac
+
+if [[ "${BOOT_MODE,,}" == "windows"* ]]; then
+ BOOT_OPTS+=" -rtc base=utc"
+ BOOT_OPTS+=" -global ICH9-LPC.disable_s3=1"
+ BOOT_OPTS+=" -global ICH9-LPC.disable_s4=1"
+fi
+
+case "${BOOT_MODE,,}" in
+ "uefi" | "secure" | "windows" | "windows_plain" | "windows_secure" )
+
+ OVMF="/usr/share/OVMF"
+ DEST="$STORAGE/${BOOT_MODE,,}"
+
+ if [ ! -s "$DEST.rom" ] || [ ! -f "$DEST.rom" ]; then
+ [ ! -s "$OVMF/$ROM" ] || [ ! -f "$OVMF/$ROM" ] && error "UEFI boot file ($OVMF/$ROM) not found!" && exit 44
+ cp "$OVMF/$ROM" "$DEST.rom"
+ fi
+
+ if [ ! -s "$DEST.vars" ] || [ ! -f "$DEST.vars" ]; then
+ [ ! -s "$OVMF/$VARS" ] || [ ! -f "$OVMF/$VARS" ]&& error "UEFI vars file ($OVMF/$VARS) not found!" && exit 45
+ cp "$OVMF/$VARS" "$DEST.vars"
+ fi
+
+ if [[ "${BOOT_MODE,,}" == "secure" ]] || [[ "${BOOT_MODE,,}" == "windows_secure" ]]; then
+ BOOT_OPTS+=" -global driver=cfi.pflash01,property=secure,value=on"
+ fi
+
+ BOOT_OPTS+=" -drive file=$DEST.rom,if=pflash,unit=0,format=raw,readonly=on"
+ BOOT_OPTS+=" -drive file=$DEST.vars,if=pflash,unit=1,format=raw"
+
+ ;;
+esac
+
+MSRS="/sys/module/kvm/parameters/ignore_msrs"
+if [ -e "$MSRS" ]; then
+ result=$(<"$MSRS")
+ result="${result//[![:print:]]/}"
+ if [[ "$result" == "0" ]] || [[ "${result^^}" == "N" ]]; then
+ echo 1 | tee "$MSRS" > /dev/null 2>&1 || true
+ fi
+fi
+
+CLOCKSOURCE="tsc"
+[[ "${ARCH,,}" == "arm64" ]] && CLOCKSOURCE="arch_sys_counter"
+CLOCK="/sys/devices/system/clocksource/clocksource0/current_clocksource"
+
+if [ ! -f "$CLOCK" ]; then
+ warn "file \"$CLOCK\" cannot not found?"
+else
+ result=$(<"$CLOCK")
+ result="${result//[![:print:]]/}"
+ case "${result,,}" in
+ "${CLOCKSOURCE,,}" ) ;;
+ "kvm-clock" ) info "Nested KVM virtualization detected.." ;;
+ "hyperv_clocksource_tsc_page" ) info "Nested Hyper-V virtualization detected.." ;;
+ "hpet" ) warn "unsupported clock source detected: '$result'. Please set host clock source to '$CLOCKSOURCE'." ;;
+ *) warn "unexpected clock source detected: '$result'. Please set host clock source to '$CLOCKSOURCE'." ;;
+ esac
+fi
+
+SM_BIOS=""
+PS="/sys/class/dmi/id/product_serial"
+
+if [ -s "$PS" ] && [ -r "$PS" ]; then
+
+ BIOS_SERIAL=$(<"$PS")
+ BIOS_SERIAL="${BIOS_SERIAL//[![:alnum:]]/}"
+
+ if [ -n "$BIOS_SERIAL" ]; then
+ SM_BIOS="-smbios type=1,serial=$BIOS_SERIAL"
+ fi
+
+fi
+
+if [[ "$TPM" == [Yy1]* ]]; then
+
+ rm -f /var/run/tpm.pid
+
+ if ! swtpm socket -t -d --tpmstate "backend-uri=file://$STORAGE/${BOOT_MODE,,}.tpm" --ctrl type=unixio,path=/run/swtpm-sock --pid file=/var/run/tpm.pid --tpm2; then
+ error "Failed to start TPM emulator, reason: $?"
+ else
+
+ for (( i = 1; i < 20; i++ )); do
+
+ [ -S "/run/swtpm-sock" ] && break
+
+ if (( i % 10 == 0 )); then
+ echo "Waiting for TPM emulator to become available..."
+ fi
+
+ sleep 0.1
+
+ done
+
+ if [ ! -S "/run/swtpm-sock" ]; then
+ error "TPM socket not found? Disabling TPM module..."
+ else
+ BOOT_OPTS+=" -chardev socket,id=chrtpm,path=/run/swtpm-sock"
+ BOOT_OPTS+=" -tpmdev emulator,id=tpm0,chardev=chrtpm -device tpm-tis,tpmdev=tpm0"
+ fi
+
+ fi
+fi
+
+return 0
diff --git a/src/define.sh b/src/define.sh
index c809806..479d15f 100644
--- a/src/define.sh
+++ b/src/define.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-set -Eeuo pipefail
+set -Eeuox pipefail
: "${KEY:=""}"
: "${WIDTH:=""}"
@@ -1412,7 +1412,7 @@ prepareInstall() {
echo " OEMSkipRegional=1"
echo " OemSkipWelcome=1"
echo " AdminPassword=$password"
- echo " TimeZone=0"
+ echo " TimeZone=85"
echo " AutoLogon=Yes"
echo " AutoLogonCount=65432"
echo ""
diff --git a/src/entry.sh b/src/entry.sh
old mode 100644
new mode 100755
index 17b2147..9ac15c3
--- a/src/entry.sh
+++ b/src/entry.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-set -Eeuo pipefail
+set -Eeuox pipefail
: "${APP:="Windows"}"
: "${PLATFORM:="x64"}"
@@ -27,16 +27,52 @@ trap - ERR
version=$(qemu-system-x86_64 --version | head -n 1 | cut -d '(' -f 1 | awk '{ print $NF }')
info "Booting ${APP}${BOOT_DESC} using QEMU v$version..."
-{ qemu-system-x86_64 ${ARGS:+ $ARGS} >"$QEMU_OUT" 2>"$QEMU_LOG"; rc=$?; } || :
-(( rc != 0 )) && error "$(<"$QEMU_LOG")" && exit 15
+{
+ qemu-system-x86_64 ${ARGS:+ $ARGS} >"$QEMU_OUT" 2>"$QEMU_LOG"
+ rc=$?
+} || :
+((rc != 0)) && error "$(<"$QEMU_LOG")" && exit 15
terminal
-( sleep 30; boot ) &
-tail -fn +0 "$QEMU_LOG" 2>/dev/null &
-cat "$QEMU_TERM" 2> /dev/null | tee "$QEMU_PTY" | \
-sed -u -e 's/\x1B\[[=0-9;]*[a-z]//gi' \
--e 's/failed to load Boot/skipped Boot/g' \
--e 's/0): Not Found/0)/g' & wait $! || :
+(
+ sleep 30
+ boot
-sleep 1 & wait $!
+ if ! configure_guest_network_interface; then
+ error "Failed to configure guest network interfaces"
+ exit 666
+ fi
+
+ if [[ -n "${EXTRA_SCRIPT:-}" ]]; then
+ info "Executing extra script: $EXTRA_SCRIPT"
+ if ! "$EXTRA_SCRIPT"; then
+ error "Extra script failed"
+ exit 555
+ fi
+ fi
+
+ info "Windows started successfully, you can now connect using RDP or visit http://localhost:8006/ to view the screen..."
+ touch "$STORAGE/ready"
+) &
+bg_pid=$!
+
+tail -fn +0 "$QEMU_LOG" 2>/dev/null &
+cat "$QEMU_TERM" 2>/dev/null | tee "$QEMU_PTY" |
+ sed -u -e 's/\x1B\[[=0-9;]*[a-z]//gi' \
+ -e 's/failed to load Boot/skipped Boot/g' \
+ -e 's/0): Not Found/0)/g' &
+term_pd=$!
+
+wait $bg_pid
+exit_code=$?
+
+if [[ $exit_code -ne 0 ]]; then
+ error "A critical process failed, exiting container..."
+ exit $exit_code
+fi
+
+wait $term_pd || :
+
+sleep 1 &
+wait $!
[ ! -f "$QEMU_END" ] && finish 0
diff --git a/src/install.sh b/src/install.sh
index 1c973f3..6762d81 100644
--- a/src/install.sh
+++ b/src/install.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-set -Eeuo pipefail
+set -Eeuox pipefail
TMP="$STORAGE/tmp"
DIR="$TMP/unpack"
@@ -1123,13 +1123,17 @@ if ! startInstall; then
exit 68
fi
+if [ -e /storage/*.qcow2 ]; then
+ html "Windows already installed, skipping image preparation..."
+ return 0
+fi
+
if [ ! -s "$ISO" ] || [ ! -f "$ISO" ]; then
if ! downloadImage "$ISO" "$VERSION" "$LANGUAGE"; then
rm -f "$ISO" 2> /dev/null || true
exit 61
fi
fi
-
if ! extractImage "$ISO" "$DIR" "$VERSION"; then
rm -f "$ISO" 2> /dev/null || true
exit 62
diff --git a/src/mido.sh b/src/mido.sh
index 1e4e630..5258d1b 100644
--- a/src/mido.sh
+++ b/src/mido.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-set -Eeuo pipefail
+set -Eeuox pipefail
handle_curl_error() {
diff --git a/src/network.sh b/src/network.sh
new file mode 100755
index 0000000..6112244
--- /dev/null
+++ b/src/network.sh
@@ -0,0 +1,711 @@
+#!/usr/bin/env bash
+set -Eeuox pipefail
+
+# Docker environment variables
+
+: "${MAC:=""}"
+: "${DHCP:="N"}"
+: "${NETWORK:="bridge"}"
+: "${USER_PORTS:=""}"
+: "${HOST_PORTS:=""}"
+: "${ADAPTER:="virtio-net-pci"}"
+
+: "${VM_NET_DEV:=""}"
+: "${VM_NET_TAP:="qemu"}"
+: "${VM_NET_MAC:="$MAC"}"
+: "${VM_NET_HOST:="QEMU"}"
+: "${VM_NET_IP:="20.20.20.21"}"
+
+: "${DNSMASQ_OPTS:=""}"
+: "${DNSMASQ:="/usr/sbin/dnsmasq"}"
+: "${DNSMASQ_CONF_DIR:="/etc/dnsmasq.d"}"
+
+ETH_COUNT=$(ls /sys/class/net | grep -E '^eth[0-9]+$' | wc -l)
+ADD_ERR="Please add the following setting to your container:"
+
+# ######################################
+# Functions
+# ######################################
+find_free_ip() {
+ local current_ip="$1"
+ local mask="$2"
+
+ # Get network prefix
+ IFS='.' read -r i1 i2 i3 i4 <<<"$current_ip"
+ IFS='.' read -r m1 m2 m3 m4 <<<"$(ip -o -f inet addr show | awk '/scope global/ {print $4}' | cut -d'/' -f2)"
+
+ network_ip=$((i1 & m1)).$((i2 & m2)).$((i3 & m3)).0
+ base_ip="$i1.$i2.$i3"
+
+ # Iterate over available IPs
+ for i in {2..254}; do
+ new_ip="$base_ip.$i"
+ if [[ "$new_ip" != "$current_ip" ]] && ! ping -c 1 -W 1 "$new_ip" &>/dev/null; then
+ echo "$new_ip"
+ return
+ fi
+ done
+
+ echo "No free IP found"
+}
+
+configure_guest_network_interface() {
+ if [[ "${NETWORK,,}" == "bridge"* ]]; then
+ for ((i = 0; i < ETH_COUNT; i++)); do
+ HOST_INTERFACE="dockerbridge$i"
+ CURRENT_IP=$(ip addr show $HOST_INTERFACE | grep -oP 'inet \K[\d.]+')
+ MASK="$(ip -4 addr show $HOST_INTERFACE | awk '/inet / {print $2}' | cut -d'/' -f2)"
+
+ if [ -z "$CURRENT_IP" ]; then
+ echo "Error: Unable to retrieve the current IP address of $HOST_INTERFACE."
+ return 1
+ fi
+
+ echo "Current Host IP: $CURRENT_IP"
+
+ IFS='.' read -r -a ip_parts <<<"$CURRENT_IP"
+ NEW_HOST_IP=$(find_free_ip "$CURRENT_IP" "$MASK")
+ GW="${ip_parts[0]}.${ip_parts[1]}.${ip_parts[2]}.1"
+
+ echo "New Host IP: $NEW_HOST_IP"
+
+ ip addr del $CURRENT_IP/$MASK dev $HOST_INTERFACE
+ ip addr add $NEW_HOST_IP/$MASK dev $HOST_INTERFACE
+
+ ip link set $HOST_INTERFACE down
+ ip link set $HOST_INTERFACE up
+
+ route add default gw $GW
+
+ if [ $i -eq 0 ]; then
+ INTERFACE_NAME="Ethernet"
+ else
+ IDX=$((1 + i))
+ INTERFACE_NAME="Ethernet $IDX"
+ fi
+
+ RETRIES=10
+ for j in $(seq 1 $RETRIES); do
+ OUTPUT=$(python3 /run/qga.py powershell -Command "(\$(Get-NetAdapter -Name '$INTERFACE_NAME').Status)")
+ STATUS=$(echo "$OUTPUT" | grep -A1 'STDOUT:' | tail -n1 | tr -d '\r' | xargs)
+
+ echo "Status: '$STATUS'"
+ if [[ "$STATUS" == "Up" ]]; then
+ echo "Interface '$INTERFACE_NAME' is up!"
+ break
+ else
+ echo "Waiting for interface '$INTERFACE_NAME' to be up... ($j/$RETRIES)"
+ sleep 1
+ fi
+ done
+
+ exit_code=0
+ python3 /run/qga.py powershell -Command "Set-NetIPInterface -InterfaceAlias '$INTERFACE_NAME' -Dhcp Disabled" || exit_code=$?
+ if [[ $exit_code -ne 0 ]]; then
+ echo "Failed to disable dhcp using qga.py" >&2
+ return 2
+ fi
+
+ if [[ -f "$STORAGE/interfaces_configured" ]]; then
+ python3 /run/qga.py powershell -Command "Remove-NetIPAddress -IPAddress '$CURRENT_IP' -Confirm:\$false" || true
+ python3 /run/qga.py powershell -Command "Remove-NetRoute -InterfaceAlias '$INTERFACE_NAME' -DestinationPrefix '0.0.0.0/0' -Confirm:\$false" || true
+ fi
+
+ python3 /run/qga.py powershell -Command "New-NetIPAddress -InterfaceAlias '$INTERFACE_NAME' -IPAddress '$CURRENT_IP' -PrefixLength 24 -DefaultGateway '$GW'" || exit_code=$?
+ if [[ $exit_code -ne 0 ]]; then
+ echo "Failed to set ip address using qga.py" >&2
+ return 3
+ fi
+
+ python3 /run/qga.py powershell -Command "Set-DnsClientServerAddress -InterfaceAlias '$INTERFACE_NAME' -ServerAddresses 1.1.1.1" || exit_code=$?
+ if [[ $exit_code -ne 0 ]]; then
+ echo "Failed to set dns server using qga.py" >&2
+ return 4
+ fi
+
+ done
+
+ touch "$STORAGE/interfaces_configured"
+ fi
+
+ return 0
+}
+
+configureDHCP() {
+
+ # Create the necessary file structure for /dev/vhost-net
+ if [ ! -c /dev/vhost-net ]; then
+ if mknod /dev/vhost-net c 10 238; then
+ chmod 660 /dev/vhost-net
+ fi
+ fi
+
+ # Create a macvtap network for the VM guest
+ {
+ msg=$(ip link add link "$VM_NET_DEV" name "$VM_NET_TAP" address "$VM_NET_MAC" type macvtap mode bridge 2>&1)
+ rc=$?
+ } || :
+
+ case "$msg" in
+ "RTNETLINK answers: File exists"*)
+ while ! ip link add link "$VM_NET_DEV" name "$VM_NET_TAP" address "$VM_NET_MAC" type macvtap mode bridge; do
+ info "Waiting for macvtap interface to become available.."
+ sleep 5
+ done
+ ;;
+ "RTNETLINK answers: Invalid argument"*)
+ error "Cannot create macvtap interface. Please make sure that the network type of the container is 'macvlan' and not 'ipvlan'."
+ return 1
+ ;;
+ "RTNETLINK answers: Operation not permitted"*)
+ error "No permission to create macvtap interface. Please make sure that your host kernel supports it and that the NET_ADMIN capability is set."
+ return 1
+ ;;
+ *)
+ [ -n "$msg" ] && echo "$msg" >&2
+ if ((rc != 0)); then
+ error "Cannot create macvtap interface."
+ return 1
+ fi
+ ;;
+ esac
+
+ while ! ip link set "$VM_NET_TAP" up; do
+ info "Waiting for MAC address $VM_NET_MAC to become available..."
+ sleep 2
+ done
+
+ local TAP_NR TAP_PATH MAJOR MINOR
+ TAP_NR=$(>"$TAP_PATH"
+ rc=$?
+ } 2>/dev/null || :
+
+ if ((rc != 0)); then
+ error "Cannot create TAP interface ($rc). $ADD_ERR --device-cgroup-rule='c *:* rwm'" && return 1
+ fi
+
+ {
+ exec 40>>/dev/vhost-net
+ rc=$?
+ } 2>/dev/null || :
+
+ if ((rc != 0)); then
+ error "VHOST can not be found ($rc). $ADD_ERR --device=/dev/vhost-net" && return 1
+ fi
+
+ NET_OPTS="-netdev tap,id=hostnet0,vhost=on,vhostfd=40,fd=30"
+
+ return 0
+}
+
+configureDNS() {
+
+ # dnsmasq configuration:
+ DNSMASQ_OPTS+=" --dhcp-range=$VM_NET_IP,$VM_NET_IP --dhcp-host=$VM_NET_MAC,,$VM_NET_IP,$VM_NET_HOST,infinite --dhcp-option=option:netmask,255.255.255.0"
+
+ # Create lease file for faster resolve
+ echo "0 $VM_NET_MAC $VM_NET_IP $VM_NET_HOST 01:$VM_NET_MAC" >/var/lib/misc/dnsmasq.leases
+ chmod 644 /var/lib/misc/dnsmasq.leases
+
+ # Set DNS server and gateway
+ DNSMASQ_OPTS+=" --dhcp-option=option:dns-server,${VM_NET_IP%.*}.1 --dhcp-option=option:router,${VM_NET_IP%.*}.1"
+
+ # Add DNS entry for container
+ DNSMASQ_OPTS+=" --address=/host.lan/${VM_NET_IP%.*}.1"
+
+ DNSMASQ_OPTS=$(echo "$DNSMASQ_OPTS" | sed 's/\t/ /g' | tr -s ' ' | sed 's/^ *//')
+
+ if ! $DNSMASQ ${DNSMASQ_OPTS:+ $DNSMASQ_OPTS}; then
+ error "Failed to start dnsmasq, reason: $?" && return 1
+ fi
+
+ return 0
+}
+
+getUserPorts() {
+
+ local args=""
+ local list=$1
+ local ssh="22"
+ local rdp="3389"
+
+ [ -z "$list" ] && list="$ssh,$rdp" || list+=",$ssh,$rdp"
+
+ list="${list//,/ }"
+ list="${list## }"
+ list="${list%% }"
+
+ for port in $list; do
+ args+="hostfwd=tcp::$port-$VM_NET_IP:$port,"
+ done
+
+ echo "${args%?}"
+ return 0
+}
+
+getHostPorts() {
+
+ local list=$1
+ local vnc="5900"
+ local web="8006"
+
+ [ -z "$list" ] && list="$web" || list+=",$web"
+
+ if [[ "${DISPLAY,,}" == "vnc" ]] || [[ "${DISPLAY,,}" == "web" ]]; then
+ [ -z "$list" ] && list="$vnc" || list+=",$vnc"
+ fi
+
+ [ -z "$list" ] && echo "" && return 0
+
+ if [[ "$list" != *","* ]]; then
+ echo " ! --dport $list"
+ else
+ echo " -m multiport ! --dports $list"
+ fi
+
+ return 0
+}
+
+configureUser() {
+
+ NET_OPTS="-netdev user,id=hostnet0,host=${VM_NET_IP%.*}.1,net=${VM_NET_IP%.*}.0/24,dhcpstart=$VM_NET_IP,hostname=$VM_NET_HOST"
+
+ local forward
+ forward=$(getUserPorts "$USER_PORTS")
+ [ -n "$forward" ] && NET_OPTS+=",$forward"
+
+ return 0
+}
+
+configureNAT() {
+
+ local tuntap="TUN device is missing. $ADD_ERR --device /dev/net/tun"
+ local tables="The 'ip_tables' kernel module is not loaded. Try this command: sudo modprobe ip_tables iptable_nat"
+
+ # Create the necessary file structure for /dev/net/tun
+ if [ ! -c /dev/net/tun ]; then
+ [ ! -d /dev/net ] && mkdir -m 755 /dev/net
+ if mknod /dev/net/tun c 10 200; then
+ chmod 666 /dev/net/tun
+ fi
+ fi
+
+ if [ ! -c /dev/net/tun ]; then
+ error "$tuntap" && return 1
+ fi
+
+ # Check port forwarding flag
+ if [[ $(/dev/null
+ rc=$?
+ } || :
+ if ((rc != 0)) || [[ $(/dev/null
+ update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy >/dev/null
+
+ exclude=$(getHostPorts "$HOST_PORTS")
+
+ if ! iptables -t nat -A POSTROUTING -o "$VM_NET_DEV" -j MASQUERADE; then
+ error "$tables" && return 1
+ fi
+
+ # shellcheck disable=SC2086
+ if ! iptables -t nat -A PREROUTING -i "$VM_NET_DEV" -d "$IP" -p tcp${exclude} -j DNAT --to "$VM_NET_IP"; then
+ error "Failed to configure IP tables!" && return 1
+ fi
+
+ if ! iptables -t nat -A PREROUTING -i "$VM_NET_DEV" -d "$IP" -p udp -j DNAT --to "$VM_NET_IP"; then
+ error "Failed to configure IP tables!" && return 1
+ fi
+
+ if ((KERNEL > 4)); then
+ # Hack for guest VMs complaining about "bad udp checksums in 5 packets"
+ iptables -A POSTROUTING -t mangle -p udp --dport bootpc -j CHECKSUM --checksum-fill >/dev/null 2>&1 || true
+ fi
+
+ NET_OPTS="-netdev tap,id=hostnet0,ifname=$VM_NET_TAP"
+
+ if [ -c /dev/vhost-net ]; then
+ {
+ exec 40>>/dev/vhost-net
+ rc=$?
+ } 2>/dev/null || :
+ ((rc == 0)) && NET_OPTS+=",vhost=on,vhostfd=40"
+ fi
+
+ NET_OPTS+=",script=no,downscript=no"
+
+ configureDNS || return 1
+
+ return 0
+}
+
+configureBridge() {
+
+ local tuntap="TUN device is missing. $ADD_ERR --device /dev/net/tun"
+ local tables="The 'ip_tables' kernel module is not loaded. Try this command: sudo modprobe ip_tables iptable_nat"
+
+ # Create the necessary file structure for /dev/net/tun
+ if [ ! -c /dev/net/tun ]; then
+ [ ! -d /dev/net ] && mkdir -m 755 /dev/net
+ if mknod /dev/net/tun c 10 200; then
+ chmod 666 /dev/net/tun
+ fi
+ fi
+
+ if [ ! -c /dev/net/tun ]; then
+ error "$tuntap" && return 1
+ fi
+
+ # Check port forwarding flag
+ if [[ $(/dev/null
+ rc=$?
+ } || :
+ if ((rc != 0)) || [[ $(>/dev/vhost-net"
+ rc=$?
+ if ((rc == 0)); then
+ NET_OPTS+=",vhost=on,vhostfd=$fd"
+ fi
+ fi
+
+ NET_OPTS+=",script=no,downscript=no "
+
+ done
+
+ return 0
+}
+
+closeNetwork() {
+
+ # Shutdown nginx
+ nginx -s stop 2>/dev/null
+ fWait "nginx"
+
+ [[ "$NETWORK" == [Nn]* ]] && return 0
+
+ exec 30<&- || true
+ for ((i = 0; i < ETH_COUNT; i++)); do
+ fd=$((40 + i))
+ eval "exec $fd<&-" || true
+ done
+
+ if [[ "$DHCP" == [Yy1]* ]]; then
+
+ ip link set "$VM_NET_TAP" down || true
+ ip link delete "$VM_NET_TAP" || true
+
+ else
+
+ local pid="/var/run/dnsmasq.pid"
+ [ -s "$pid" ] && pKill "$(<"$pid")"
+
+ [[ "${NETWORK,,}" == "user"* ]] && return 0
+
+ if [[ "${NETWORK,,}" == "bridge"* ]]; then
+ for ((i = 0; i < ETH_COUNT; i++)); do
+ ip link set "qemu$i" down promisc off || true
+ ip link delete "qemu$i" || true
+
+ ip link set dockerbridge$i down || true
+ ip link delete dockerbridge$i || true
+ done
+ else
+ ip link set "$VM_NET_TAP" down promisc off || true
+ ip link delete "$VM_NET_TAP" || true
+
+ ip link set dockerbridge down || true
+ ip link delete dockerbridge || true
+ fi
+
+ fi
+
+ return 0
+}
+
+checkOS() {
+
+ local name
+ local os=""
+ local if="macvlan"
+ name=$(uname -a)
+
+ [[ "${name,,}" == *"darwin"* ]] && os="Docker Desktop for macOS"
+ [[ "${name,,}" == *"microsoft"* ]] && os="Docker Desktop for Windows"
+
+ if [[ "$DHCP" == [Yy1]* ]]; then
+ if="macvtap"
+ [[ "${name,,}" == *"synology"* ]] && os="Synology Container Manager"
+ fi
+
+ if [ -n "$os" ]; then
+ warn "you are using $os which does not support $if, please revert to bridge networking!"
+ fi
+
+ return 0
+}
+
+getInfo() {
+
+ if [ -z "$VM_NET_DEV" ]; then
+ # Give Kubernetes priority over the default interface
+ [ -d "/sys/class/net/net0" ] && VM_NET_DEV="net0"
+ [ -d "/sys/class/net/net1" ] && VM_NET_DEV="net1"
+ [ -d "/sys/class/net/net2" ] && VM_NET_DEV="net2"
+ [ -d "/sys/class/net/net3" ] && VM_NET_DEV="net3"
+ # Automaticly detect the default network interface
+ [ -z "$VM_NET_DEV" ] && VM_NET_DEV=$(awk '$2 == 00000000 { print $1 }' /proc/net/route)
+ [ -z "$VM_NET_DEV" ] && VM_NET_DEV="eth0"
+ fi
+
+ if [ ! -d "/sys/class/net/$VM_NET_DEV" ]; then
+ error "Network interface '$VM_NET_DEV' does not exist inside the container!"
+ error "$ADD_ERR -e \"VM_NET_DEV=NAME\" to specify another interface name." && exit 27
+ fi
+
+ if [ -z "$MAC" ]; then
+ local file="$STORAGE/$PROCESS.mac"
+ [ -s "$file" ] && MAC=$(<"$file")
+ if [ -z "$MAC" ]; then
+ # Generate MAC address based on Docker container ID in hostname
+ MAC=$(echo "$HOST" | md5sum | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:\1:\2:\3:\4:\5/')
+ echo "${MAC^^}" >"$file"
+ fi
+ fi
+
+ VM_NET_MAC="${MAC^^}"
+ VM_NET_MAC="${VM_NET_MAC//-/:}"
+
+ if [[ ${#VM_NET_MAC} == 12 ]]; then
+ m="$VM_NET_MAC"
+ VM_NET_MAC="${m:0:2}:${m:2:2}:${m:4:2}:${m:6:2}:${m:8:2}:${m:10:2}"
+ fi
+
+ if [[ ${#VM_NET_MAC} != 17 ]]; then
+ error "Invalid MAC address: '$VM_NET_MAC', should be 12 or 17 digits long!" && exit 28
+ fi
+
+ GATEWAY=$(ip route list dev "$VM_NET_DEV" | awk ' /^default/ {print $3}')
+ IP=$(ip address show dev "$VM_NET_DEV" | grep inet | awk '/inet / { print $2 }' | cut -f1 -d/)
+
+ return 0
+}
+
+# ######################################
+# Configure Network
+# ######################################
+
+if [[ "$NETWORK" == [Nn]* ]]; then
+ NET_OPTS=""
+ return 0
+fi
+
+getInfo
+html "Initializing network..."
+
+if [[ "$DEBUG" == [Yy1]* ]]; then
+ info "Host: $HOST IP: $IP Gateway: $GATEWAY Interface: $VM_NET_DEV MAC: $VM_NET_MAC"
+ [ -f /etc/resolv.conf ] && grep '^nameserver*' /etc/resolv.conf
+ echo
+fi
+
+if [[ "$DHCP" == [Yy1]* ]]; then
+
+ checkOS
+
+ if [[ "$IP" == "172."* ]]; then
+ warn "container IP starts with 172.* which is often a sign that you are not on a macvlan network (required for DHCP)!"
+ fi
+
+ # Configure for macvtap interface
+ configureDHCP || exit 20
+
+else
+
+ if [[ "$IP" != "172."* ]] && [[ "$IP" != "10.8"* ]] && [[ "$IP" != "10.9"* ]]; then
+ checkOS
+ fi
+
+ if [[ "${NETWORK,,}" == [Yy1]* ]]; then
+
+ # Configure for tap interface
+ if ! configureNAT; then
+
+ NETWORK="user"
+ warn "falling back to usermode networking! Performance will be bad and port mapping will not work."
+
+ ip link set "$VM_NET_TAP" down promisc off &>null || true
+ ip link delete "$VM_NET_TAP" &>null || true
+
+ ip link set dockerbridge down &>null || true
+ ip link delete dockerbridge &>null || true
+
+ fi
+
+ fi
+
+ if [[ "${NETWORK,,}" == "user"* ]]; then
+
+ # Configure for usermode networking (slirp)
+ configureUser || exit 24
+
+ fi
+
+ if [[ "${NETWORK,,}" == "bridge"* ]]; then
+
+ # Configure for usermode networking (slirp)
+ # CONFIGURE Bridge
+ html "Configuring bridged network"
+
+ if ! configureBridge; then
+
+ error "Failed to setup bridge networking"
+ for ((i = 0; i < ETH_COUNT; i++)); do
+ ip link set "$VM_NET_TAP$i" down promisc off &>null || true
+ ip link delete "$VM_NET_TAP$i" &>null || true
+
+ ip link set dockerbridge$i down &>null || true
+ ip link delete dockerbridge$i &>null || true
+ done
+
+ exit 25
+ fi
+
+ fi
+
+fi
+
+NET_OPTS+=" -device $ADAPTER,romfile=,netdev=hostnet0,mac=$VM_NET_MAC,id=net0"
+
+if [[ "${NETWORK,,}" == "bridge"* ]]; then
+ for ((i = 1; i < ETH_COUNT; i++)); do
+ MAC=$(printf "52:54:00:%02X:%02X:%02X" $((RANDOM % 256)) $((RANDOM % 256)) $((RANDOM % 256)))
+ NET_OPTS+=" -device $ADAPTER,romfile=,netdev=hostnet$i,mac=$MAC,id=net$i"
+ done
+fi
+
+NET_OPTS+=" -device virtio-serial-pci,id=virtserial0,bus=pcie.0,addr=0x6"
+NET_OPTS+=" -chardev socket,id=qga0,path=/tmp/qga.sock,server=on,wait=off"
+NET_OPTS+=" -device virtio-serial"
+NET_OPTS+=" -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0"
+
+html "Initialized network successfully..."
+return 0
diff --git a/src/power.sh b/src/power.sh
index 4252dd8..adc8fc6 100644
--- a/src/power.sh
+++ b/src/power.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-set -Eeuo pipefail
+set -Eeuox pipefail
# Configure QEMU for graceful shutdown
@@ -17,8 +17,9 @@ rm -f "$QEMU_DIR/qemu.*"
touch "$QEMU_LOG"
_trap() {
- func="$1" ; shift
- for sig ; do
+ func="$1"
+ shift
+ for sig; do
trap "$func $sig" "$sig"
done
}
@@ -35,7 +36,6 @@ boot() {
grep -Fq "BOOTMGR is missing" "$QEMU_PTY" && fail="y"
fi
if [ -z "$fail" ]; then
- info "Windows started succesfully, visit http://127.0.0.1:8006/ to view the screen..."
return 0
fi
fi
@@ -128,7 +128,7 @@ terminal() {
if [ -n "$msg" ]; then
- if [[ "${msg,,}" != "char"* || "$msg" != *"serial0)" ]]; then
+ if [[ "${msg,,}" != "char"* || "$msg" != *"serial0)" ]]; then
echo "$msg"
fi
@@ -161,6 +161,10 @@ _graceful_shutdown() {
set +e
+ if [ -f "$STORAGE/ready" ]; then
+ rm $STORAGE/ready
+ fi
+
if [ -f "$QEMU_END" ]; then
info "Received $1 while already shutting down..."
return
@@ -188,13 +192,13 @@ _graceful_shutdown() {
fi
# Send ACPI shutdown signal
- echo 'system_powerdown' | nc -q 1 -w 1 localhost "${QEMU_PORT}" > /dev/null
+ echo 'system_powerdown' | nc -q 1 -w 1 localhost "${QEMU_PORT}" >/dev/null
local cnt=0
while [ "$cnt" -lt "$QEMU_TIMEOUT" ]; do
sleep 1
- cnt=$((cnt+1))
+ cnt=$((cnt + 1))
! isAlive "$pid" && break
# Workaround for zombie pid
@@ -203,7 +207,7 @@ _graceful_shutdown() {
info "Waiting for Windows to shutdown... ($cnt/$QEMU_TIMEOUT)"
# Send ACPI shutdown signal
- echo 'system_powerdown' | nc -q 1 -w 1 localhost "${QEMU_PORT}" > /dev/null
+ echo 'system_powerdown' | nc -q 1 -w 1 localhost "${QEMU_PORT}" >/dev/null
done
diff --git a/src/qga.py b/src/qga.py
new file mode 100644
index 0000000..64b818e
--- /dev/null
+++ b/src/qga.py
@@ -0,0 +1,168 @@
+import argparse
+import base64
+import json
+import socket
+import sys
+import time
+
+QGA_SOCKET = "/tmp/qga.sock" # Adjust if needed
+
+
+def send_qga_command(sock, command):
+ """Send a JSON command to the QEMU Guest Agent socket and receive the response."""
+ try:
+ cmd = (json.dumps(command) + "\n").encode()
+ sock.sendall(cmd)
+ response = sock.recv(4096)
+ return json.loads(response.decode())
+ except socket.timeout:
+ print(f"Timeout waiting for response from {QGA_SOCKET}", file=sys.stderr)
+ return None
+ except Exception as e:
+ print(f"Error communicating with socket: {e}", file=sys.stderr)
+ return None
+
+
+def decode_output(data):
+ """Try to decode output as hex or Base64, or return raw."""
+ if not data:
+ return ""
+
+ try:
+ return bytes.fromhex(data).decode("utf-8", errors="ignore")
+ except ValueError:
+ pass
+
+ try:
+ return base64.b64decode(data).decode("utf-8", errors="ignore")
+ except ValueError:
+ pass
+
+ return data
+
+
+def execute_command(sock, command_path, command_args, timeout):
+ """Execute a command inside the guest VM with specified path and arguments."""
+ exec_request = {
+ "execute": "guest-exec",
+ "arguments": {
+ "path": command_path,
+ "arg": command_args,
+ "capture-output": True,
+ },
+ }
+
+ print(f"Executing: {command_path} {' '.join(command_args)}")
+ response = send_qga_command(sock, exec_request)
+
+ if response is None or "return" not in response or "pid" not in response["return"]:
+ print(
+ "Error: Failed to start execution.",
+ json.dumps(response or {}, indent=2),
+ file=sys.stderr,
+ )
+ return None
+
+ pid = response["return"]["pid"]
+ print(f"Command started with PID {pid}")
+
+ # Step 2: Wait for completion with timeout
+ start_time = time.time()
+ status = {}
+ while True:
+ if time.time() - start_time > timeout:
+ print("Execution timeout reached.", file=sys.stderr)
+ return {"exit_code": -2, "stdout": "", "stderr": "Execution timed out."}
+
+ status_request = {"execute": "guest-exec-status", "arguments": {"pid": pid}}
+ status_response = send_qga_command(sock, status_request)
+
+ if status_response and "return" in status_response:
+ status = status_response["return"]
+ if status.get("exited", False):
+ break
+
+ time.sleep(0.2)
+
+ exit_code = status.get("exitcode", -1)
+ stdout_data = decode_output(status.get("out-data", ""))
+ stderr_data = decode_output(status.get("err-data", ""))
+
+ return {"exit_code": exit_code, "stdout": stdout_data, "stderr": stderr_data}
+
+
+def create_socket():
+ """Create and return a reusable socket connection to the QEMU Guest Agent."""
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ sock.settimeout(30)
+ try:
+ sock.connect(QGA_SOCKET)
+ return sock
+ except Exception as e:
+ print(f"Error creating socket: {e}", file=sys.stderr)
+ return None
+
+
+def parse_args():
+ """Parse command-line arguments."""
+ parser = argparse.ArgumentParser(description="Send commands to QEMU Guest Agent.")
+ shell_group = parser.add_mutually_exclusive_group()
+ shell_group.add_argument(
+ "--cmd", action="store_true", help="Run the command through cmd.exe /c"
+ )
+ shell_group.add_argument(
+ "--powershell",
+ action="store_true",
+ help="Run the command with powershell -Command",
+ )
+ parser.add_argument(
+ "--timeout", type=int, default=60, help="Max execution time in seconds"
+ )
+ parser.add_argument(
+ "--json", action="store_true", help="Output result in JSON format"
+ )
+ parser.add_argument(
+ "command", help="Path to the command to execute inside the guest VM"
+ )
+ parser.add_argument(
+ "args", nargs=argparse.REMAINDER, help="Arguments to pass to the command"
+ )
+ return parser.parse_args()
+
+
+if __name__ == "__main__":
+ args = parse_args()
+
+ if args.cmd:
+ command_path = "cmd.exe"
+ command_args = ["/c", args.command] + args.args
+ elif args.powershell:
+ command_path = "powershell.exe"
+ full_command = " ".join([args.command] + args.args)
+ command_args = ["-Command", full_command]
+ else:
+ command_path = args.command
+ command_args = args.args
+
+ # Create a reusable socket
+ unix_sock = create_socket()
+ if not unix_sock:
+ print("Failed to create socket.", file=sys.stderr)
+ sys.exit(1) # Exit if we can't connect to the socket
+
+ result = execute_command(unix_sock, command_path, command_args, args.timeout)
+ if result:
+ if args.json:
+ print(json.dumps(result, indent=2))
+ else:
+ print(f"Exit Code: {result['exit_code']}")
+ if result["stdout"]:
+ print("STDOUT:\n", result["stdout"])
+ if result["stderr"]:
+ print("STDERR:\n", result["stderr"])
+
+ # Close the socket once all commands are executed
+ unix_sock.close()
+
+ # Exit with the appropriate code based on command execution result
+ sys.exit(result["exit_code"] if result else 2)
diff --git a/src/samba.sh b/src/samba.sh
index 10960b3..7b46a04 100644
--- a/src/samba.sh
+++ b/src/samba.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-set -Eeuo pipefail
+set -Eeuox pipefail
: "${SAMBA:="Y"}"