From d04cc507f0a2b79a9b474cb5f387a21d05316079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gytis=20Sto=C5=A1kevi=C4=8Dius?= Date: Fri, 4 Apr 2025 13:53:19 +0000 Subject: [PATCH] add extra_script option and qemu-guest-agen sock wrapper --- custom.xml | 5 ++ scripts/enable_sshd.ps1 | 62 ++++++++++++++++++ scripts/install.bat | 1 + src/entry.sh | 32 ++++++++-- src/network.sh | 34 ++++++++-- src/qga.py | 138 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 263 insertions(+), 9 deletions(-) create mode 100644 scripts/enable_sshd.ps1 create mode 100644 src/qga.py diff --git a/custom.xml b/custom.xml index d630612..de1a76b 100644 --- a/custom.xml +++ b/custom.xml @@ -465,6 +465,11 @@ 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/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 index cd0a3e2..ebc720c 100644 --- a/scripts/install.bat +++ b/scripts/install.bat @@ -3,5 +3,6 @@ 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/src/entry.sh b/src/entry.sh index 6574d91..4de2f59 100755 --- a/src/entry.sh +++ b/src/entry.sh @@ -36,16 +36,38 @@ terminal ( sleep 30 boot - configure_guest_network_interface - info "Windows started succesfully, you can now connect using RDP" - if [[ "${NETWORK,,}" != "bridge"* ]]; then - info "or visit http://localhost:8006/ to view the screen..." + + 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" & -wait $! || : +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 $! diff --git a/src/network.sh b/src/network.sh index 17d7f0e..60dbcf2 100755 --- a/src/network.sh +++ b/src/network.sh @@ -58,7 +58,7 @@ configure_guest_network_interface() { if [ -z "$CURRENT_IP" ]; then echo "Error: Unable to retrieve the current IP address of $HOST_INTERFACE." - exit 1 + return 1 fi echo "Current Host IP: $CURRENT_IP" @@ -84,10 +84,33 @@ configure_guest_network_interface() { INTERFACE_NAME="Ethernet $IDX" fi - echo -e '{"execute": "guest-exec", "arguments": {"path": "C:\\\\Windows\\\\System32\\\\netsh.exe", "capture-output": true, "arg": ["interface", "ipv4", "set", "address", "'"$INTERFACE_NAME"'", "static", "'$CURRENT_IP'", "255.255.255.0", "'$GW'"]}}' | nc -U /tmp/qga.sock -w 5 - echo -e '{"execute": "guest-exec", "arguments": {"path": "C:\\\\Windows\\\\System32\\\\netsh.exe", "capture-output": true, "arg": ["interface", "ipv4", "add", "dnsservers", "'"$INTERFACE_NAME"'", "1.1.1.1", "index=1"]}}' | nc -U /tmp/qga.sock -w 5 + 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 @@ -465,7 +488,10 @@ closeNetwork() { [[ "$NETWORK" == [Nn]* ]] && return 0 exec 30<&- || true - exec 40<&- || true + for ((i = 0; i < ETH_COUNT; i++)); do + fd=$((40 + i)) + eval "exec $fd<&-" || true + done if [[ "$DHCP" == [Yy1]* ]]; then diff --git a/src/qga.py b/src/qga.py new file mode 100644 index 0000000..c57f017 --- /dev/null +++ b/src/qga.py @@ -0,0 +1,138 @@ +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: + # Try Hex decoding first + return bytes.fromhex(data).decode("utf-8", errors="ignore") + except ValueError: + pass + + try: + # If hex fails, try Base64 decoding + return base64.b64decode(data).decode("utf-8", errors="ignore") + except ValueError: + pass + + # If all decoding fails, return raw + return data + + +def execute_command(sock, command_path, command_args): + """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, # Capture stdout and stderr + }, + } + response = send_qga_command(sock, exec_request) + + if response is None: + return None + + if "return" not in response or "pid" not in response["return"]: + print("Error: Failed to start execution:", response, file=sys.stderr) + return None + + pid = response["return"]["pid"] + print(f"Command started with PID {pid}") + + # Step 2: Wait for completion + while True: + status_request = {"execute": "guest-exec-status", "arguments": {"pid": pid}} + status_response = send_qga_command(sock, status_request) + + if status_response is None: + continue + + if "return" in status_response: + status = status_response["return"] + if status.get("exited", False): + break # Command finished + time.sleep(0.2) # Wait before checking again + + # Step 3: Get exit code and output + 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) # 30 seconds timeout + 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.") + 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__": + # Parse command-line arguments + args = parse_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 + + # Execute the command + result = execute_command(unix_sock, args.command, args.args) + if result: + 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)