diff --git a/backend/src/routes/guacamole.ts b/backend/src/routes/guacamole.ts index 5a03d0d..a6c4d08 100644 --- a/backend/src/routes/guacamole.ts +++ b/backend/src/routes/guacamole.ts @@ -93,6 +93,26 @@ export async function guacamoleRoutes(app: FastifyInstance) { }) const connectionId = randomUUID() + + // guacamole-lite 1.2.0 forwards EVERY client WS message straight to guacd, + // including guacamole-common-js's internal tunnel "ping" (opcode is the + // empty INTERNAL_DATA_OPCODE: `0.,4.ping,;`). guacd doesn't understand + // or echo that ping, so the client's tunnel never sees the expected echo, + // hits its receiveTimeout, closes with UPSTREAM_TIMEOUT, and reconnects in + // a loop (symptom: RDP desktop flickers in then drops while still showing + // "connected"). Fix: intercept the ping here and echo it back to the client + // ourselves, before ClientConnection's own handler forwards it to guacd. + // ponytail: simple substring match on the internal ping opcode rather than a + // full protocol parser — the ping frame is fixed-shape; revisit if guacd's + // internal opcodes change. + socket.on('message', (raw: unknown) => { + const msg = typeof raw === 'string' ? raw : String(raw) + // INTERNAL_DATA_OPCODE is empty, so a ping arrives as "0.,4.ping,..." + if (msg.startsWith('0.,4.ping,')) { + if ((socket as { readyState?: number }).readyState === 1) socket.send(msg) + } + }) + const clientConnection = new ClientConnection( CLIENT_OPTIONS, connectionId,