fix: repair Windows WebVNC credentials

This commit is contained in:
Vincent Koc 2026-05-05 00:37:07 -07:00 committed by GitHub
parent a0af15bd47
commit 4e5ce36538
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 89 additions and 12 deletions

View File

@ -10,6 +10,7 @@
### Fixed
- Fixed auto-shell command reconstruction so arguments with spaces stay quoted when shell operators such as `&&` are present.
- Fixed Windows WebVNC credential handling so generated portal links preserve special characters and managed TightVNC sessions copy service passwords into the logged-in user's registry profile.
## 0.5.0 - 2026-05-04

View File

@ -32,9 +32,11 @@ The runner VNC service stays bound to loopback.
`--open` opens the portal page after the bridge starts. If the VNC password is
available, the command also places it in the URL fragment for the local browser
tab. URL fragments are not sent to the coordinator. If the portal login flow
redirects first, the page may still prompt for the VNC password; use the
password printed by the command.
tab. URL fragments are not sent to the coordinator, and Crabbox preserves
special characters such as `!` when building the fragment. If the portal login
flow redirects first, the page may still prompt for the VNC password; use the
password printed by the command. If an old browser tab is retrying with a stale
fragment, close it before opening the new bridge URL.
Flags:

View File

@ -28,6 +28,8 @@ Bootstrap flow:
- Crabbox installs Git for Windows and TightVNC.
- Crabbox creates a local `crabbox` administrator.
- Windows auto-logon starts a visible console session for that user.
- TightVNC runs in that logged-in user session, with its HKCU password values
copied from the service configuration during startup.
- The generated password is stored at
`C:\ProgramData\crabbox\vnc.password`.
- VNC remains reachable only through the SSH tunnel.
@ -103,6 +105,13 @@ VNC opens an OS credential prompt
Check `managed:` in `crabbox vnc` output. If it is `false`, you opened a static
host. Use that host's credentials and pass `--host-managed` intentionally.
WebVNC keeps retrying in the browser
Close any older retrying tab and start a fresh `crabbox webvnc` bridge. A stale
tab can keep reconnecting with an old URL fragment. On managed AWS Windows,
Crabbox configures TightVNC in the logged-in user's registry profile; if direct
VNC auth also fails, recreate the lease with a current Crabbox build.
Related docs:
- [Interactive desktop and VNC](interactive-desktop-vnc.md)

View File

@ -336,15 +336,23 @@ $password = (Get-Content -Raw -LiteralPath (Join-Path $base "vnc.password")).Tri
$serverKey = "HKCU:\Software\TightVNC\Server"
$serviceKey = "HKLM:\Software\TightVNC\Server"
$serviceConfig = Get-ItemProperty -Path $serviceKey -ErrorAction SilentlyContinue
function Set-TightVNCBinaryValue($Name) {
$hex = ""
if ($serviceConfig -and $serviceConfig.$Name) {
$bytes = [byte[]]$serviceConfig.$Name
if ($bytes -and $bytes.Length -gt 0) {
$hex = -join ($bytes | ForEach-Object { $_.ToString("X2") })
}
}
if ($hex) {
& reg.exe add "HKCU\Software\TightVNC\Server" /v $Name /t REG_BINARY /d $hex /f | Out-Null
}
}
New-Item -Force -Path $serverKey | Out-Null
New-ItemProperty -Force -Path $serverKey -Name UseVncAuthentication -PropertyType DWord -Value 1 | Out-Null
if ($serviceConfig -and $serviceConfig.Password) {
New-ItemProperty -Force -Path $serverKey -Name Password -PropertyType Binary -Value $serviceConfig.Password | Out-Null
}
Set-TightVNCBinaryValue "Password"
New-ItemProperty -Force -Path $serverKey -Name UseControlAuthentication -PropertyType DWord -Value 1 | Out-Null
if ($serviceConfig -and $serviceConfig.ControlPassword) {
New-ItemProperty -Force -Path $serverKey -Name ControlPassword -PropertyType Binary -Value $serviceConfig.ControlPassword | Out-Null
}
Set-TightVNCBinaryValue "ControlPassword"
New-ItemProperty -Force -Path $serverKey -Name AllowLoopback -PropertyType DWord -Value 1 | Out-Null
New-ItemProperty -Force -Path $serverKey -Name AcceptHttpConnections -PropertyType DWord -Value 0 | Out-Null
$exe = "C:\Program Files\TightVNC\tvnserver.exe"

