The builder stage compiles the guacd SPICE plugin (--with-spice + the
-dev lib), but the runtime stage never installed the SPICE runtime
shared library. libguac-client-spice.so was present yet failed to
dlopen (libspice-client-glib-2.0.so.8 missing), so guacd reported
"Support for protocol spice is not installed". Add the runtime lib
to the runtime stage; apt pulls the rest of the chain (gstreamer,
opus). Bare-metal installs were unaffected. Broken since SPICE was
vendored (c2c5679).
build-deb.sh had lost its guacd pin during a patch rebase and was cloning
apache/guacamole-server master (--depth 1, latest) and applying patches
best-effort with git apply --check, silently skipping any that no longer
applied. The sol1 apt CI runs build-deb.sh in a fresh container, so its
.deb built guacd from whatever upstream master was that day, with patches
possibly dropped - a non-reproducible build that diverged from the pinned
release.yml and install.sh paths and shipped a broken guacd.
Restore GUACD_COMMIT=6719b20d (the commit the patch set is rebased onto,
matching release.yml and install.sh), build from a fresh pinned clone into
a throwaway dir (leaving any local ../guacamole-server checkout untouched;
GUACD_SRC_OVERRIDE reuses one for local patch iteration), and apply patches
strictly so a non-applying patch fails the build instead of silently
shipping without it.
Refresh Connections (credentials banner/pills, Credentials nav, larger
folder chevron), and the RDP/VDI/Web/SSH-tunnel editors, plus the
Sessions/Recordings/Reports/Admin pages (real data sanitised). Add a new
My Credentials screenshot (scroll/filter/collapse, masked fields). Drop
the stale version note. tokens_view and recordings_player_histogram are
left as their prior captures.
- configuration.md: [vault_shared]/[vault_local] backends, the DR rationale,
user_credentials_default_scope, and the VAULT_SHARED/LOCAL_SECRET_ID env vars.
- credential-variables.md: Credentials nav link, scroll/filter/collapsible
editor, resumable focused prompt, needed-pill + banner, and the shared/local
per-credential scope with its outage trade-off.
- migration.md: vault-migrate runbook (dry-run, copy, cutover ordering).
- CLAUDE.md: multi-Vault backend summary under the Vault section.
Dismissing the "N credentials to set up" banner now persists in
localStorage instead of a page-lifetime flag, so it stays gone across
refreshes. It records exactly which credentials were dismissed and
re-appears only if a genuinely new unset credential shows up — so a
partial save that shrinks the set won't re-nag, but a new requirement
still surfaces.
The collapsible group caret used the small-triangle glyph (U+25BE) like
the old folder chevron did, so it read faint. Switch to the full-size
U+25BC and bump its size, matching the folder-tree chevron.
The credential editor rendered a flat list inside a modal that scrolled as
a whole, so past a few variables the Save/Close buttons and context
scrolled off and it got unusable. Restructure it: the modal header (title,
description, filter box) and the Save/Close footer are pinned and only the
credential list scrolls. Add a filter box (shown once there are more than a
handful of variables) that live-filters rows by name/domain, and group each
domain into a collapsible section. Focused single-connect mode is unchanged
(few rows, no filter box).
Fetch the user's set credential variables on load and cross-reference
against each entry's credential_variables (already on EntryInfo). Entries
that reference variables the user hasn't set get a "credentials needed"
pill, and a dismissible banner reports how many credentials are still to
set up (aggregated across the search index). Both link to the editor, and
refresh after a save. Turns the old reactive "connect fails, then hunt for
the menu" flow into something visible up front.
When a connect fails for missing credential variables, open the My
Credentials editor focused on just the missing variables ("credentials
needed to connect to X") and, on save, re-run that exact connect instead
of dead-ending the user in the full editor with a toast. openMyCredentials
now takes { onlyVars, heading, onSaved }.
Also promote My Credentials to a visible "Credentials" nav link (kept in
the Settings menu too) so people can find it without hitting a failure
first.
Each per-user credential variable can live in the shared backend (fleet-
wide) or the local backend (this instance) - its location is the scope.
Reads merge both backends (local wins); writes partition by a per-key
scope sent from My Credentials, moving a key between backends when its
toggle changes and honouring "leave blank to keep". A "Shared across
sites" toggle appears per credential.
Quick-start preserved: with one Vault the shared and local cells alias the
default, creds_split() is false, the toggle is hidden, and everything is
written to the single store in one call - byte-identical to before. New
config user_credentials_default_scope (default "local") seeds new toggles.
The write partition is a pure function with unit tests covering single-
store, blank-keeps-existing, scope routing, cross-scope moves, and the
default-scope fallback.
list_all_folders now returns (folders, unavailable_scopes); a scope whose
dedicated backend is down is reported instead of silently dropped, and
GET /api/addressbook includes unavailable_scopes. The Connections tree
renders a non-interactive greyed "<scope> connections unavailable" node
so a partial outage is visible while other scopes stay usable; the 15s
/api/me poll refreshes it.
Single-Vault is unaffected: every scope aliases the one backend, so
unavailable_scopes is always empty while it is up (full outage still
returns 503 and the existing banner). Adds a test asserting a lone
[vault] block is sufficient (quick-start invariant).
Copies an address-book scope subtree (entries + every folder's .config,
so allowed_groups/inherit_from_parent come along) between two configured
backends, resolving each from config + its VAULT_*_SECRET_ID. Because the
scope->path layout is identical in every backend, this is a same-identity
server-to-server copy, not a path rewrite.
Flags: --scope shared|instance, --from/--to (vault|vault_shared|
vault_local), --dry-run, skip-existing by default with --overwrite, and
--users to bulk-copy users/* (documented as "makes them shared"). Exits
non-zero if any copy fails. Adds get/put_user_credentials_by_key and
list_user_keys to VaultClient for the --users path.
Add optional [vault_shared] and [vault_local] config blocks so the shared
and instance scopes can each be served by a dedicated Vault, with its own
connection, retry loop, and token renewal. A bare [vault] is unchanged:
shared and local alias the default cell, so every scope resolves to the
one connection exactly as before.
VaultBackends gains scope-dispatching wrappers (get_entry, list_entries,
resolve_folder_access, ...) that route by the scope argument, plus a
fan-out list_all_folders across scopes tolerant of a down backend. Folder
listing is split into list_folders_in_scope; a new VaultError::Unavailable
marks a configured-but-unconnected backend. Per-user credential variables
and the LUKS key stay on the default backend for now.
Secret IDs: VAULT_SECRET_ID (default), VAULT_SHARED_SECRET_ID,
VAULT_LOCAL_SECRET_ID.
The folder expand/collapse indicator used the small-triangle glyphs
(U+25BE/U+25B8), which render as faint, hard-to-see ink even at 20px,
while every other collapsible on the page uses the full-size triangles
(U+25BC/U+25B6). Switch the folder chevron to match so it is clearly
visible.
Wrap the single Vault connection cell in a VaultBackends struct that will
hold per-scope backends (default/shared/local). Today all three alias one
cell, so behaviour is identical; this isolates the ~20-handler type
plumbing from the upcoming multi-Vault backend split.
The Guacamole import parser scanned line by line, so it missed the
multi-row INSERT layout that modern mariadb-dump/mysqldump emit by
default (VALUES on its own line, one tuple per line). Valid dumps
reported "No connections found". Parse whole statements instead, so both
the default multi-line format and single-row dumps import correctly.
String literals, backtick identifiers, and SQL comments are skipped so
embedded semicolons and apostrophes cannot split or corrupt a statement.
Multi-monitor support for SPICE and RDP, plus an "open in new window" action
for address-book entries.
- SPICE multi-channel composite (Windows/PVE multi-QXL topology), main-window
crop to monitor 0, per-monitor pop-out windows.
- RDP multi-monitor via the Display Control channel (patch 010).
- Open a connection in its own browser window from the address book.
- Client: fit-to-window on connect, and pop-outs close/notify with the session.
For a multi-channel SPICE guest (each head is a separate display channel, e.g.
a Windows multi-QXL guest), publish_layout positioned each monitor at
origin_x + the guest-reported config->x/y. But config->x/y is the guest's own
virtual-desktop arrangement, not an offset within the channel's surface: once
the guest rearranges a secondary head (which happens as soon as the primary is
resized, e.g. by fit-on-connect), it reports config->x = primary width, so the
monitor was placed at origin_x + config->x (double-counted) and clamped to a
sliver off-canvas. This is exactly where the compositor already blits the
surface (at origin_x), so publish each channel's whole surface at its origin
and ignore the guest x/y. Single-channel guests (one combined surface with
several monitor regions) still split by the reported regions.
fit-on-connect: a session opened in a new window (at a fixed size), or otherwise
created at dimensions that differ from the viewing window, now fits the window
without a manual resize. The client previously only sent a size on a
window-resize event. The one-time fit is retried past the point where the resize
path becomes ready (RDP Display Control channel connected / SPICE agent and
display ready) because an early send is silently dropped; guacd coalesces the
retries into a single resize since the requested size is unchanged.
pop-out teardown: a monitor pop-out is only a satellite of the main window (it
blits from that window's canvas and sends input through its client), so it
cannot outlive it. Closing the main window now closes its pop-outs instead of
leaving a frozen orphan, and a session disconnect shows a "Disconnected" overlay
in the pop-out rather than a stale live image.
Adds RDP multi-monitor, reusing the protocol-agnostic client work already used
by SPICE (secondary-monitors advertisement, multimon-layout cropping, pop-out
windows, native-mouse mapping).
guacd (patch 010): a "secondary-monitors" arg enables multi-monitor. The
Display Update module now tracks a per-monitor layout (tiled left-to-right,
top-aligned, widths rounded to RDP-valid geometry) and sends the full
DISPLAY_CONTROL_MONITOR_LAYOUT array via SendMonitorLayout rather than a single
monitor. The host extends its desktop across the monitors and streams one
combined framebuffer, so there is no client-side compositing (unlike SPICE).
guacd advertises secondary-monitors on user join and publishes the
multimon-layout so the client can split the framebuffer into per-monitor
windows.
rustguac: RdpParams gains secondary_monitors, wired from the entry's
max_monitors and sent as the secondary-monitors connect arg (resize-method is
already display-update, which the Display Control channel requires). The
address-book entry editor gains a Monitors field for RDP.
Validated against a Windows RDP host: ticking a second monitor extends the
desktop, the second head renders and resizes, and clicks land on both.
Each entry gains a compact icon button beside Connect that launches the
session in a separate browser window (which can be dragged to another
physical display, and its secondary monitors popped out from there). Connect
is unchanged (opens a new tab).
The window is opened synchronously in the click handler so it is not
popup-blocked after the async connect fetch, then navigated once the session
exists; it also works through the credential prompt. The icon is a compact,
vertically-centred sibling that stays on one line with Connect.
guacd (patch-008): composite multiple SPICE display channels into one
framebuffer. A Windows/PVE guest exposes each head as a separate QXL device
(a separate display channel with its own primary surface); the previous code
tracked a single display channel and shared surface, so a second head
clobbered it and the session showed black or dropped. Now each channel is
tracked by id, tiled left-to-right into the combined default layer, and the
multimon layout enumerates every head with its offset. The SPICE pointer is
mapped from the combined coordinate to the owning head's local coordinate
(clamped to bounds so an edge value maps to the correct head).
client: the main window now crops to monitor 0 instead of showing the whole
combined framebuffer, and rescales to fit on resize. Secondary monitors open
in their own window that fills and rescales; that window's pointer uses native
mouse events mapped from the canvas's live rect, because Guacamole.Mouse does
not track the X axis correctly in a popup window. The per-frame blit clamps to
the source framebuffer bounds so a resize cannot read past it and produce a
corrupt image.
Validated on a Windows PVE console (VMID 300): both heads render composited,
clicks land on target on both monitors, and resizing the second monitor is
stable.
client.html: per-monitor tick-boxes (contiguity-enforced) that open each
secondary monitor in its own window, blitting that monitor's region of the
combined framebuffer (rAF) and forwarding mouse (combined coords) + keyboard;
untick/close releases the monitor. Requests floor to a sane size if a popup
reports zero dimensions.
Entry storage: AddressBookEntry/EntryInfo gain max_monitors, ab_connect_entry
passes it through, and the Proxmox editor gets a Monitors field. Lets a saved
Proxmox/SPICE entry offer multiple monitors (previously only the ad-hoc API
path could).
KNOWN ISSUE: enabling multi-monitor (secondary-monitors>0) intermittently drops
the SPICE connection ~2-6s after connect, independent of the request. Under
investigation; single-monitor is unaffected. A3 end-to-end (tick -> activate ->
render) still needs a real-browser verification (Playwright can't size popups).
Adds a Controls side panel (autohide-integrated, mirroring the clipboard/files
tabs) with a Send Keys section: Ctrl+Alt+Del, Ctrl+Alt+Bksp, Alt+F4, Alt+Tab,
Win, Esc, PrtSc, sent via a press-in-order/release-in-reverse key combo. Also a
Monitors section that reflects the server-advertised count (per-monitor tick-box
selection + rendering land in the next increment).
Verified on the canary vs VMID 300: Ctrl+Alt+Del triggers the Windows secure
attention screen; Monitors shows '2 monitors available'.
Client library (Client.js): new onmultimonlayout callback + a multimon-layout
layerPropertyHandler (mirrors multi-touch), and sendSize extended to carry the
optional per-monitor x_position/top_offset.
rustguac: SpiceParams.secondary_monitors + a secondary-monitors connect arg so
guacd advertises the allowed monitor count; CreateSessionRequest.max_monitors
(secondary = max-1) wired through both SPICE branches.
client.html: onargv reads the secondary-monitors count and onmultimonlayout
parses the layout JSON (logging for now; per-monitor windows are A2).
Verified on the canary vs VMID 300: client logs 'server allows 2 monitors' and
receives a multimon-layout. The layout is request-driven (guacd activates a
second guest head only when the client sends a size for monitor 1), so the
second monitor appears in A2.
Documents the new SPICE and Proxmox VE console session types (intro, architecture
diagram, and session-type table) and the headless ws-ticket API integration.
Also replaces em-dashes with colons/sentence breaks throughout the README to
match the project prose style.
Adds a 'Connecting to a session' section covering the owner-vs-join
distinction, the three owner-auth modes (OIDC cookie, sessionStorage key,
ws-ticket URL), POST /api/ws-ticket, and the end-to-end headless integration
recipe (mint a ticket, open /client/{id}?ticket=...). Documents the new
spice/proxmox/vdi session types and their spice_*/proxmox_* fields, and fixes
the stale share_url response example (was &key=; now ?token= with ws_url/status).
Clears Dependabot alerts GHSA-g9hv-x236-4qp3, GHSA-cqjc-rmpq-xprq,
GHSA-5xvq-cp9x-6p6r (russh pre/post-auth panics, patched in 0.62.4). Only the
fuzz harness's lockfile was affected; the shipped binary already uses russh
0.62.4 via the main lockfile, so v1.9.0 is not vulnerable.
The built-in client could only authenticate the owner WebSocket via an OIDC
session cookie or a sessionStorage API key. Headless API integrations have
neither, so the owner connection was rejected and guacd reported 'User is not
responding'. Now /client/{id}?ticket=<wst> is honoured: a backend mints a
single-use ticket via POST /api/ws-ticket and hands the browser a ready URL,
keeping the durable API key server-side. The metadata fetch is skipped in this
path (it needs its own auth and would consume the one-shot ticket).
Clears dependabot #182-#188. base64 needed the direct constraint widened to
0.23 (still used for JWT decode in oidc.rs); russh 0.62.4 moves curve25519/
ed25519-dalek off release candidates onto stable. 279 tests green, clippy clean.
For TLS-only SPICE (Proxmox), rustguac sends an empty plain port so guacd
uses tls-port. guac_spice_session_configure() set the spice-gtk port property
for any non-NULL settings->port, but an omitted arg parses to an empty string,
so spice-gtk logged 'Invalid port value' per channel. Only set the port when
non-empty.
Split SPICE into two connection types: "spice" (direct libvirt/QEMU) and
"proxmox" (PVE console brokered via the spiceproxy API). Both produce a guacd
SPICE connection.
- Deliver the SPICE ticket/password as a connect arg instead of a post-connect
argv stream, so it is set before guacd authenticates. Fixes an auth race that
produced intermittent "SPICE authentication failed".
- TLS-only SPICE sends an empty plain port so guacd connects via tls-port
rather than plaintext against a TLS endpoint.
- Proxmox node is optional: resolve it from the VM id via /cluster/resources
(as the PVE web UI does).
- Split the PVE API token into a visible Token ID (shown in the User column)
and a masked secret; join them as "id=secret" for the API.
- Surface the PVE response body on non-2xx (safe: only a 2xx spiceproxy
response carries a ticket), turning opaque 500s into actionable messages.
- SSH tunneling for Proxmox: tunnel both the PVE API call and the spiceproxy
connection through the jump-host chain in-branch. Also rewrite tls_port
(not port) for direct-SPICE TLS over a tunnel.
- Store proxmox fields on address book entries; populate Host/User columns;
orange Proxmox badge. Runtime dep: libspice-client-glib-2.0-8.
Add a just-in-time Proxmox broker for SPICE consoles. PVE issues one-time,
~30s SPICE tickets via its API, so they cannot be stored; the broker fetches
the config at connect time:
- src/pve.rs: minimal PVE API client. POSTs to
/api2/json/nodes/{node}/qemu/{vmid}/spiceproxy with an API-token header,
parses host / proxy / tls-port / password(ticket) / ca / host-subject, and
unescapes the CA PEM newlines. Never logs the token or ticket, and never
puts the response body (which carries the ticket) in an error.
- session.rs: CreateSessionRequest spice_pve_* fields (host/node/vmid/token/
verify_tls); when spice_pve_host is set, the SPICE create_session branch
calls the broker and maps the result onto SpiceParams (hostname=host, plus
proxy, tls, tls-port, ca-cert, cert-subject, and the argv ticket).
API-testable now (POST /api/sessions with session_type:spice + spice_pve_*).
Address-book entry storage + a Proxmox UI are the next increment.
Add SPICE to the connections entry editor: a SPICE type option, a fields
block (hostname / port / password / color-depth, plus TLS / tls-port /
ignore-cert / CA cert / cert-subject / proxy for connecting through a SPICE
proxy such as Proxmox's), and the show/hide, save, load, and clear wiring
mirroring the VNC type. connections.html is served from disk, so no binary
rebuild is needed for this file.
Wire SPICE as a first-class session type through the rustguac stack,
mirroring the VNC/RDP pattern:
- guacd.rs: SpiceParams + ConnectionParams::Spice + protocol select + arg
mapping. SPICE credentials (password/username) are streamed to guacd via an
argv stream after connect (send_argv), since guacd's SPICE client reads them
from argv, not the connect args. SPICE has no width/height/dpi connect args
(it sizes via the size instruction).
- session.rs: SessionType::Spice, CreateSessionRequest spice_* fields
(tls/tls-port/ca-cert/cert-subject/proxy), a SPICE create_session branch,
and tunnel host/port handling.
- vault.rs/api.rs/import.rs: AddressBookEntry + EntryInfo spice_* fields
threaded through the connect / quick-connect / import paths.
The tls/ca-cert/cert-subject/proxy fields lay groundwork for brokered Proxmox
VE consoles. No connections.html UI yet (to follow); usable via the API.
Vendors native SPICE protocol support (libguac-client-spice) from upstream
PR apache/guacamole-server#688 (GUACAMOLE-261) as patch 008, on top of the
pinned guacd (6719b20d) + existing patches. Wires --with-spice and the
libspice-client-glib-2.0-dev build dep into build-deb.sh, install.sh,
Dockerfile, and dev.sh. guacclip is kept in the source but not built
(--disable-guacclip, like guacenc/guaclog); the PR's incidental non-SPICE
terminal.c keyboard change is excluded.
guacd builds green with libguac-client-spice on Debian 13 under -Werror.
rustguac-side wiring (SessionType::Spice / SpiceParams) still to come.
- Bump pin 2980cf0 -> 6719b20d in Dockerfile, install.sh, release.yml,
docs/installation.md. -Werror verified clean on the new base (the
GUACAMOLE-2221 pin reason no longer applies).
- Drop patch 006 (terminal OSC-consume): upstreamed as GUACAMOLE-2213
(guac_terminal_unknown_osc).
- Rebase patch 004 (H.264 display worker) onto the refactored libguac
display internals: the queued-H.264-frame free moved into the deferred
guac_display_free_removed_layers path.
- Patches 001/002/003/005/007 unchanged (apply clean on new base).
Local build green under -Werror (guacd + rdp/ssh/vnc). H.264 passthrough
still needs runtime verification on an xrdp+x264 target.
Add an autohide_side_tabs option (Option<bool>, default off) on the address
book entry, threaded through the same path as fullscreen_on_connect
(AddressBookEntry, EntryInfo, CreateSessionRequest, Session, SessionInfo, the
API connect/quick-connect builders, and import defaults). When set, client.html
slides the left-edge Clipboard and Files tabs off screen when idle and brings
them back when the pointer nears the left edge; defaults preserve the current
always-visible behaviour. Checkbox added to the entry editor.
validate_api_key and validate_user_token enforced expires_at only when it
parsed as strict RFC 3339, silently ignoring any other format, so a malformed
value (e.g. "2026-12-31" or the SQLite "YYYY-MM-DD HH:MM:SS" timestamp the DB
itself writes) let the credential authenticate forever. Add parse_expires_at,
which accepts RFC 3339, ISO-without-zone, SQLite datetime and bare dates
(end-of-day UTC), and treat an unparseable value as expired. Reasonable
formats now enforce correctly rather than locking the credential out.
get_vdi_container_thumbnail served any container's live desktop screenshot
to any authenticated user: it took only the container name and did no
ownership check, and names are the deterministic rustguac-vdi-{user}. Add an
owner-or-admin gate mirroring get_session_thumbnail: a caller may only read a
container derived from their own username (rustguac-vdi-{user}[-{entry}]);
admins may read any. Returns 404 for non-owners so container existence is not
leaked.
These three RDP visual flags were hardcoded off in guacd.rs. Expose them
as per-connection options (Option<bool>, default false) threaded through
the same path as enable_desktop_composition: RdpParams, the session
request, Vault entry + response, the API connect/quick-connect builders,
import defaults, and the connections.html entry editor (Video Performance
section). Defaults preserve existing behavior; VDI sessions stay off.
Cherry-picked from pletch/rustguac@da3cfda
The H.264 passthrough advertised GfxAVC444, so Windows hosts encoded with AVC444,
which splits the image across two bitstreams (luma main view + auxiliary chroma).
The passthrough only forwards bitstream[0], so the browser WebCodecs decoder
rendered a luma+chroma split — two blocks with green and magenta casts. RFX was
unaffected (separate codec path).
Set GfxAVC444 = FALSE in patches/004 (both the FreeRDP3 setter and direct-field
hunks), keeping GfxH264 = TRUE; AVC444v2 is never enabled and defaults off, so the
client now advertises AVC420-only. AVC420 carries a complete YUV420 frame the
decoder handles correctly. Verified against a Windows RDP session. README updated.
Cherry-picked from pletch/rustguac@17213e2