mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:18:37 +00:00
fix(gateway): skip device pairing for local backend self-connections (#30801)
* fix(gateway): skip device pairing for local backend self-connections When gateway.tls is enabled, sessions_spawn (and other internal callGateway operations) creates a new WebSocket to the gateway. The gateway treated this self-connection like any external client and enforced device pairing, rejecting it with "pairing required" (close code 1008). This made sub-agent spawning impossible when TLS was enabled in Docker with bind: "lan". Skip pairing for connections that are gateway-client self-connections from localhost with valid shared auth (token/password). These are internal backend calls (e.g. sessions_spawn, subagent-announce) that already have valid credentials and connect from the same host. Closes #30740 * gateway: tighten backend self-pair bypass guard * tests: cover backend self-pairing local-vs-remote auth path * changelog: add gateway tls pairing fix credit --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -56,6 +56,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Feishu/Probe status caching: cache successful `probeFeishu()` bot-info results for 10 minutes (bounded cache with per-account keying) to reduce repeated status/onboarding probe API calls, while bypassing cache for failures and exceptions. (#28907) Thanks @Glucksberg.
|
- Feishu/Probe status caching: cache successful `probeFeishu()` bot-info results for 10 minutes (bounded cache with per-account keying) to reduce repeated status/onboarding probe API calls, while bypassing cache for failures and exceptions. (#28907) Thanks @Glucksberg.
|
||||||
- Feishu/Opus media send type: send `.opus` attachments with `msg_type: "audio"` (instead of `"media"`) so Feishu voice messages deliver correctly while `.mp4` remains `msg_type: "media"` and documents remain `msg_type: "file"`. (#28269) Thanks @Glucksberg.
|
- Feishu/Opus media send type: send `.opus` attachments with `msg_type: "audio"` (instead of `"media"`) so Feishu voice messages deliver correctly while `.mp4` remains `msg_type: "media"` and documents remain `msg_type: "file"`. (#28269) Thanks @Glucksberg.
|
||||||
- Gateway/WS security: keep plaintext `ws://` loopback-only by default, with explicit break-glass private-network opt-in via `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`; align onboarding/client/call validation and tests to this strict-default policy. (#28670) Thanks @dashed, @vincentkoc.
|
- Gateway/WS security: keep plaintext `ws://` loopback-only by default, with explicit break-glass private-network opt-in via `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`; align onboarding/client/call validation and tests to this strict-default policy. (#28670) Thanks @dashed, @vincentkoc.
|
||||||
|
- Gateway/Subagent TLS pairing: allow authenticated local `gateway-client` backend self-connections to skip device pairing while still requiring pairing for non-local/direct-host paths, restoring `sessions_spawn` with `gateway.tls.enabled=true` in Docker/LAN setups. Fixes #30740. Thanks @Sid-Qin and @vincentkoc.
|
||||||
- Feishu/Mobile video media type: treat inbound `message_type: "media"` as video-equivalent for media key extraction, placeholder inference, and media download resolution so mobile-app video sends ingest correctly. (#25502) Thanks @4ier.
|
- Feishu/Mobile video media type: treat inbound `message_type: "media"` as video-equivalent for media key extraction, placeholder inference, and media download resolution so mobile-app video sends ingest correctly. (#25502) Thanks @4ier.
|
||||||
- Feishu/Inbound sender fallback: fall back to `sender_id.user_id` when `sender_id.open_id` is missing on inbound events, and use ID-type-aware sender lookup so mobile-delivered messages keep stable sender identity/routing. (#26703) Thanks @NewdlDewdl.
|
- Feishu/Inbound sender fallback: fall back to `sender_id.user_id` when `sender_id.open_id` is missing on inbound events, and use ID-type-aware sender lookup so mobile-delivered messages keep stable sender identity/routing. (#26703) Thanks @NewdlDewdl.
|
||||||
- Feishu/Reply context metadata: include inbound `parent_id` and `root_id` as `ReplyToId`/`RootMessageId` in inbound context, and parse interactive-card quote bodies into readable text when fetching replied messages. (#18529) Thanks @qiangu.
|
- Feishu/Reply context metadata: include inbound `parent_id` and `root_id` as `ReplyToId`/`RootMessageId` in inbound context, and parse interactive-card quote bodies into readable text when fetching replied messages. (#18529) Thanks @qiangu.
|
||||||
|
|||||||
@@ -121,6 +121,13 @@ const NODE_CLIENT = {
|
|||||||
mode: GATEWAY_CLIENT_MODES.NODE,
|
mode: GATEWAY_CLIENT_MODES.NODE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const BACKEND_GATEWAY_CLIENT = {
|
||||||
|
id: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
|
||||||
|
version: "1.0.0",
|
||||||
|
platform: "node",
|
||||||
|
mode: GATEWAY_CLIENT_MODES.BACKEND,
|
||||||
|
};
|
||||||
|
|
||||||
async function expectHelloOkServerVersion(port: number, expectedVersion: string) {
|
async function expectHelloOkServerVersion(port: number, expectedVersion: string) {
|
||||||
const ws = await openWs(port);
|
const ws = await openWs(port);
|
||||||
try {
|
try {
|
||||||
@@ -1791,5 +1798,38 @@ describe("gateway server auth/connect", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("allows local gateway backend shared-auth connections without device pairing", async () => {
|
||||||
|
const { server, ws, prevToken } = await startServerWithClient("secret");
|
||||||
|
try {
|
||||||
|
const localBackend = await connectReq(ws, {
|
||||||
|
token: "secret",
|
||||||
|
client: BACKEND_GATEWAY_CLIENT,
|
||||||
|
});
|
||||||
|
expect(localBackend.ok).toBe(true);
|
||||||
|
} finally {
|
||||||
|
ws.close();
|
||||||
|
await server.close();
|
||||||
|
restoreGatewayToken(prevToken);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("requires pairing for gateway backend clients when connection is not local-direct", async () => {
|
||||||
|
const { server, ws, port, prevToken } = await startServerWithClient("secret");
|
||||||
|
ws.close();
|
||||||
|
const wsRemoteLike = await openWs(port, { host: "gateway.example" });
|
||||||
|
try {
|
||||||
|
const remoteLikeBackend = await connectReq(wsRemoteLike, {
|
||||||
|
token: "secret",
|
||||||
|
client: BACKEND_GATEWAY_CLIENT,
|
||||||
|
});
|
||||||
|
expect(remoteLikeBackend.ok).toBe(false);
|
||||||
|
expect(remoteLikeBackend.error?.message ?? "").toContain("pairing required");
|
||||||
|
} finally {
|
||||||
|
wsRemoteLike.close();
|
||||||
|
await server.close();
|
||||||
|
restoreGatewayToken(prevToken);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Remaining tests require isolated gateway state.
|
// Remaining tests require isolated gateway state.
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import {
|
|||||||
} from "../../net.js";
|
} from "../../net.js";
|
||||||
import { resolveNodeCommandAllowlist } from "../../node-command-policy.js";
|
import { resolveNodeCommandAllowlist } from "../../node-command-policy.js";
|
||||||
import { checkBrowserOrigin } from "../../origin-check.js";
|
import { checkBrowserOrigin } from "../../origin-check.js";
|
||||||
import { GATEWAY_CLIENT_IDS } from "../../protocol/client-info.js";
|
import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../../protocol/client-info.js";
|
||||||
import {
|
import {
|
||||||
ConnectErrorDetailCodes,
|
ConnectErrorDetailCodes,
|
||||||
resolveDeviceAuthConnectErrorDetailCode,
|
resolveDeviceAuthConnectErrorDetailCode,
|
||||||
@@ -136,6 +136,28 @@ function shouldAllowSilentLocalPairing(params: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldSkipBackendSelfPairing(params: {
|
||||||
|
connectParams: ConnectParams;
|
||||||
|
isLocalClient: boolean;
|
||||||
|
hasBrowserOriginHeader: boolean;
|
||||||
|
sharedAuthOk: boolean;
|
||||||
|
authMethod: GatewayAuthResult["method"];
|
||||||
|
}): boolean {
|
||||||
|
const isGatewayBackendClient =
|
||||||
|
params.connectParams.client.id === GATEWAY_CLIENT_IDS.GATEWAY_CLIENT &&
|
||||||
|
params.connectParams.client.mode === GATEWAY_CLIENT_MODES.BACKEND;
|
||||||
|
if (!isGatewayBackendClient) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const usesSharedSecretAuth = params.authMethod === "token" || params.authMethod === "password";
|
||||||
|
return (
|
||||||
|
params.isLocalClient &&
|
||||||
|
!params.hasBrowserOriginHeader &&
|
||||||
|
params.sharedAuthOk &&
|
||||||
|
usesSharedSecretAuth
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function resolveDeviceSignaturePayloadVersion(params: {
|
function resolveDeviceSignaturePayloadVersion(params: {
|
||||||
device: {
|
device: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -712,11 +734,14 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
authOk,
|
authOk,
|
||||||
authMethod,
|
authMethod,
|
||||||
});
|
});
|
||||||
const skipPairing = shouldSkipControlUiPairing(
|
const skipPairing =
|
||||||
controlUiAuthPolicy,
|
shouldSkipBackendSelfPairing({
|
||||||
sharedAuthOk,
|
connectParams,
|
||||||
trustedProxyAuthOk,
|
isLocalClient,
|
||||||
);
|
hasBrowserOriginHeader,
|
||||||
|
sharedAuthOk,
|
||||||
|
authMethod,
|
||||||
|
}) || shouldSkipControlUiPairing(controlUiAuthPolicy, sharedAuthOk, trustedProxyAuthOk);
|
||||||
if (device && devicePublicKey && !skipPairing) {
|
if (device && devicePublicKey && !skipPairing) {
|
||||||
const formatAuditList = (items: string[] | undefined): string => {
|
const formatAuditList = (items: string[] | undefined): string => {
|
||||||
if (!items || items.length === 0) {
|
if (!items || items.length === 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user