mirror of https://github.com/openclaw/openclaw.git
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:
parent
3002f13ca7
commit
e1e715c53d
|
|
@ -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/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/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/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.
|
||||
|
|
|
|||
|
|
@ -121,6 +121,13 @@ const NODE_CLIENT = {
|
|||
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) {
|
||||
const ws = await openWs(port);
|
||||
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.
|
||||
});
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ import {
|
|||
} from "../../net.js";
|
||||
import { resolveNodeCommandAllowlist } from "../../node-command-policy.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 {
|
||||
ConnectErrorDetailCodes,
|
||||
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: {
|
||||
device: {
|
||||
id: string;
|
||||
|
|
@ -712,11 +734,14 @@ export function attachGatewayWsMessageHandler(params: {
|
|||
authOk,
|
||||
authMethod,
|
||||
});
|
||||
const skipPairing = shouldSkipControlUiPairing(
|
||||
controlUiAuthPolicy,
|
||||
sharedAuthOk,
|
||||
trustedProxyAuthOk,
|
||||
);
|
||||
const skipPairing =
|
||||
shouldSkipBackendSelfPairing({
|
||||
connectParams,
|
||||
isLocalClient,
|
||||
hasBrowserOriginHeader,
|
||||
sharedAuthOk,
|
||||
authMethod,
|
||||
}) || shouldSkipControlUiPairing(controlUiAuthPolicy, sharedAuthOk, trustedProxyAuthOk);
|
||||
if (device && devicePublicKey && !skipPairing) {
|
||||
const formatAuditList = (items: string[] | undefined): string => {
|
||||
if (!items || items.length === 0) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue