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"}"