mirror of
https://github.com/sol1/rustguac.git
synced 2026-10-25 13:09:56 +00:00
vnc: network presets, like UltraVNC Viewer's quick options
A VNC connection can choose how the server encodes its screen for rustguac: Auto (default, unchanged), LAN (Ultra/ZRLE/Hextile first, low compression, top JPEG quality), WAN (Tight/ZRLE, compression and JPEG quality 6) or Slow link (Tight, compression 9, JPEG quality 3, 16-bit colour unless the connection sets a colour depth). They set guacd's encodings, compress-level and quality-level; Auto sends none of them. An unknown preset is refused rather than ignored. Web sessions are unchanged. Docs: the VNC section of the overview now covers screen size, the presets, and which VNC authentication methods work (VNC password, VeNCrypt, macOS, UltraVNC MS-Logon II) and which don't (RSA-AES, UltraVNC encryption plugins, MS-Logon III).
This commit is contained in:
@@ -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) |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<String>,
|
||||
pub compress_level: Option<u8>,
|
||||
pub quality_level: Option<u8>,
|
||||
/// 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<u8>)> {
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
|
||||
+43
-1
@@ -109,6 +109,11 @@ pub struct CreateSessionRequest {
|
||||
/// can, and one that can't leaves the session frozen.
|
||||
#[serde(default)]
|
||||
pub vnc_resize: Option<bool>,
|
||||
/// 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<String>,
|
||||
pub disable_copy: Option<bool>,
|
||||
/// Disable clipboard paste (client → server).
|
||||
pub disable_paste: Option<bool>,
|
||||
@@ -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<DriveConfig>) -> (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<u8>);
|
||||
fn vnc_network(preset: Option<&str>) -> Result<Option<VncPreset>, 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
|
||||
|
||||
@@ -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<bool>,
|
||||
/// VNC network preset: `auto`, `lan`, `wan` or `slow`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub vnc_network: Option<String>,
|
||||
/// Disable clipboard copy (server → client). Prevents copying from the remote session.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub disable_copy: Option<bool>,
|
||||
@@ -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<bool>,
|
||||
/// VNC network preset.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub vnc_network: Option<String>,
|
||||
/// Disable clipboard copy (server → client).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub disable_copy: Option<bool>,
|
||||
@@ -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(),
|
||||
|
||||
@@ -967,6 +967,15 @@
|
||||
Resize the remote desktop to fit the window
|
||||
</label>
|
||||
<div class="field-hint">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.</div>
|
||||
<label style="margin-top:0.6em">Network
|
||||
<select id="em-vnc-network">
|
||||
<option value="">Auto (default)</option>
|
||||
<option value="lan">LAN: fast, best picture</option>
|
||||
<option value="wan">WAN: compressed, JPEG for photos and video</option>
|
||||
<option value="slow">Slow link: least bandwidth, 16-bit colour</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="field-hint">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.</div>
|
||||
<div class="entry-only">
|
||||
<label style="margin-top:0.8em">
|
||||
<input type="checkbox" id="em-vnc-prompt-creds" style="display:inline;width:auto;margin-right:0.4em"> Prompt for credentials at connect time
|
||||
@@ -4414,6 +4423,7 @@
|
||||
document.getElementById('em-vnc-password').value = '';
|
||||
document.getElementById('em-vnc-color-depth').value = '';
|
||||
document.getElementById('em-vnc-resize').checked = false;
|
||||
document.getElementById('em-vnc-network').value = '';
|
||||
document.getElementById('em-vnc-prompt-creds').checked = false;
|
||||
document.getElementById('em-spice-hostname').value = '';
|
||||
document.getElementById('em-spice-port').value = '5900';
|
||||
@@ -4576,6 +4586,7 @@
|
||||
document.getElementById('em-vnc-username').value = entryData.username || '';
|
||||
document.getElementById('em-vnc-color-depth').value = entryData.color_depth ? String(entryData.color_depth) : '';
|
||||
document.getElementById('em-vnc-resize').checked = !!entryData.vnc_resize;
|
||||
document.getElementById('em-vnc-network').value = entryData.vnc_network || '';
|
||||
document.getElementById('em-vnc-prompt-creds').checked = !!entryData.prompt_credentials;
|
||||
} else if (type === 'spice') {
|
||||
document.getElementById('em-spice-hostname').value = entryData.hostname || '';
|
||||
@@ -4802,6 +4813,8 @@
|
||||
var cd = document.getElementById('em-vnc-color-depth').value;
|
||||
if (cd) entry.color_depth = parseInt(cd);
|
||||
if (document.getElementById('em-vnc-resize').checked) entry.vnc_resize = true;
|
||||
var net = document.getElementById('em-vnc-network').value;
|
||||
if (net) entry.vnc_network = net;
|
||||
if (document.getElementById('em-vnc-prompt-creds').checked) entry.prompt_credentials = true;
|
||||
} else if (type === 'spice') {
|
||||
var h = document.getElementById('em-spice-hostname').value.trim();
|
||||
|
||||
@@ -269,3 +269,23 @@ test('VNC asks the server to resize only when the box is ticked', async ({ page
|
||||
else expect(sent.vnc_resize).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
// The VNC network preset is sent only when one is picked.
|
||||
test('a VNC network preset is sent when chosen, and nothing for Auto', async ({ page }) => {
|
||||
const api = await installApi(page, { role: 'poweruser' });
|
||||
await page.goto('/connections.html');
|
||||
await settle(page);
|
||||
for (const preset of ['', 'slow']) {
|
||||
await openEditor(page);
|
||||
await page.locator('#em-type').selectOption('vnc');
|
||||
await expect(page.locator('#em-vnc-network')).toHaveValue('');
|
||||
if (preset) await page.locator('#em-vnc-network').selectOption(preset);
|
||||
await page.locator('#em-vnc-hostname').fill('192.0.2.21');
|
||||
const before = adhocCalls(api).length;
|
||||
await Promise.all([page.waitForEvent('popup'), page.locator('#em-connect').click()]);
|
||||
await expect.poll(() => adhocCalls(api).length).toBe(before + 1);
|
||||
const sent = adhocCalls(api)[before].body.entry;
|
||||
if (preset) expect(sent.vnc_network).toBe(preset);
|
||||
else expect(sent.vnc_network).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user