From bf40baaa4dfad1f5deb90851d4e3657af84e0799 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 10 Apr 2026 12:38:31 +0100 Subject: [PATCH] fix(gateway): improve websocket auth logging --- src/gateway/call.test.ts | 19 ++++++ src/gateway/call.ts | 15 ++++- src/gateway/server/ws-connection.ts | 62 +++++++++++++++++-- .../server/ws-connection/message-handler.ts | 25 +++++++- 4 files changed, 113 insertions(+), 8 deletions(-) diff --git a/src/gateway/call.test.ts b/src/gateway/call.test.ts index b8ab5f5d18b..b009e38613d 100644 --- a/src/gateway/call.test.ts +++ b/src/gateway/call.test.ts @@ -27,6 +27,7 @@ let lastClientOptions: { token?: string; password?: string; tlsFingerprint?: string; + clientDisplayName?: string; scopes?: string[]; deviceIdentity?: unknown; onHelloOk?: (hello: { features?: { methods?: string[] } }) => void | Promise; @@ -58,6 +59,7 @@ vi.mock("./client.js", () => ({ url?: string; token?: string; password?: string; + clientDisplayName?: string; scopes?: string[]; onHelloOk?: (hello: { features?: { methods?: string[] } }) => void | Promise; onClose?: (code: number, reason: string) => void; @@ -95,6 +97,7 @@ class StubGatewayClient { url?: string; token?: string; password?: string; + clientDisplayName?: string; scopes?: string[]; onHelloOk?: (hello: { features?: { methods?: string[] } }) => void | Promise; onClose?: (code: number, reason: string) => void; @@ -452,6 +455,22 @@ describe("callGateway url resolution", () => { expect(lastClientOptions?.scopes).toEqual([]); }); + it("labels default backend calls with the requested method", async () => { + setLocalLoopbackGatewayConfig(); + + await callGateway({ method: "sessions.delete" }); + + expect(lastClientOptions?.clientDisplayName).toBe("gateway:sessions.delete"); + }); + + it("does not synthesize display names for CLI calls", async () => { + setLocalLoopbackGatewayConfig(); + + await callGatewayCli({ method: "health" }); + + expect(lastClientOptions?.clientDisplayName).toBeUndefined(); + }); + it("yields one event-loop turn before starting CLI pairing requests", async () => { setLocalLoopbackGatewayConfig(); diff --git a/src/gateway/call.ts b/src/gateway/call.ts index 08c9d144b21..ed95cc79063 100644 --- a/src/gateway/call.ts +++ b/src/gateway/call.ts @@ -108,6 +108,19 @@ const gatewayCallDeps = { ...defaultGatewayCallDeps, }; +function resolveGatewayClientDisplayName(opts: CallGatewayBaseOptions): string | undefined { + if (opts.clientDisplayName) { + return opts.clientDisplayName; + } + const clientName = opts.clientName ?? GATEWAY_CLIENT_NAMES.CLI; + const mode = opts.mode ?? GATEWAY_CLIENT_MODES.CLI; + if (mode !== GATEWAY_CLIENT_MODES.BACKEND && clientName !== GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT) { + return undefined; + } + const method = opts.method.trim(); + return method ? `gateway:${method}` : "gateway:request"; +} + function loadGatewayConfig(): OpenClawConfig { const loadConfigFn = typeof gatewayCallDeps.loadConfig === "function" @@ -745,7 +758,7 @@ async function executeGatewayRequestWithScopes(params: { tlsFingerprint, instanceId: opts.instanceId ?? randomUUID(), clientName: opts.clientName ?? GATEWAY_CLIENT_NAMES.CLI, - clientDisplayName: opts.clientDisplayName, + clientDisplayName: resolveGatewayClientDisplayName(opts), clientVersion: opts.clientVersion ?? VERSION, platform: opts.platform, mode: opts.mode ?? GATEWAY_CLIENT_MODES.CLI, diff --git a/src/gateway/server/ws-connection.ts b/src/gateway/server/ws-connection.ts index 8222ee7b1bd..9a5b1722a9f 100644 --- a/src/gateway/server/ws-connection.ts +++ b/src/gateway/server/ws-connection.ts @@ -1,4 +1,5 @@ import { randomUUID } from "node:crypto"; +import type { Socket } from "node:net"; import type { WebSocket, WebSocketServer } from "ws"; import { resolveCanvasHostUrl } from "../../infra/canvas-host-url.js"; import { removeRemoteNodeInfo } from "../../infra/skills-remote.js"; @@ -61,6 +62,45 @@ const sanitizeLogValue = (value: string | undefined): string | undefined => { return truncateUtf16Safe(cleaned, LOG_HEADER_MAX_LEN); }; +function formatSocketEndpoint( + address: string | undefined, + port: number | undefined, +): string | undefined { + if (!address) { + return undefined; + } + if (port === undefined) { + return address; + } + return address.includes(":") ? `[${address}]:${port}` : `${address}:${port}`; +} + +function resolveSocketAddress(socket: WebSocket): { + remoteAddr?: string; + remotePort?: number; + localAddr?: string; + localPort?: number; + endpoint?: string; +} { + const rawSocket = (socket as WebSocket & { _socket?: Socket })._socket; + const remoteAddr = rawSocket?.remoteAddress; + const remotePort = rawSocket?.remotePort; + const localAddr = rawSocket?.localAddress; + const localPort = rawSocket?.localPort; + const remoteEndpoint = formatSocketEndpoint(remoteAddr, remotePort); + const localEndpoint = formatSocketEndpoint(localAddr, localPort); + return { + remoteAddr, + remotePort, + localAddr, + localPort, + endpoint: + remoteEndpoint && localEndpoint + ? `${remoteEndpoint}->${localEndpoint}` + : (remoteEndpoint ?? localEndpoint), + }; +} + export type GatewayWsSharedHandlerParams = { wss: WebSocketServer; clients: Set; @@ -127,8 +167,7 @@ export function attachGatewayWsConnectionHandler(params: AttachGatewayWsConnecti let closed = false; const openedAt = Date.now(); const connId = randomUUID(); - const remoteAddr = (socket as WebSocket & { _socket?: { remoteAddress?: string } })._socket - ?.remoteAddress; + const { remoteAddr, remotePort, localAddr, localPort, endpoint } = resolveSocketAddress(socket); const preauthBudgetKey = ( socket as WebSocket & { __openclawPreauthBudgetClaimed?: boolean; @@ -159,7 +198,7 @@ export function attachGatewayWsConnectionHandler(params: AttachGatewayWsConnecti localAddress: upgradeReq.socket?.localAddress, }); - logWs("in", "open", { connId, remoteAddr }); + logWs("in", "open", { connId, remoteAddr, remotePort, localAddr, localPort, endpoint }); let handshakeState: "pending" | "connected" | "failed" = "pending"; let holdsPreauthBudget = true; let closeCause: string | undefined; @@ -254,6 +293,11 @@ export function attachGatewayWsConnectionHandler(params: AttachGatewayWsConnecti origin: logOrigin, userAgent: logUserAgent, forwardedFor: logForwardedFor, + remoteAddr, + remotePort, + localAddr, + localPort, + endpoint, ...closeMeta, }; if (!client) { @@ -261,7 +305,7 @@ export function attachGatewayWsConnectionHandler(params: AttachGatewayWsConnecti ? logWsControl.debug : logWsControl.warn; logFn( - `closed before connect conn=${connId} remote=${remoteAddr ?? "?"} fwd=${logForwardedFor || "n/a"} origin=${logOrigin || "n/a"} host=${logHost || "n/a"} ua=${logUserAgent || "n/a"} code=${code ?? "n/a"} reason=${logReason || "n/a"}`, + `closed before connect conn=${connId} peer=${endpoint ?? "n/a"} remote=${remoteAddr ?? "?"} fwd=${logForwardedFor || "n/a"} origin=${logOrigin || "n/a"} host=${logHost || "n/a"} ua=${logUserAgent || "n/a"} code=${code ?? "n/a"} reason=${logReason || "n/a"}`, closeContext, ); } @@ -293,6 +337,7 @@ export function attachGatewayWsConnectionHandler(params: AttachGatewayWsConnecti lastFrameType, lastFrameMethod, lastFrameId, + endpoint, }); close(); }); @@ -303,8 +348,11 @@ export function attachGatewayWsConnectionHandler(params: AttachGatewayWsConnecti handshakeState = "failed"; setCloseCause("handshake-timeout", { handshakeMs: Date.now() - openedAt, + endpoint, }); - logWsControl.warn(`handshake timeout conn=${connId} remote=${remoteAddr ?? "?"}`); + logWsControl.warn( + `handshake timeout conn=${connId} peer=${endpoint ?? "n/a"} remote=${remoteAddr ?? "?"}`, + ); close(); } }, handshakeTimeoutMs); @@ -314,6 +362,10 @@ export function attachGatewayWsConnectionHandler(params: AttachGatewayWsConnecti upgradeReq, connId, remoteAddr, + remotePort, + localAddr, + localPort, + endpoint, forwardedFor, realIp, requestHost, diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index a3fc3203f86..aa3f0472bee 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -161,6 +161,10 @@ export function attachGatewayWsMessageHandler(params: { upgradeReq: IncomingMessage; connId: string; remoteAddr?: string; + remotePort?: number; + localAddr?: string; + localPort?: number; + endpoint?: string; forwardedFor?: string; realIp?: string; requestHost?: string; @@ -197,6 +201,10 @@ export function attachGatewayWsMessageHandler(params: { upgradeReq, connId, remoteAddr, + remotePort, + localAddr, + localPort, + endpoint, forwardedFor, realIp, requestHost, @@ -248,6 +256,7 @@ export function attachGatewayWsMessageHandler(params: { trustedProxies, allowRealIpFallback, }); + const peerLabel = endpoint ?? remoteAddr ?? "n/a"; // If proxy headers are present but the remote address isn't trusted, don't treat // the connection as local. This prevents auth bypass when running behind a reverse @@ -369,7 +378,7 @@ export function attachGatewayWsMessageHandler(params: { }); } else { logWsControl.warn( - `invalid handshake conn=${connId} remote=${remoteAddr ?? "?"} fwd=${forwardedFor ?? "n/a"} origin=${requestOrigin ?? "n/a"} host=${requestHost ?? "n/a"} ua=${requestUserAgent ?? "n/a"}`, + `invalid handshake conn=${connId} peer=${formatForLog(peerLabel)} remote=${remoteAddr ?? "?"} fwd=${formatForLog(forwardedFor ?? "n/a")} origin=${formatForLog(requestOrigin ?? "n/a")} host=${formatForLog(requestHost ?? "n/a")} ua=${formatForLog(requestUserAgent ?? "n/a")}`, ); } const closeReason = truncateCloseReason(handshakeError || "invalid handshake"); @@ -389,6 +398,10 @@ export function attachGatewayWsMessageHandler(params: { clientDisplayName: connectParams.client.displayName, mode: connectParams.client.mode, version: connectParams.client.version, + platform: connectParams.client.platform, + deviceFamily: connectParams.client.deviceFamily, + modelIdentifier: connectParams.client.modelIdentifier, + instanceId: connectParams.client.instanceId, }; const markHandshakeFailure = (cause: string, meta?: Record) => { setHandshakeState("failed"); @@ -529,9 +542,17 @@ export function attachGatewayWsMessageHandler(params: { authProvided, authReason: failedAuth.reason, allowTailscale: resolvedAuth.allowTailscale, + peer: peerLabel, + remoteAddr, + remotePort, + localAddr, + localPort, + role, + scopeCount: scopes.length, + hasDeviceIdentity: Boolean(device), }); logWsControl.warn( - `unauthorized conn=${connId} remote=${remoteAddr ?? "?"} client=${clientLabel} ${connectParams.client.mode} v${connectParams.client.version} reason=${failedAuth.reason ?? "unknown"}`, + `unauthorized conn=${connId} peer=${formatForLog(peerLabel)} remote=${remoteAddr ?? "?"} client=${formatForLog(clientLabel)} ${connectParams.client.mode} v${formatForLog(connectParams.client.version)} role=${role} scopes=${scopes.length} auth=${authProvided} device=${device ? "yes" : "no"} platform=${formatForLog(connectParams.client.platform)} instance=${formatForLog(connectParams.client.instanceId ?? "n/a")} host=${formatForLog(requestHost ?? "n/a")} origin=${formatForLog(requestOrigin ?? "n/a")} ua=${formatForLog(requestUserAgent ?? "n/a")} reason=${failedAuth.reason ?? "unknown"}`, ); const authMessage = formatGatewayAuthFailureMessage({ authMode: resolvedAuth.mode,