diff --git a/docs/api.md b/docs/api.md index bf9b976..82ccd49 100644 --- a/docs/api.md +++ b/docs/api.md @@ -219,6 +219,7 @@ The `jump_hosts` array defines an ordered chain of SSH bastion hops. Each hop co | `kerberos_cache` | string | RDP | Path to Kerberos credential cache (advanced) | | `color_depth` | integer | RDP | Color depth in bits (8, 16, 24, 32) | | `vnc_resize` | boolean | VNC | Ask the VNC server to resize its screen to the browser window (default `false`: the screen is scaled to fit). Only for servers with their own virtual screen (TigerVNC, Xvnc); a physical console such as UltraVNC, RealVNC or x11vnc can't resize, and the session stalls if asked. | +| `vnc_network` | string | VNC | Network preset for the link between the VNC server and rustguac: `auto` (default), `lan`, `wan` or `slow`. See [VNC network presets](overview.md#vnc-network-presets). | | `enable_drive` | boolean | RDP, SSH | Enable file transfer / drive redirection | | `disable_copy` | boolean | All | Disable clipboard copy (server → client) | | `disable_paste` | boolean | All | Disable clipboard paste (client → server) | @@ -649,6 +650,7 @@ Create a connection entry. The body includes a `name` field plus all entry field | `kdc_url` | string | RDP | Kerberos KDC URL | | `color_depth` | integer | RDP | Color depth | | `vnc_resize` | boolean | VNC | Resize the server's screen to the browser window (default `false`). See [`POST /api/sessions`](#post-apisessions). | +| `vnc_network` | string | VNC | Network preset: `auto` (default), `lan`, `wan` or `slow`. | | `enable_drive` | boolean | RDP, SSH | Enable file transfer | | `disable_copy` | boolean | All | Disable clipboard copy (server → client) | | `disable_paste` | boolean | All | Disable clipboard paste (client → server) | diff --git a/docs/overview.md b/docs/overview.md index 86e2712..8b8eca5 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -103,6 +103,23 @@ Connects guacd to a target VNC server. Supports password-based authentication, p Supports optional [multi-hop SSH tunnel chains](#ssh-tunnel--jump-hosts) to reach VNC targets through bastion hosts. +**Screen size.** By default the remote screen keeps its own size and is scaled to fit the browser window. Tick **Resize the remote desktop to fit the window** only for servers with their own virtual screen (TigerVNC, Xvnc). A physical console (UltraVNC, RealVNC, x11vnc) can't resize, and asking it to stalls the session. + +#### VNC network presets + +**Network** on a VNC connection chooses how the server encodes the screen for rustguac, like UltraVNC Viewer's quick options: + +| Preset | Encodings (in order) | Compression | JPEG quality | Colour | +|---|---|---|---|---| +| Auto (default) | negotiated | default | default | as set | +| LAN | Ultra, ZRLE, Hextile, CopyRect, Raw | 1 | 9 | as set | +| WAN | Tight, ZRLE, CopyRect | 6 | 6 | as set | +| Slow link | Tight, CopyRect | 9 | 3 | 16-bit unless set | + +This is the link between the VNC server and rustguac. The picture rustguac sends on to the browser is compressed separately, so a preset helps most when the VNC server is far from rustguac. + +**Authentication.** These work: VNC password, VeNCrypt (including username and password over TLS, e.g. TigerVNC with PAM), macOS Screen Sharing, and UltraVNC's Windows login (MS-Logon II, "New MS-Logon"), which allows Active Directory accounts. These don't: RealVNC's or UltraVNC's RSA-AES encryption (security types 5, 6, 129, 130), UltraVNC's encryption plugins, and MS-Logon III. A server offering only those fails with "Unknown authentication scheme". MS-Logon II protects the password weakly and doesn't encrypt the session, so use it on a trusted network or through a jump host. + ### SPICE and Proxmox VE SPICE sessions connect guacd's SPICE plugin to a SPICE server, such as a libvirt/QEMU VM console, with optional TLS, CA verification, certificate-subject pinning and a SPICE proxy. Proxmox VE console sessions open a Proxmox VE VM's console: at connect time rustguac fetches a one-time SPICE ticket from the Proxmox API, so only the API token is stored. diff --git a/src/api.rs b/src/api.rs index c57c337..9b1fb4c 100644 --- a/src/api.rs +++ b/src/api.rs @@ -4015,6 +4015,7 @@ pub fn session_request_from_entry( autofill: entry.autofill, allowed_domains: entry.allowed_domains, vnc_resize: entry.vnc_resize, + vnc_network: entry.vnc_network, disable_copy: entry.disable_copy, disable_paste: entry.disable_paste, enable_gfx: entry.enable_gfx, @@ -7380,6 +7381,7 @@ pub async fn quick_connect( autofill: None, allowed_domains: None, vnc_resize: None, + vnc_network: None, disable_copy: None, disable_paste: None, enable_gfx: None, @@ -7655,6 +7657,7 @@ mod tests { autofill: ab_entry.autofill, allowed_domains: ab_entry.allowed_domains, vnc_resize: ab_entry.vnc_resize, + vnc_network: ab_entry.vnc_network.clone(), disable_copy: ab_entry.disable_copy, disable_paste: ab_entry.disable_paste, enable_gfx: ab_entry.enable_gfx, @@ -7750,6 +7753,7 @@ mod tests { autofill: ab_entry.autofill, allowed_domains: ab_entry.allowed_domains, vnc_resize: ab_entry.vnc_resize, + vnc_network: ab_entry.vnc_network.clone(), disable_copy: ab_entry.disable_copy, disable_paste: ab_entry.disable_paste, enable_gfx: ab_entry.enable_gfx, diff --git a/src/guacd.rs b/src/guacd.rs index 9763bbb..89bf7ae 100644 --- a/src/guacd.rs +++ b/src/guacd.rs @@ -113,10 +113,36 @@ pub struct VncParams { /// requesting updates while it waits for a resize that never comes, so /// the session all but freezes. Off, guacd scales the screen instead. pub display_resize: bool, + /// How the VNC server should encode the screen for guacd: encodings in + /// order of preference, and Tight's zlib compression and JPEG quality + /// (0-9). `None` leaves libvncclient's defaults. + pub encodings: Option, + pub compress_level: Option, + pub quality_level: Option, /// Wake-on-LAN parameters. pub wol: WolParams, } +/// A VNC "network" preset, like UltraVNC Viewer's quick options: how to +/// trade picture quality for bandwidth between the VNC server and guacd. +/// Returns (encodings, compress level, quality level, colour depth to use +/// when the connection doesn't set one). `None` for `auto` or an unknown +/// name, which leave every setting at its default. +pub fn vnc_network_preset(preset: &str) -> Option<(&'static str, u8, u8, Option)> { + match preset { + // Fast, lossless-looking: UltraVNC's own LZO encoding first. + "lan" => Some(("ultra zrle hextile copyrect raw", 1, 9, None)), + // Tight with JPEG for photos and video, moderate compression. + "wan" => Some(("tight zrle copyrect", 6, 6, None)), + // As little bandwidth as possible. + "slow" => Some(("tight copyrect", 9, 3, Some(16))), + _ => None, + } +} + +/// The preset names `vnc_network_preset` knows, `auto` included. +pub const VNC_NETWORK_PRESETS: &[&str] = &["auto", "lan", "wan", "slow"]; + /// SPICE connection parameters to pass to guacd. /// /// Credentials (`password`/`username`) are sent as connect args: guacd's SPICE @@ -185,6 +211,9 @@ fn vnc_arg_value(name: &str, p: &VncParams) -> String { "disable-copy" => if p.disable_copy { "true" } else { "false" }.into(), "disable-paste" => if p.disable_paste { "true" } else { "false" }.into(), "disable-display-resize" => if p.display_resize { "false" } else { "true" }.into(), + "encodings" => p.encodings.clone().unwrap_or_default(), + "compress-level" => p.compress_level.map(|v| v.to_string()).unwrap_or_default(), + "quality-level" => p.quality_level.map(|v| v.to_string()).unwrap_or_default(), _ => wol_arg_value(name, &p.wol).unwrap_or_else(|| { tracing::debug!("Unknown guacd VNC parameter '{}', sending empty", name); String::new() @@ -759,10 +788,39 @@ mod tests { disable_copy: false, disable_paste: false, display_resize, + encodings: None, + compress_level: None, + quality_level: None, wol: WolParams::default(), } } + #[test] + fn vnc_network_presets_set_encodings_compression_and_quality() { + // Auto (no preset): every one empty, so guacd keeps libvncclient's + // own defaults. + let p = vnc(false); + for arg in ["encodings", "compress-level", "quality-level"] { + assert_eq!(vnc_arg_value(arg, &p), "", "{arg}"); + } + let (enc, compress, quality, depth) = vnc_network_preset("slow").unwrap(); + let mut p = vnc(false); + p.encodings = Some(enc.into()); + p.compress_level = Some(compress); + p.quality_level = Some(quality); + assert_eq!(vnc_arg_value("encodings", &p), "tight copyrect"); + assert_eq!(vnc_arg_value("compress-level", &p), "9"); + assert_eq!(vnc_arg_value("quality-level", &p), "3"); + assert_eq!(depth, Some(16)); + // Every listed preset other than auto resolves; anything else doesn't. + for name in VNC_NETWORK_PRESETS.iter().filter(|n| **n != "auto") { + let (enc, c, q, _) = vnc_network_preset(name).unwrap(); + assert!(!enc.is_empty() && c <= 9 && q <= 9, "{name}"); + } + assert!(vnc_network_preset("auto").is_none()); + assert!(vnc_network_preset("modem").is_none()); + } + #[test] fn vnc_display_resize_is_off_unless_asked() { // A physical console can't resize, and libvncclient then stops asking diff --git a/src/import.rs b/src/import.rs index 7c200fe..e30bd66 100644 --- a/src/import.rs +++ b/src/import.rs @@ -156,6 +156,7 @@ pub async fn cmd_import_guacamole( autofill: None, allowed_domains: None, vnc_resize: None, + vnc_network: None, disable_copy: None, disable_paste: None, banner: None, diff --git a/src/session.rs b/src/session.rs index 873d086..7f38b20 100644 --- a/src/session.rs +++ b/src/session.rs @@ -109,6 +109,11 @@ pub struct CreateSessionRequest { /// can, and one that can't leaves the session frozen. #[serde(default)] pub vnc_resize: Option, + /// VNC: a network preset (`auto`, `lan`, `wan`, `slow`) choosing the + /// encodings, compression and JPEG quality between the server and guacd, + /// like UltraVNC Viewer's quick options. Unset or `auto`: the defaults. + #[serde(default)] + pub vnc_network: Option, pub disable_copy: Option, /// Disable clipboard paste (client → server). pub disable_paste: Option, @@ -1348,12 +1353,13 @@ impl SessionManager { "Creating new VNC session" ); + let preset = vnc_network(req.vnc_network.as_deref())?; let params = guacd::ConnectionParams::Vnc(guacd::VncParams { hostname: connect_host, port, username: req.username.clone().filter(|u| !u.is_empty()), password: req.password.clone(), - color_depth: req.color_depth, + color_depth: req.color_depth.or(preset.and_then(|p| p.3)), width, height, dpi, @@ -1361,6 +1367,9 @@ impl SessionManager { disable_paste: req.disable_paste.unwrap_or(false), // Off unless the entry says the server can resize. display_resize: req.vnc_resize.unwrap_or(false), + encodings: preset.map(|p| p.0.to_string()), + compress_level: preset.map(|p| p.1), + quality_level: preset.map(|p| p.2), wol: wol.clone(), }); ( @@ -1673,6 +1682,9 @@ impl SessionManager { disable_paste: req.disable_paste.unwrap_or(false), // Our own Xvnc can always be resized to the browser. display_resize: true, + encodings: None, + compress_level: None, + quality_level: None, // Local Xvnc — WoL not applicable. wol: guacd::WolParams::default(), }); @@ -2825,6 +2837,21 @@ fn drive_cleanup_settings(drive: &Option) -> (bool, u64) { } } +/// The settings for a VNC network preset, or `None` for `auto` / unset. An +/// unknown name is refused rather than silently treated as `auto`. +type VncPreset = (&'static str, u8, u8, Option); +fn vnc_network(preset: Option<&str>) -> Result, SessionError> { + match preset.map(str::trim).filter(|p| !p.is_empty()) { + None | Some("auto") => Ok(None), + Some(p) => guacd::vnc_network_preset(p).map(Some).ok_or_else(|| { + SessionError::ValidationError(format!( + "vnc_network must be one of {}", + guacd::VNC_NETWORK_PRESETS.join(", ") + )) + }), + } +} + /// A SPICE CA certificate on its way to guacd. guacd's SPICE plugin hands a /// PEM to spice-gtk to verify the server against, but if the PEM holds no /// certificate spice-gtk quietly falls back to the system trust store, so a @@ -3174,6 +3201,21 @@ mod tests { assert_eq!(req.vnc_resize, Some(true)); } + #[test] + fn vnc_network_preset_names_are_checked() { + assert!(vnc_network(None).unwrap().is_none()); + assert!(vnc_network(Some("")).unwrap().is_none()); + assert!(vnc_network(Some("auto")).unwrap().is_none()); + assert_eq!( + vnc_network(Some("lan")).unwrap().unwrap().0, + "ultra zrle hextile copyrect raw" + ); + assert!( + vnc_network(Some("modem")).is_err(), + "an unknown preset is refused, not ignored" + ); + } + #[test] fn vdi_username_must_be_a_plain_name() { // It becomes a directory under home_base that is bind-mounted into the diff --git a/src/vault.rs b/src/vault.rs index 0e779b6..4a9616e 100644 --- a/src/vault.rs +++ b/src/vault.rs @@ -354,6 +354,9 @@ pub struct AddressBookEntry { /// only servers with a virtual screen (Xvnc, TigerVNC) can resize. #[serde(default, skip_serializing_if = "Option::is_none")] pub vnc_resize: Option, + /// VNC network preset: `auto`, `lan`, `wan` or `slow`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub vnc_network: Option, /// Disable clipboard copy (server → client). Prevents copying from the remote session. #[serde(default, skip_serializing_if = "Option::is_none")] pub disable_copy: Option, @@ -598,6 +601,9 @@ pub struct EntryInfo { /// VNC: resize the server's screen to the browser window. #[serde(skip_serializing_if = "Option::is_none")] pub vnc_resize: Option, + /// VNC network preset. + #[serde(skip_serializing_if = "Option::is_none")] + pub vnc_network: Option, /// Disable clipboard copy (server → client). #[serde(skip_serializing_if = "Option::is_none")] pub disable_copy: Option, @@ -780,6 +786,7 @@ impl From<(&str, &AddressBookEntry)> for EntryInfo { autofill: e.autofill.clone(), allowed_domains: e.allowed_domains.clone(), vnc_resize: e.vnc_resize, + vnc_network: e.vnc_network.clone(), disable_copy: e.disable_copy, disable_paste: e.disable_paste, banner: e.banner.clone(), diff --git a/static/connections.html b/static/connections.html index 5fc7845..0e5bdd6 100644 --- a/static/connections.html +++ b/static/connections.html @@ -967,6 +967,15 @@ Resize the remote desktop to fit the window
Only for VNC servers with their own virtual screen, such as TigerVNC or Xvnc. Leave off for a physical console (UltraVNC, RealVNC, x11vnc): they can't resize, and the session stalls if asked to. When off, the screen is scaled to fit instead.
+ +
How the VNC server encodes the screen for rustguac, like UltraVNC Viewer's quick options. Auto lets the server and client agree; pick a preset only if a link is slow or the picture is poor. A colour depth set above takes precedence.