A busy main thread does not delay pointer movement, it discards it: moves arriving while a handler runs are merged into one, and what is dispatched afterwards carries only the last position. A drag feels that immediately -- the path between two points is gone, so a dragged scrollbar thumb is dropped rather than merely lagged -- while a hover or a video degrades gracefully under the same load. getCoalescedEvents() returns what was merged. Replaying it in order restores the path, though not the timing: nothing running on a blocked thread can do that, and the positions still arrive together once the thread is free. What changes is that the remote end sees where the pointer went rather than only where it ended up. Deliberately additive. The mousemove listener is untouched and still delivers every position it did before, so touch, the compatibility mouse events it produces and the counter that swallows them all behave as they did; the dispatched position arrives twice and the second is dropped by the unchanged-coordinates check already in move(). Migrating this file to pointer events outright is the tidier shape and a far larger change -- every listener would have to move together, or synthetic events from touch would arrive with nothing left counting them down. Only while a button is held, because the path matters for a drag and the endpoint is all that matters for a hover, and replaying every merged move of an idle pointer would multiply a session's upstream mouse traffic for movement nobody can perceive. Capped and evenly sampled, because a pointer reporting at 1000Hz behind a thread blocked for 100ms offers a hundred positions for one drag, and answering a rendering problem with a traffic one is no answer. An ordinary pointer never reaches the cap. The pop-out monitor windows get the same replay. They bypass Guacamole.Mouse entirely -- it does not track the X axis correctly in a popup -- and map the pointer themselves, so a drag on a second head would otherwise lose its path exactly as the first one did. The sampler moves to a static so there is one definition of the rule rather than two that can drift, and that path needs one thing the main display does not: it sends unconditionally, where move() drops an unchanged position, so it keeps what it last sent and drops a repeat -- keyed on the buttons as well as the position, since a press and a release happen at one place and are not repeats of each other. Feature-detected on the event rather than on PointerEvent, because a pop-out window is a realm of its own and the constructor this page can see is not the one that made the event it is handed. That pointer handling lives in Guacamole.MonitorPointer rather than inline in client.html, because these windows cannot be opened without a live multi-monitor session: while it sat in the page the only thing a test could do was match the source text and hope, and the two things most worth checking there are a coordinate mapping and a suppression rule -- exactly the kind that look right in a diff and are off by a term. It takes the canvas, a function returning this monitor's rectangle in the combined framebuffer (a call rather than a captured value, since the layout changes under a running window) and a function to send a state. Writing those tests found a real defect in the wheel, which sends a momentary press and release: it built one state object, sent it, mutated it and sent it again, which works only while every consumer copies synchronously. It sends two states now. Tested rather than tried, because this path only runs once the browser has already thrown something away: the case it exists for is the one that cannot be reliably produced, and a broken version feels like the behaviour it replaced. Twenty tests cover the mapping, the button mask, the repeat suppression, the wheel's reset of it and the replay, plus a harness driving real pointer input through the debugger with the main thread deliberately blocked -- because the unit tests feed events shaped the way the code expects, and that assumption is the one worth checking against a browser. (cherry picked from commit d8ce743fe0801bd8c776281ee5541abf8040a339)
rustguac
A lightweight Rust replacement for the Apache Guacamole Java webapp. Browser-based SSH, RDP, VNC, SPICE, Proxmox VE consoles, web browsing, and VDI desktop containers through guacd.
No Java. No Tomcat. Single binary + guacd.
Architecture
Browser (HTML/JS)
|
| WebSocket over HTTPS
v
rustguac (Rust, axum)
|
| TLS (Guacamole protocol)
v
guacd (C, from guacamole-server)
|
+---> SSH server
+---> RDP server
+---> VNC server
+---> SPICE server (libvirt/QEMU displays)
+---> Proxmox VE VM console (SPICE via the PVE spiceproxy API)
+---> Xvnc + Chromium (web browser sessions)
+---> Docker container + xrdp (VDI desktop sessions)
Features
Session types
| Type | Description |
|---|---|
| SSH | Browser-based terminal with password, private key, or ephemeral keypair auth. SFTP file transfer. |
| RDP | Windows/Linux RDP with auto-fit resize, Kerberos NLA, RemoteApp/RAIL, H.264 passthrough, GFX pipeline. |
| VNC | Connect to any VNC server (KVM/IPMI consoles, remote desktops, VM displays). |
| SPICE | Direct SPICE displays (libvirt/QEMU consoles) with TLS, CA verification, certificate-subject pinning, and SPICE-proxy support. |
| Proxmox VE | VM consoles brokered through the Proxmox API. One-time SPICE tickets fetched just-in-time at connect (only the API token is stored), node auto-detected from the VM ID, and SSH-tunnel aware. |
| Web | Headless Chromium on Xvnc with native autofill, domain allowlisting, login script automation. |
| VDI | Ephemeral Docker desktop containers per user. Persist after disconnect, auto-cleanup on idle. |
Security & authentication
- OIDC single sign-on: Authentik, Google, Okta, Keycloak, or any OpenID Connect provider
- 4-tier role system: admin, poweruser, operator, viewer with OIDC group mapping
- API key auth: SHA-256 hashed keys with IP allowlists and expiry
- Vault-backed connections: credentials in HashiCorp Vault or OpenBao KV v2, never reach the browser (see Requirements)
- TLS everywhere: HTTPS for clients, TLS between rustguac and guacd
- CIDR allowlists: per-protocol network restrictions for session targets
- Per-entry clipboard control: disable copy and/or paste for data loss prevention
- Rate limiting: per-IP, per-endpoint via tower_governor
- Session recording: Guacamole format with playback UI, disk rotation, per-entry limits
Connectivity
- Multi-hop SSH tunnels: chain jump hosts/bastions to reach isolated networks (all session types, including the Proxmox API and console hops)
- Session sharing: share links for view-only or collaborative access, plus admin shadowing
- Headless API integration: create a session over the REST API and hand a browser a ready-to-open URL via a single-use WebSocket ticket, with no OIDC login and no API key in the browser (see Connecting to a session)
- Encrypted file transfer: LUKS-encrypted per-session drive storage (RDP), SFTP (SSH)
- Credential variables: shared credentials across connections entries
VDI desktop containers
- Docker-based: one container per user, deterministic naming, BYO image
- Persist after disconnect: reconnect to the same desktop within idle timeout
- Logout detection: desktop logout stops the container, tab close preserves it
- Session thumbnails: live preview in the connections, click to reconnect
- Persistent home directories: bind-mounted user data survives container restarts
- Per-entry resource limits: CPU, memory, idle timeout per connections entry
- VdiDriver trait: extensible for downstream forks (Nomad, Proxmox, cloud)
UI
- Connections with folder-based organisation and OIDC group access control
- Active Sessions section with live thumbnail previews
- Session ended overlay with Reconnect/Close buttons
- Clipboard panel controls (Home + Fullscreen)
- 8 built-in themes with CSS gradient backgrounds, or configure your own
- Reports page with session analytics, history, and CSV export
Requirements
| Component | Status | Notes |
|---|---|---|
| guacd | Bundled | Built from apache/guacamole-server, ships in the .deb and Docker image. No separate install. |
| Vault or OpenBao | Required for the Connections UI | Stores connection entries and credentials server-side. Without it the Connections page is unavailable and users can only run ad-hoc sessions via the API. Use contrib/vault-quickstart.sh for one-command setup (auto-detects vault or bao, supports --dev and --local modes). |
| OIDC provider | Optional | For SSO. API-key auth works on its own. Authentik/Google/Okta/Keycloak/JumpCloud all tested. |
| Docker | Optional | Only needed for VDI desktop containers. |
Supported browsers
The client runs in any modern browser (Chrome, Firefox, Edge, Safari, Chromium, Brave). One caveat applies only to H.264-accelerated RDP, which is opt-in per connection (the per-entry H.264 toggle, off by default):
- Standard connections (SSH, RDP, VNC, web sessions, VDI) work in every modern browser.
- H.264-accelerated connections need a browser that can decode H.264 through the WebCodecs API. Chrome and Firefox work. Open-source Chromium and Brave builds without the bundled H.264 codec, and older Safari, render a blank display on those connections. Leave H.264 off (the default) for universal browser support, or use Chrome or Firefox where it is enabled.
Quick start
Debian 13 (.deb)
Pre-built packages for amd64 and arm64 are available from Releases:
sudo apt install ./rustguac_*.deb
/opt/rustguac/bin/rustguac --config /opt/rustguac/config.toml add-admin --name admin
sudo systemctl enable --now rustguac
Docker
docker pull sol1/rustguac:latest
docker run -d -p 8089:8089 sol1/rustguac:latest
For VDI support, mount the Docker socket:
docker run -d -p 8089:8089 \
-v /var/run/docker.sock:/var/run/docker.sock \
--group-add $(getent group docker | cut -d: -f3) \
sol1/rustguac:latest
Other distributions
Pre-built packages are provided for Debian 13. For other distributions, build from source:
sudo ./install.sh
See the Installation guide for full details including Docker Compose, TLS setup, and development builds.
VDI setup
VDI requires Docker on the host:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker rustguac
sudo systemctl restart rustguac
Add [vdi] to your config and create a VDI entry in the connections. See VDI Desktop Containers for image requirements and configuration.
Documentation
Getting started
- Installation: Debian packages, Docker, bare-metal, development builds
- Configuration: TOML config reference with all sections
- Deployment Guide: step-by-step production setup
Features
- Roles & Access Control: OIDC, roles, group mappings, API tokens
- Web Browser Sessions: autofill, domain allowlisting, login scripts
- VDI Desktop Containers: Docker desktops, image requirements, persistent homes
- RDP Video Performance: H.264 passthrough, GFX pipeline, xrdp tuning
- RDP H.264 Passthrough: enabling it, and the Windows host settings it requires
- Credential Variables: shared credentials across entries
- Reports: session analytics, history, CSV export
Integration & reference
- Integrations: Vault, LUKS drives, SSH tunnels, Kerberos, HAProxy, Knocknoc
- NetBox: connections sync via custom fields and webhooks
- Security: TLS, rate limiting, headers, audit logging, hardening
- API Reference: REST API endpoints, the session connection flow, and headless ws-ticket integration
- Migration from Apache Guacamole: MySQL/MariaDB to Vault
Commercial support
Commercial support for rustguac is available from Sol1.
License
Apache License 2.0. See LICENSE for details.