Skip to content

Spike: live UltraVNC (Windows) and Apple Screen Sharing (macOS) on hosted runners - #335

Closed
sibson wants to merge 4 commits into
mainfrom
claude/spike-os-servers
Closed

Spike: live UltraVNC (Windows) and Apple Screen Sharing (macOS) on hosted runners#335
sibson wants to merge 4 commits into
mainfrom
claude/spike-os-servers

Conversation

@sibson

@sibson sibson commented Aug 13, 2026

Copy link
Copy Markdown
Owner

What this is

Viability spike for Tier 2 of the server compatibility plan: can GitHub-hosted windows-latest / macos-latest runners host live OS-bound VNC servers for vncdotool to test against? Verdict: viable on both, with one macOS caveat. Final spike-os-servers run is green on both jobs after four evidence-driven rounds. Single file added: .github/workflows/spike-os-servers.yml.

Windows / UltraVNC — works end-to-end

  • choco install ultravnc; winvnc.exe is at the fixed path C:\Program Files\uvnc bvba\UltraVNC\winvnc.exe (do not search Program Files recursively — minutes-slow on runner images)
  • UltraVNC refuses all connections until a password is set, regardless of AuthRequired — no no-auth shortcut exists (server literally replies "Until a password is set, incoming connections cannot be accepted")
  • The ultravnc.ini passwd= hex uses the classic VNC password-file obfuscation (DES with the fixed vncauth.c key, bit-reversed) — computed in a few lines of Python on the runner; note this is not the challenge-response transform
  • Must run as a Windows service (winvnc.exe -install + Start-Service); winvnc.exe -run opens an interactive settings dialog instead of serving
  • vncdo type + capture succeed with genuine, non-black desktop content → Windows supports real visual assertions

macOS / Screen Sharing — works at the protocol level

  • Create a dedicated user (sysadminctl -addUser), enable Remote Management via kickstart; Screen Sharing is socket-activated on :5900 out of the box
  • Auth is ARD/Diffie-Hellman (type 30) with username+password — vncdotool's existing ARD support handles the full round trip (connect, auth, type, capture)
  • Legacy VNC password is confirmed dead on modern macOS: kickstart accepts -setvnclegacy/-setvncpw silently but the server still demands ARD auth
  • Caveat: the captured framebuffer is fully black — the runner has no rendered desktop. macOS jobs therefore validate protocol/auth/input, not pixels, pending a follow-up into whether a display can be attached
  • Bonus robustness finding: with no username supplied, rfb.py falls into an interactive input("username:") prompt and dies with EOFError on a TTY-less runner — filed under the plan's Phase 1 fail-loudly items

Spike status

Spike-quality proof, triggered only on pushes to this branch + manual dispatch. Graduation (per docs/server-compatibility-plan.md): both jobs move under the change-triggered/path-filtered policy — Windows as a full type+capture check, macOS with pixel assertions explicitly excluded — with passwords moved to repository secrets and the dedicated-user/service-mode setup retained.


Generated by Claude Code

claude added 4 commits August 13, 2026 18:01
Round 1: test whether windows-latest can host UltraVNC (no-auth,
loopback-only) and macos-latest can host Screen Sharing / Remote
Management via kickstart, both driven by vncdotool over the real
network stack. Windows tries an unauthenticated loopback config
first; macOS creates a dedicated local user and tries ARD/DH auth
with a legacy-VNC-password fallback. Jobs are independent so one
OS failing does not cancel the other, and both upload screenshots
plus diagnostic logs regardless of outcome.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STWkReabCK9sJW2qQ45brM
Round 1 results:
- macOS: ARD/Diffie-Hellman auth via a dedicated local user worked
  end to end (connect, handshake, auth, type, capture) but the
  captured framebuffer was fully black. The legacy VNC password path
  is confirmed non-functional - kickstart accepts the command but
  the server still demands ARD/DH auth with a username.
- Windows: a full recursive scan for winvnc.exe over C:\Program Files
  took over 5 minutes on the loaded windows-latest toolcache, and
  launching winvnc.exe directly (-run) opened an interactive
  "Settings" window rather than truly headless-serving, so the RFB
  connection failed silently under default (non-verbose) logging.

This round hardcodes the known choco install path, switches Windows
to service mode (UltraVNC's normal unattended-deployment path), and
runs vncdo at -v -v with explicit exit-code checks so a real failure
is visible instead of silently falling through. The macOS job drops
the non-functional legacy attempt from its pass/fail gate and adds
diagnostics aimed at explaining the black framebuffer (caffeinate to
rule out display sleep, console/session/display state dumps); a
black-but-connected result still counts as a working screenshot per
the spike's success criteria and no longer fails the job.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STWkReabCK9sJW2qQ45brM
Round 2 result: with service mode fixed, vncdo successfully connected
and typed, but capture failed with a definitive server-side error:
"This server does not have a valid password enabled. Until a password
is set, incoming connections cannot be accepted." This holds
regardless of AuthRequired, so a real password is mandatory rather
than optional for UltraVNC.

This round computes the UltraVNC ini's DES-encrypted passwd field
using the exact same RFB DES-variant key transform vncdotool itself
implements in rfb.py's _vnc_des(): the password's bits are reversed
per byte to form a DES key, which then encrypts 8 zero bytes; the
result is hex-encoded with a trailing 00 update-flag byte. Verified
locally that this key derivation is byte-for-byte identical to
_vnc_des() and that the DES encryption round-trips correctly; whether
UltraVNC's ini format accepts this exact encoding is what this run
will confirm empirically.

macOS is unchanged from round 2, where it passed: ARD/Diffie-Hellman
auth via a dedicated local user connects, authenticates, types, and
captures successfully end to end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STWkReabCK9sJW2qQ45brM
Round 3 result: with a passwd= field set, UltraVNC dropped the
"no password enabled" rejection and offered VNC_AUTHENTICATION - but
the server rejected the auth challenge response for "vncspike1",
meaning the password it decoded from our ini hex was wrong. That
hex was computed by using the password itself (bit-reversed) as a
DES key to encrypt 8 zero bytes - the same transform vncdotool's
rfb.py _vnc_des() uses for the RFB challenge *response*, but that's
the wrong primitive for password *storage*.

This round switches to the actual classic VNC password-file
algorithm shared by the whole VNC family (originating in AT&T's
vncauth.c, and still what TightVNC/TigerVNC/x11vnc/UltraVNC all
build on): a DES key derived from a fixed constant
{23,82,107,6,35,78,88,7} (via the same bit-reversal transform)
encrypts the null-padded password bytes as plaintext. That's the
inverse pairing of round 3 (fixed key encrypts the password, rather
than the password deriving a key that encrypts zeros).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STWkReabCK9sJW2qQ45brM
# artifact uploads and diagnostics.
#
# Round-1 findings baked into this version:
# - Recursively scanning C:\Program Files for winvnc.exe took 5+ minutes on the

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

too much omment here. narrative should be in docs and linked here not inline

@sibson

sibson commented Aug 17, 2026

Copy link
Copy Markdown
Owner Author

🤖 Claude Code: Superseded — Tier 2 graduation work already merged to main: setup/diagnostic scripts under tests/servers/ultravnc/ and tests/servers/screen-sharing/, tests/functional/test_os_servers.py, and docs/server-compatibility-plan.md updated with this spike's findings. This raw spike workflow is no longer needed.

@sibson sibson closed this Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants