RDP diagnostics: TCP pre-check, disable-gfx, better logging

- Add TCP connectivity pre-test before guacd handshake — gives a clear
  error message if the RDP host is unreachable from the Docker network
- Add disable-gfx:true (disables GFX Pipeline Extension, known FreeRDP
  2.x crash source on Windows 10/11) and cert-tofu:true
- Log tcpBuf flush content and all guacd data at debug level so we can
  see exactly what guacd sends after the ready instruction

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
deadRabbit
2026-02-22 14:24:28 +01:00
parent 42c63a5f2d
commit 459674d2fd

View File

@@ -128,7 +128,29 @@ export async function rdpWebsocket(fastify: FastifyInstance) {
return; return;
} }
fastify.log.info({ host: conn.host, port: conn.port, user: conn.username }, 'RDP: starting guacd handshake'); // Pre-check: verify the RDP server is TCP-reachable from this container.
// The backend is on the same Docker network as guacd, so same reachability.
try {
await new Promise<void>((resolve, reject) => {
const test = createConnection(conn.port, conn.host);
const timer = setTimeout(() => {
test.destroy();
reject(new Error(`Cannot reach ${conn.host}:${conn.port} — connection timed out`));
}, 5000);
test.once('connect', () => { clearTimeout(timer); test.destroy(); resolve(); });
test.once('error', (err) => {
clearTimeout(timer);
reject(new Error(`Cannot reach ${conn.host}:${conn.port}${err.message}`));
});
});
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : 'Cannot reach RDP host';
fastify.log.warn({ host: conn.host, port: conn.port }, msg);
socket.close(1011, msg);
return;
}
fastify.log.info({ host: conn.host, port: conn.port, user: conn.username }, 'RDP: TCP reachable, starting guacd handshake');
try { try {
// 1. Select protocol // 1. Select protocol
@@ -158,6 +180,8 @@ export async function rdpWebsocket(fastify: FastifyInstance) {
'create-drive-path': 'false', 'create-drive-path': 'false',
'enable-printing': 'false', 'enable-printing': 'false',
'disable-glyph-caching': 'true', 'disable-glyph-caching': 'true',
'disable-gfx': 'true', // disable GFX Pipeline Extension (known FreeRDP 2.x crash source)
'cert-tofu': 'true', // trust certificate on first use
'resize-method': 'display-update', 'resize-method': 'display-update',
}; };
@@ -188,8 +212,9 @@ export async function rdpWebsocket(fastify: FastifyInstance) {
socket.send(buildInstruction('', guacdUUID)); socket.send(buildInstruction('', guacdUUID));
// 6. Flush any buffered bytes that arrived after 'ready' // 6. Flush any buffered bytes that arrived after 'ready'
if (tcpBuf.value.length > 0 && socket.readyState === WebSocket.OPEN) { if (tcpBuf.value.length > 0) {
socket.send(tcpBuf.value); fastify.log.info({ flushed: tcpBuf.value.substring(0, 300) }, 'RDP: flushing buffered data after ready');
if (socket.readyState === WebSocket.OPEN) socket.send(tcpBuf.value);
tcpBuf.value = ''; tcpBuf.value = '';
} }
} catch (err: unknown) { } catch (err: unknown) {
@@ -205,10 +230,7 @@ export async function rdpWebsocket(fastify: FastifyInstance) {
// --- Proxy mode --- // --- Proxy mode ---
guacd.on('data', (data: Buffer) => { guacd.on('data', (data: Buffer) => {
const text = data.toString('utf8'); const text = data.toString('utf8');
// Log the first message from guacd (usually an error or first instruction) fastify.log.debug({ guacdData: text.substring(0, 300) }, 'RDP: data from guacd');
if (text.startsWith('5.error') || text.startsWith('10.disconnect')) {
fastify.log.warn({ guacdMsg: text.substring(0, 200) }, 'RDP: guacd sent error/disconnect');
}
if (socket.readyState === WebSocket.OPEN) socket.send(text); if (socket.readyState === WebSocket.OPEN) socket.send(text);
}); });