From 1bf225ae27be35893208add9f721d46b8aad5297 Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Sat, 14 Mar 2026 06:32:36 -0400 Subject: [PATCH] fix(rdp): send client capability instructions before CONNECT handshake MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ROOT CAUSE: guacd showed "User resolution is 0x0 at 0 DPI" and immediately killed every RDP connection. The Guacamole protocol requires five client capability instructions (size, audio, video, image, timezone) BETWEEN receiving 'args' and sending 'connect'. Our handshake skipped all five and jumped straight to CONNECT. guacd never received the display dimensions, defaulted to 0x0, and terminated the connection. Now sends the complete handshake: select → (receive args) → size → audio → video → image → timezone → connect Co-Authored-By: Claude Opus 4.6 --- backend/src/rdp/guacamole.service.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/backend/src/rdp/guacamole.service.ts b/backend/src/rdp/guacamole.service.ts index 5013997..0fe1769 100644 --- a/backend/src/rdp/guacamole.service.ts +++ b/backend/src/rdp/guacamole.service.ts @@ -64,11 +64,31 @@ export class GuacamoleService { } this.logger.log(`guacd expects ${argNames.length} args: ${argNames.join(', ')}`); - // Phase 2: CONNECT — send values in the exact order guacd expects + // Phase 2: Client capability instructions — MUST be sent before CONNECT. + // The Guacamole protocol requires: size, audio, video, image, timezone + // between receiving 'args' and sending 'connect'. Without these, guacd + // sees 0x0 resolution and immediately kills the connection. + const width = String(params.width); + const height = String(params.height); + const dpi = String(params.dpi || 96); + socket.write(this.encode('size', width, height, dpi)); + socket.write(this.encode('audio')); + socket.write(this.encode('video')); + socket.write(this.encode('image', 'image/png', 'image/jpeg', 'image/webp')); + + let tz: string; + try { + tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + } catch { + tz = 'UTC'; + } + socket.write(this.encode('timezone', tz)); + + // Phase 3: CONNECT — send values in the exact order guacd expects const connectInstruction = this.buildConnectInstruction(params, argNames); this.logger.log( `Sending CONNECT: host=${params.hostname}:${params.port} user=${params.username} domain=${params.domain || '(none)'} ` + - `security=${params.security || 'any'} size=${params.width}x${params.height} ignoreCert=${params.ignoreCert !== false}`, + `security=${params.security || 'any'} size=${width}x${height}@${dpi}dpi ignoreCert=${params.ignoreCert !== false}`, ); socket.write(connectInstruction);