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
Backend stores timestamps as SQLite datetime('now') (UTC, no zone marker) and the
admin page printed them verbatim, so last-login/created/last-used/audit times read
as GMT. Add a localTime() helper that tags the unzoned string as UTC and renders
toLocaleString(); apply it to all full date-time cells. Date-only token columns
are left as UTC dates (localizing a 23:59:59Z expiry could roll the date a day).
Cherry-picked from pletch/rustguac@b5ea32e
Both of rustguac's socket hops carry tiny, latency-sensitive writes; under
default settings Nagle coalesces them against delayed-ACK, stalling input
and frame/sync traffic by ~40ms (up to ~200ms):
- rustguac -> guacd (apply_keepalive, covers both connect sites): forwards
mouse/keyboard input events.
- rustguac -> browser (both TLS and plain listeners): display frames and
H.264 sync acks. Linux propagates the option to accepted sockets, matching
how keepalive is already applied here.
socket2 exposes this as set_tcp_nodelay().
Cherry-picked from pletch/rustguac@a11e7a2
Two guacamole-server patches ported from pletch/guacamole-server
(fixes-1.6.0), verified to apply cleanly on top of 001-005 against the
pinned base (apache/guacamole-server@2980cf0):
- 006-terminal-osc-consume: route unrecognized OSC sequences to the APC
handler instead of reverting to echo (GUACAMOLE-2213). Fixes garbage
output from e.g. systemd OSC 3008 context sequences.
- 007-rdp-disp-mod16: round RDP display dimensions down to mod-16 to avoid
the green band along the bottom edge from H.264 macroblock padding;
complements 005 (legacy bitmap path) by covering the H.264/GFX path.
The fork's SO_ERROR connect fix (GUACAMOLE-2107) is already in the pinned
base, and its GFX H.264 enablement is already covered by 004.
Cherry-picked from pletch/rustguac@cfe2c2e
The recordings page rendered every recording into one table, which got
unwieldy with a large backlog and pushed the SSH Typescripts section far
down the page. Add client-side pagination (50/page, Prev/Next) to both
the recordings and typescripts lists; it composes with the existing
search and sort, and auto-refresh preserves the active filter + page.
Also surface where typescripts live on disk: /api/typescripts now returns
{path, items} (endpoint is new in this release, so no compatibility
break) and the typescript section shows "Stored at <path> on the rustguac
host" — useful since the content is intentionally not downloadable.
No new endpoint.
Typescript recording is now per-connection opt-in, off by default. Adds a
record_typescript flag on the address-book entry (Vault), threaded through
EntryInfo / CreateSessionRequest, and a "Enable typescript recording for
this session" checkbox in the connection editor's Recording Settings (SSH
entries only). The SSH branch records a typescript only when the entry has
opted in AND [recording].typescript_path is configured globally. Ad-hoc
SSH sessions (no entry) never record.
Docs: document the per-connection opt-in, and add a LUKS-at-rest recipe
(point typescript_path at a subdir of the LUKS-encrypted drive volume
rustguac already mounts) as the recommended way to encrypt typescripts at
rest with no extra infrastructure.
Add GET /api/typescripts (poweruser+) and an "SSH Typescripts" section on
the recordings page. List-only by design: it shows that a session was
recorded (name, size, time) but never serves or downloads the content.
Typescripts capture full terminal output, which can include passwords
typed at prompts or secrets printed to screen, so exposing the text via
the web UI would widen its blast radius. A poweruser gets accountability
(a session was recorded) while retrieving the actual log still requires
direct access to the rustguac host or storage. There is deliberately no
serve or delete endpoint, hence no name parameter and no path-traversal
surface. The .timing sidecar is filtered out so one row == one session.
Expose guacd's SSH typescript recording via a [recording] config block:
typescript_path / typescript_name / create_typescript_path. When
typescript_path is set, guacd writes a plain-text log of the full
terminal session (scriptreplay-compatible, greppable) for every SSH
session. Aimed at audit/compliance on network gear.
guacd does not template the typescript filename (it uses the name
verbatim and only appends a numeric suffix to avoid clobbering), so
rustguac expands its own brace tokens before passing the name on:
{user} {connection} {host} {date} {time} {session}. Substituted values
are sanitised to [A-Za-z0-9_-], so OIDC emails and free-text entry
names can't produce path separators or traversal. Default template is
{connection}-{user}-{date}-{time} for identifiable audit filenames.
recording-include-keys (keystroke logging in guacd's graphical
recording, for guaclog) is intentionally not wired up: rustguac records
the proxied stream itself rather than driving guacd-side graphical
recording, so that flag would be a no-op. The typescript is the
supported text-audit path.
Docs in configuration.md. 5 unit tests covering token expansion,
the default template, sanitisation/traversal, empty-value fallback,
and unknown tokens.
Closes#159.
The v1.7.2 floating "⛶ Fullscreen" corner button at 0.45 opacity was
still 80px of permanent clutter in the top-right of the remote session
display before fullscreen was entered. This PR moves the manual
fullscreen action into the existing Ctrl+Alt+Shift session-menu panel
(next to the Home button), removing the floating overlay entirely.
The per-entry `fullscreen_on_connect` flag and the in-fullscreen top
bar (entry name + Exit + Disconnect) are unchanged. Esc-key forwarding
via navigator.keyboard.lock still applies.
Also adds an "In-session keyboard shortcuts" section to
docs/web-sessions.md documenting the Ctrl+Alt+Shift panel toggle,
Ctrl+V clipboard paste-sync, Esc behaviour, and the disable_copy /
disable_paste interaction.
Closes#156.
Add 'Other Linux distributions' section explaining the FreeRDP ABI
mismatch that breaks drive/audio when running the Debian 13 .deb on
Ubuntu 24.04 (and likely other distros). Recommend the Docker image as
the supported path; provide an untested build-from-source recipe for
Ubuntu 24.04 against system FreeRDP 3.5. Mirror the pointer from
deployment-guide.md. Prompted by #153.
The in-fullscreen top bar covered the remote desktop's own menubar
(xfce4 panel, Windows taskbar). Match the mstsc.exe pattern that #154
referenced: show briefly on fullscreen entry, then slide up out of
view. Reveals when the mouse hits the top 4px edge; hides again ~600ms
after the mouse moves below the bar area. Hysteresis between 36 and 48
pixels keeps a jittering pointer from flickering the bar.
Covers the claim-vs-scope distinction that trips Entra setups when users
copy the Authentik example. Includes the AADSTS650053 error explainer
plus a troubleshooting section. Promised in #153.
Per-entry boolean fullscreen_on_connect flag. When set, the client enters
browser fullscreen on the first user gesture after CONNECTED and locks
the Escape key (Chromium navigator.keyboard.lock API) so it reaches the
remote session instead of exiting fullscreen. Firefox / Safari fall back
to standard fullscreen with a one-time toast explaining Esc will exit.
A small floating "Fullscreen" toggle in the top-right corner lets any
user enter fullscreen at any time once the session is connected. In
fullscreen mode a thin top bar shows the entry name plus Exit and
Disconnect buttons.
Closes#154.
russh 0.61.1 carries security fixes:
- GHSA-wwx6-x28x-8259 (compression "ZIP bomb" bypassing max-packet checks)
- GHSA-hpv4-5h6f-wqr3 (server-side username-change state-reset)
This reverses the v1.7.0 decision in #144 to hold at the 0.60.x patch line.
The 0.61 channel API churn is unchanged from then, but the security fixes
make absorbing it the right call now.
Closes#149, #150, #151, #152.
Themes were a Vec hardcoded in src/config.rs (builtin_presets()) -
every new preset required editing Rust, recompiling, and shipping a
new release, for what is purely presentation data. PR #148 from
@dav0l surfaced this nicely by failing to compile on a brace count
in the array.
This change adds a config::load_themes(&static_path) loader that
starts from the eight built-in presets (unchanged) and then merges
in any *.toml files from <static_path>/themes/. Disk themes can
add new entries or override a built-in by using the same name; the
existing builtin_presets() remains as the always-available fallback
when the themes directory is missing or empty.
File format: flat TOML table, one file per theme, filename (minus
extension) is the theme id. See static/themes/catppuccin-macchiato.toml
for a full example. Theme names are validated against the same
allowlist we use for Vault entry names ([a-zA-Z0-9_-]{1,64}) so they
are safe to render in the UI picker and in log lines, and can't be
used for path traversal or homoglyph mischief via crafted filenames.
dav0l's Catppuccin Macchiato palette lands here as
static/themes/catppuccin-macchiato.toml - their submission is the
first user-contributed theme under the new mechanism. Closes#148.
Backward compatibility: explicit. ThemeConfig::resolve() is now a
thin wrapper over resolve_with(builtins), so existing test callers
and any production callers see no behavioural change. Existing
[theme] sections in user config.toml files - preset only, preset +
overrides, overrides only, empty section, typo'd preset - all
resolve byte-equal to 1.7.0 (verified by the new
existing_user_config_with_theme_section_keeps_working_after_upgrade
test). Eight new tests in total cover the loader, the override
behaviour, the filename validation, and the upgrade scenario.
Docs broken out: themes get docs/themes.md (the full reference);
docs/configuration.md is trimmed to a brief stub and pointer.
No build-system changes needed - debian/rules, install.sh and the
Dockerfile all use recursive `cp -r static/` so the new themes
subdirectory is picked up automatically.
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.
Addresses #118. After a dynamic RDP resize, guac_rdp_gdi_desktop_resize()
resized the GDI buffer and display layer but never marked the layer dirty,
so close_raw() flushed nothing and newly-exposed/stale regions rendered as
solid black until something else repainted them.
The patch marks the whole layer dirty and issues a RefreshRect for the new
desktop area after the resize. Root cause diagnosed and fix supplied by
@Bails309 on the issue; reworked here without the downstream debug logging
and scoped to the resize function only (the end-paint handler shares the
same close_raw call and must not be touched).
Fixes the legacy bitmap path (rustguac's default, enable_gfx=false). The
RDPGFX surface cache ignores RefreshRect so GFX sessions aren't addressed,
but they have not reproduced the artifact in practice.
Verified: applies cleanly via git apply against the pinned guacd 2980cf0;
full guacd build with all five patches compiles and links the RDP plugin
with no errors (only pre-existing FreeRDP deprecation warnings).
Closes#147.
ab_list_subfolders checked access to the parent folder but then returned
every subfolder unfiltered, so a user who could open a parent saw all of
its children regardless of per-child group ACLs - clicking one they
weren't entitled to gave "no access". (Top-level folders were already
filtered; this was the subfolder gap.)
Subfolders are now filtered per child. A folder is shown if the user can
access it directly OR can access any descendant of it, so a deeper grant
(child with its own allowed_groups and inherit_from_parent=false under a
denied folder) is never orphaned out of the tree. Admins still see all.
New folder_or_descendant_accessible helper does the recursive (boxed
async) OR over resolve_folder_access, short-circuiting on the first
accessible folder. No Vault mock harness exists to unit-test the
Vault-backed path; verified via build + the logic being a thin recursive
wrapper over the already-shipping resolve_folder_access.
docs/roles-and-access-control.md gains explicit notes that inaccessible
folders are hidden (not shown-then-denied) at every level, the
descendant-visibility rule, and how inheritance interacts.
Closes dependabot #146, #143, #142, #145, #141, #140.
All patch/minor bumps within existing Cargo.toml constraints; no source
changes. russh held at the 0.60.x patch line (0.60.3); the 0.61.0 minor
(#144) was declined for now to avoid absorbing its new-API churn without
a reason to. fuzz/Cargo.lock russh bumped to match (#140).
229 tests pass, clippy and fmt clean.
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.
Contributed by @vk2amv (Lindsay). Adds two example VDI container images under contrib/: one with PulseAudio audio, one with PulseAudio + x264/GFX. Contrib-only, no impact on shipping code.
Thanks Lindsay.
Contributed by @vk2amv (Lindsay). Fixes VDI container name collisions when a user has multiple VDI entries that resolve to the same in-container username. Container names now include the sanitized final segment of the address book entry key: rustguac-vdi-{username}-{entry}. Same user + same entry still reuses one container (reconnect works); same user + different entries now run independently.
Thanks Lindsay.