russh 0.63 changed the check_server_key callback to take
&PublicKeyOrCertificate; resolve it to a PublicKey via .public_key() in
both the tunnel and probe handlers, and widen the russh constraint to
0.63. Closes the retargeted dependabot PRs #204 (uuid) and #205 (russh).
The CI `cargo audit` step was failing on RUSTSEC-2026-0258 (h2 unbounded
empty DATA frames, a HTTP/2 DoS), pulled in transitively via reqwest/hyper.
Bump h2 0.4.15 -> 0.4.19 (patched) and chacha20 0.10.1 -> 0.10.2 (the prior
version was yanked).
Adds a per-entry RDP keyboard layout, wiring guacd's `server-layout`
parameter (previously hardcoded to empty) through the address-book entry,
the connect request, and the guacd handshake, with a dropdown in the
connections editor and a migration mapping from Guacamole's `server-layout`
on import. Empty keeps guacd's default (en-us-qwerty).
list_credential_variables walked from top-level folders using
check_folder_access + continue, short-circuiting the whole subtree when an
ancestor was not directly accessible. An operator granted access at a
subfolder level saw "No credential variables" in My Credentials even though
the connections tree showed the entry (the tree is descendant-aware). It now
descends past inaccessible ancestors, collecting variables only from folders
the user can directly access. Adds regression tests.
CI's runners updated to clippy 0.1.98, which enforces two lints that
existing code tripped under -D warnings. Rewrite the two late-init
if/else blocks in the SSH tunnel builder as tuple `let` bindings
(needless-late-init), and allow result-large-err on check_folder_access,
whose Err is an axum Response by design.
Maps Apache Guacamole passthrough tokens (${GUAC_USERNAME} etc.) to rustguac
credential variables during import, so imported connections arrive wired to
My Credentials. Repeatable FROM=TO; applied to the username, password,
domain, and private_key fields. Adds tests and docs.
Disk-based rotation deleted the oldest .guac unconditionally until statvfs
usage fell below max_disk_percent. Recordings for live sessions are held
open by the recording tee, so unlinking one frees no space and statvfs never
drops: the loop then deleted every recording in the directory in a single
pass while the disk stayed full (a production host lost all recordings this
way).
rotate() now excludes live-session recordings (SessionManager::
active_recording_paths) and stops if a deletion frees no space. Adds
regression tests.
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.