Adds diagnostic infrastructure for #118 (black tile investigation) and
similar protocol-level rendering bugs. The browser is the most honest
observer of what guacd actually emits, so tapping the instruction stream
here lets us see drawing-op-level detail without changing guacd or
chasing the resize-callback theory.
The previous logger printed each opcode + arg count once per session,
which doesn't help locate a black region. This logs the meaningful args
of draw opcodes (rect, cfill, copy, img, dispose, size) into a 2000-entry
ring buffer, decoded into {op, layer, rect, note}. Coverage is zero-cost
in the steady state; the buffer is consulted only when the user calls
the dump helper from the DevTools console.
Two entry points:
rustguacDumpDraws() Print all recent draw ops as a console.table.
rustguacDumpDraws(x, y) Filter to ops whose rectangle covers that
pixel - point at a black tile's top-left to
see exactly what painted there.
`cfill` with rgba(0,0,0,255) is annotated `<BLACK>` so guacd-emitted
black fills jump out. `copy` ops carry the source layer + coords in the
note so we can see when guac_display's search_for_copies optimisation
sources a region that was itself black or uninitialised.
For live per-op logging append `?debug=draw` to the client URL; the
ring buffer is recorded regardless.
Ctrl+V (and any modifier chord) could leave Ctrl/Alt/Shift stuck "down"
on the remote. The Ctrl+V paste path calls navigator.clipboard.readText(),
which shifts focus to a clipboard-permission prompt; the subsequent
modifier keyup then lands off-page and Guacamole.Keyboard never sees it,
so the keyup is never forwarded to the remote. The modifier stays held
until pressed again.
Adds blur + visibilitychange handlers that call keyboard.reset(), which
releases every tracked key (firing onkeyup -> sendKeyEvent(0, ...) for
each). This is the canonical Guacamole guard for focus-loss key sticking
and also covers alt-tab-with-modifier-held and clicking away to another
app. We already reset() on clipboard/file panel close; this extends the
same guard to window focus loss.
The previous commit (Fix zombie WebSocket) wired tunnel.onerror /
onstatechange into client.onerror so the disconnected overlay
appears when a mid-path WS drop is detected. That fixes the
"frozen tab" symptom but leaves the Reconnect button doing
window.location.reload(), which re-opens client.html?session=<id>
against a session that no longer exists. By the time the WS
proxy task in src/websocket.rs returns BrowserEnded, disconnect_viewer
has decremented active_connections to 0 and complete_session has
flipped status to Completed; the guacd stream was dropped when the
proxy task ended. So a reload just walks back into the same overlay.
Reconnect now relaunches against the original Connections entry
when one exists, which is the user's actual intent. The fetch at
the top of setupClient already pulls /api/sessions/:id; we now
stash address_book_entry, address_book_folder, and
entry_display_name from that response into a relaunchInfo object.
On Reconnect click:
- shareToken viewer: reload (unchanged; nothing better to offer).
- relaunchInfo present: POST to
/api/addressbook/folders/{scope}/{folder}/entries/{entry}/connect
with the current window dims and DPI, then navigate to
data.client_url + ?name=<display>. Fresh session id, fresh
guacd connection, RDP/xrdp resumes the desktop state on the
server side.
- relaunchInfo absent (ad-hoc) or any non-2xx (412 missing creds,
403 forbidden, 404 entry deleted) or network error: redirect to
/connections.html so the user can fill in what is missing or
pick somewhere else to go.
Splitting scope/folder/entry from the composite entry_key relies
on the constraint enforced in vault.rs validate_name: entry names
cannot contain slashes. Folder paths can, but only ever sit
between scope and entry, so first-slash gives scope and last-slash
gives entry.
Bundled fix: client.onerror now clearInterval(_thumbInterval) when
it runs. That closes the secondary leak called out in the prior
commit message (thumbnail XHR uploader continuing to 404 against
an already cleaned-up session) for users who walk away from the
overlay rather than clicking Close or Reconnect immediately.
Web sessions (Chromium + Xvnc) are entry-backed too and will
relaunch via the same path. The new session means a fresh
Chromium process with empty tab state, but that matches what the
user expects from Reconnect on a web session anyway.
When a mid-path middlebox silently drops the TCP between the browser
and HAProxy, HAProxy closes the backend which rustguac logs as
"Connection reset without closing handshake" (termination state CD--
in HAProxy's log). Firefox's WS socket then fires onclose on the
browser side, Tunnel.js's close_tunnel() runs, and the tunnel's
internal state transitions to CLOSED.
But upstream Guacamole's Client.js doesn't listen for tunnel.onerror
or tunnel.onstatechange — the Apache webapp's AngularJS wiring does
that externally. Our lean client.html inherited Tunnel.js + Client.js
verbatim from upstream but missed that glue, so tunnel errors fired
into the void and the Guacamole client stayed in CONNECTED forever.
Effect on users: a dead session that looks like a freeze. Mouse moves
locally, clicks don't register (they're being sent into a closed
WebSocket whose send() silently no-ops at the browser layer). The
"Session Ended" overlay never appears. Meanwhile the thumbnail
uploader keeps running on its own XHR stream, getting 404s from the
already-cleaned-up session.
Fix: wire tunnel.onerror to forward into the existing client.onerror
handler, and wire tunnel.onstatechange so CLOSED also triggers the
overlay (and UNSTABLE updates the status text). This reuses the
existing overlay wiring at client.html:895; no new UI, no new state
machine, no new heartbeat. The upstream-inherited 5s nop keepalive
and 15s receiveTimeout in Tunnel.js are already present and
correct — the missing piece was just propagating their output into
the client's state cascade.
Triggered a lot of detective work chasing false leads (H.264 decoder
closed-state hypothesis, h2 bridging bug, HAProxy timeout tuning,
client/server heartbeats). The actual cause was much simpler and
the diff is six lines of JavaScript.
Feature #103: single-entry auto-connect
- New `auto_open_if_singleton: Option<bool>` on AddressBookEntry and
EntryInfo. Admin ticks it per entry in the Connections modal;
importer initialises to None.
- After the /api/addressbook batch resolves on the Connections page,
if the user sees exactly one entry and it has the flag set, the
page fetches /connect and navigates the current tab to the new
session. Same-tab navigation (not a popup) because browsers block
window.open after an async fetch without a user gesture.
- A sessionStorage flag gates the auto-open to once per browser
session — an accidental refresh of Connections doesn't re-spawn
sessions. Logout clears the flag so the next login fires again.
- Admins never meet the singleton condition (they have many
entries) so this is effectively kiosk-only.
Escape hatch: client.html Home button
- Ctrl+Alt+Shift panel header now has a 🏠 Home button next to the
close ×. Takes the tab back to /connections.html — primary route
for a kiosk user whose session auto-opened into this tab.
Bug fix: folder-modal group picker silently added a group on save
- addFmGroupFromInput used to fall through to picking the combo's
highlighted suggestion when the input was empty. fm-save calls
addFmGroupFromInput to flush any typed-but-not-chipped text; with
an empty input that meant every Save on an existing folder tacked
on the first unselected known group (e.g. an operator group) —
visible only on the next edit. Split the "pick suggestion when
empty" behaviour out to an explicit helper that only fires from
Enter-with-empty-input, never from Save.
- Switch default theme from dark to aurora across server config,
all 9 static HTML pages, example config, and docs
- Fix theme docs: list all 8 presets (was 6, missing jaguar/aurora),
add missing type_vdi_bg/type_vdi_fg fields
- Add Vault/address book setup as recommended post-install step in
installation docs — the address book is the primary user-facing
feature and requires Vault
- Renumber subsequent install steps
The H.264 decode path bypassed the Guacamole sync mechanism — the sync
response fired immediately without waiting for WebCodecs to finish
decoding. guacd thought the client was keeping up and sent at full rate,
causing the decode queue to grow without bound (30+ seconds of lag
observed over time).
Fix: gate the sync response on H.264 decode completion so guacd gets
real backpressure. Also drop delta frames when the decode queue exceeds
5 frames as a safety valve for transient overload (tab backgrounding).
- Add pending-decode tracking and per-frame position capture to
H264Decoder.js (fixes shared mutable state race)
- Add waitForPending() with 1s safety timeout for sync gating
- Add frame dropping when decodeQueueSize > 5 (never drops keyframes)
- Add stats() method for console debugging (__guac_client._h264Decoder.stats())
- Gate sync response in Client.js on H.264 decode completion
Closes#93
Client captures display thumbnail every 10s (JPEG, 320px), uploads
to server. Address book shows "Active Sessions" section with thumbnail
grid — click to reconnect via address book connect flow.
- PUT/GET /api/sessions/{id}/thumbnail endpoints
- GET /api/vdi/containers — list running VDI containers for current user
- VDI container labels: rustguac.entry, rustguac.image for reconnect
- Thumbnail copied to container-keyed file on disconnect (persists)
- Dormant VDI containers shown with last thumbnail
- Cache-busting on thumbnail images for live updates
- Moved logout button out of settings dropdown
- Fixed: VDI containers endpoint in authenticated route group
- Fixed: thumbnail click uses address book connect (not stale session URL)
Show a centered overlay when the Guacamole client disconnects or
errors, instead of leaving a frozen canvas. Offers Reconnect (reload)
and Close (navigate to home) buttons. Applies to all session types.
API key users now exchange their key for a single-use 30-second
ticket via POST /api/ws-ticket before connecting. The ticket is
passed as ?ticket= in the WebSocket URL instead of the raw API key.
Prevents API key exposure in reverse proxy logs, browser history,
and Referer headers. Legacy ?key= still works as fallback.
Phase 1-3 of H.264 passthrough for premium RDP video performance.
guacamole-server patch (patches/004-h264-passthrough.patch):
- Enable GfxH264 and GfxAVC444 in FreeRDP settings when GFX is on
- Intercept AVC420/AVC444 SurfaceCommand before GDI decode
- Extract raw H.264 NAL units and send as new "h264" instruction
- Keyframe detection via Annex B start code + NAL type parsing
- Original GDI decode path still runs as fallback
Browser-side (static/guac/):
- H264Decoder.js: WebCodecs VideoDecoder, hardware-accelerated decode
- Client.js: "h264" instruction handler, base64→ArrayBuffer→decode
- Feature detection: falls back gracefully if WebCodecs unavailable
rustguac:
- Advertise video/h264 in guacd handshake
Audio:
- RDP audio output now works through guacamole. Advertise audio/L16
and audio/L8 mimetypes in the guacd handshake, and explicitly set
disable-audio=false. Fixed mimetype mismatch that silently prevented
guacd from creating audio streams.
- Browser AudioContext auto-resumed on user interaction (click/keydown)
to comply with autoplay policy.
Video performance:
- Per-entry GFX pipeline toggle (enable_gfx) — enables RemoteFX codec
- Per-entry desktop composition toggle (enable_desktop_composition)
- Per-entry force lossless toggle (force_lossless) — PNG-only mode
- WebSocket proxy buffer increased from 8KB to 64KB
- Video Performance section in address book UI for RDP entries
Documentation:
- RDP Video Performance guide with Windows server tuning (AVC444,
60fps, GPU encoding) and Linux xrdp setup (Debian 13)
- contrib/setup-xrdp-gfx.sh — automated GFX/H.264 setup for xrdp
- contrib/setup-xrdp-audio.sh — automated PulseAudio module build
Credential variables — address book entries reference $domain_username /
$domain_password instead of storing static credentials. Users fill in their
own values via My Credentials (gear menu), stored per-user in Vault KV.
All variables set → silent launch; missing → prompted. Hyphens allowed in
variable names. Docs section added.
Bug fixes:
- Rate limiting disabled by default; opt-in via rate_limit = true (#62)
- Docker: copy FreeRDP guac-common-svc plugins to runtime image (#64)
- Docker/install: add chromium-sandbox package for non-root web sessions (#61)
- Logo: skip redundant JS src= when server-side branding already set (#65)
- Sessions page: hide Open/Share buttons for non-active sessions (#63)
- Drive: expose drive_configured in /api/auth/status, warn in UI when
[drive] not configured
- install.sh: verify FreeRDP plugin installation
UI polish:
- Nav bar: border separator + spacing between header and nav on all pages
- Address book: password show/hide toggle on all password fields
- Drive diagnostic logging (session.rs, websocket.rs, client.html)
Closes#61, #62, #63, #64, #65
- Send actual browser window dimensions at session creation (addressbook,
sessions) instead of defaulting to 1920x1080
- Send initial sendSize() on WebSocket connect so guacd can resize the
remote display via Display Update channel
- Debounce window resize handler (250ms) to avoid overwhelming RDP server
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Vault TLS: replace hardcoded danger_accept_invalid_certs(true) with
configurable tls_skip_verify option (default: false)
- Share tokens: use constant-time SHA-256 hash comparison to prevent
timing side-channel attacks
- OIDC pending states: add 10-minute TTL, evict stale entries on each
login to prevent unbounded HashMap growth
- Recording path traversal: add canonical path validation as defense-
in-depth alongside existing string checks
- Frontend XSS: escape all user-controlled data (filenames, paths) in
innerHTML via escapeHtml/escapeAttr in client.html and recordings.html
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>