View File

@ -134,6 +134,9 @@ func TestAWSUserDataWindowsProfile(t *testing.T) {
"CrabboxUserVNC",
"crabbox-user-vnc.cmd",
"start-user-vnc.ps1",
"Set-TightVNCBinaryValue",
`reg.exe add "HKCU\Software\TightVNC\Server"`,
`$hex = -join ($bytes | ForEach-Object { $_.ToString("X2") })`,
"-run",
"Set-Service -StartupType Disabled",
"New-CrabboxPassword",

View File

@ -248,6 +248,8 @@ func webVNCPortalURL(base, leaseID, username, password string) string {
}
u.Path = strings.TrimRight(u.Path, "/") + "/portal/leases/" + url.PathEscape(leaseID) + "/vnc"
u.RawQuery = ""
u.Fragment = ""
u.RawFragment = ""
if strings.TrimSpace(username) != "" || strings.TrimSpace(password) != "" {
values := url.Values{}
if strings.TrimSpace(username) != "" {
@ -256,7 +258,10 @@ func webVNCPortalURL(base, leaseID, username, password string) string {
if strings.TrimSpace(password) != "" {
values.Set("password", strings.TrimSpace(password))
}
u.Fragment = values.Encode()
u.RawFragment = values.Encode()
if fragment, err := url.PathUnescape(u.RawFragment); err == nil {
u.Fragment = fragment
}
}
return u.String()
}

View File

@ -22,6 +22,12 @@ func TestWebVNCURLs(t *testing.T) {
if got := webVNCPortalURL("https://crabbox.openclaw.ai/", "cbx_abcdef123456", "ec2-user", "secret value"); got != "https://crabbox.openclaw.ai/portal/leases/cbx_abcdef123456/vnc#password=secret+value&username=ec2-user" {
t.Fatalf("portal URL=%q", got)
}
if got := webVNCPortalURL("https://crabbox.openclaw.ai/", "cbx_abcdef123456", "", "Cb1!abc"); got != "https://crabbox.openclaw.ai/portal/leases/cbx_abcdef123456/vnc#password=Cb1%21abc" {
t.Fatalf("portal URL=%q", got)
}
if got := webVNCPortalURL("https://crabbox.openclaw.ai/#stale", "cbx_abcdef123456", "", ""); got != "https://crabbox.openclaw.ai/portal/leases/cbx_abcdef123456/vnc" {
t.Fatalf("portal URL=%q", got)
}
}
func TestConnectWebVNCBridgeRegistersAgentBeforeServe(t *testing.T) {

View File

@ -120,6 +120,8 @@ $sshPorts = ${windowsSSHPortsPowerShell(config)}
$vncPasswordPath = "C:\\ProgramData\\crabbox\\vnc.password"
$windowsUsernamePath = "C:\\ProgramData\\crabbox\\windows.username"
$windowsPasswordPath = "C:\\ProgramData\\crabbox\\windows.password"
$userVNCStartupPath = "C:\\ProgramData\\crabbox\\start-user-vnc.ps1"
$userVNCStartupCommandPath = Join-Path (Join-Path (Join-Path "C:\\Users" $user) "AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup") "crabbox-user-vnc.cmd"
$setupCompletePath = "C:\\ProgramData\\crabbox\\setup-complete"
$openSSHZip = "$env:TEMP\\OpenSSH-Win64.zip"
$gitInstaller = "$env:TEMP\\Git-2.52.0-64-bit.exe"
@ -209,8 +211,43 @@ if (-not (Test-Path -LiteralPath "C:\\Program Files\\TightVNC\\tvnserver.exe"))
"SET_ACCEPTHTTPCONNECTIONS=1", "VALUE_OF_ACCEPTHTTPCONNECTIONS=0"
) -Wait
}
Get-Service -Name tvnserver -ErrorAction SilentlyContinue | Set-Service -StartupType Automatic
Start-Service -Name tvnserver -ErrorAction SilentlyContinue
$userVNCStartup = @'
$ErrorActionPreference = "SilentlyContinue"
$serverKey = "HKCU:\\Software\\TightVNC\\Server"
$serviceKey = "HKLM:\\Software\\TightVNC\\Server"
$serviceConfig = Get-ItemProperty -Path $serviceKey -ErrorAction SilentlyContinue
function Set-TightVNCBinaryValue($Name) {
$hex = ""
if ($serviceConfig -and $serviceConfig.$Name) {
$bytes = [byte[]]$serviceConfig.$Name
if ($bytes -and $bytes.Length -gt 0) {
$hex = -join ($bytes | ForEach-Object { $_.ToString("X2") })
}
}
if ($hex) {
& reg.exe add "HKCU\\Software\\TightVNC\\Server" /v $Name /t REG_BINARY /d $hex /f | Out-Null
}
}
New-Item -Force -Path $serverKey | Out-Null
New-ItemProperty -Force -Path $serverKey -Name UseVncAuthentication -PropertyType DWord -Value 1 | Out-Null
Set-TightVNCBinaryValue "Password"
New-ItemProperty -Force -Path $serverKey -Name UseControlAuthentication -PropertyType DWord -Value 1 | Out-Null
Set-TightVNCBinaryValue "ControlPassword"
New-ItemProperty -Force -Path $serverKey -Name AllowLoopback -PropertyType DWord -Value 1 | Out-Null
New-ItemProperty -Force -Path $serverKey -Name AcceptHttpConnections -PropertyType DWord -Value 0 | Out-Null
$exe = "C:\\Program Files\\TightVNC\\tvnserver.exe"
Get-Process tvnserver -ErrorAction SilentlyContinue | Where-Object { $_.SessionId -eq (Get-Process -Id $PID).SessionId } | Stop-Process -Force -ErrorAction SilentlyContinue
Start-Sleep -Milliseconds 500
Start-Process -FilePath $exe -ArgumentList "-run" -WindowStyle Minimized
'@
Set-Content -Encoding UTF8 -LiteralPath $userVNCStartupPath -Value $userVNCStartup
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $userVNCStartupCommandPath) | Out-Null
Set-Content -Encoding ASCII -LiteralPath $userVNCStartupCommandPath -Value ('@echo off' + [Environment]::NewLine + 'powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + $userVNCStartupPath + '"' + [Environment]::NewLine)
$startupTask = "CrabboxUserVNC"
cmd.exe /c "schtasks.exe /Delete /TN $startupTask /F 2>NUL" | Out-Null
schtasks.exe /Create /TN $startupTask /SC ONCE /ST ((Get-Date).AddMinutes(1).ToString("HH:mm")) /TR "powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File $userVNCStartupPath" /RU $user /IT /F | Out-Null
Get-Service -Name tvnserver -ErrorAction SilentlyContinue | Set-Service -StartupType Disabled
Stop-Service -Name tvnserver -Force -ErrorAction SilentlyContinue
$winlogon = "HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon"
Set-ItemProperty -Path $winlogon -Name AutoAdminLogon -Value "1" -Type String
Set-ItemProperty -Path $winlogon -Name ForceAutoLogon -Value "1" -Type String

View File

@ -143,6 +143,12 @@ describe("cloud-init bootstrap", () => {
expect(got).toContain("tightvnc-2.8.85-gpl-setup-64bit.msi");
expect(got).toContain("VALUE_OF_PASSWORD=$vncPassword");
expect(got).toContain("VALUE_OF_ALLOWLOOPBACK=1");
expect(got).toContain("CrabboxUserVNC");
expect(got).toContain("crabbox-user-vnc.cmd");
expect(got).toContain("start-user-vnc.ps1");
expect(got).toContain("Set-TightVNCBinaryValue");
expect(got).toContain('reg.exe add "HKCU\\Software\\TightVNC\\Server"');
expect(got).toContain('$hex = -join ($bytes | ForEach-Object { $_.ToString("X2") })');
expect(got).toContain("New-CrabboxPassword");
expect(got).toContain("${userSID}:F");
expect(got).toContain("C:\\ProgramData\\crabbox\\windows.username");