fix(gateway): classify loopback shared-secret clients as local for pairing (#69397)

This commit is contained in:
SARAMALI15792
2026-04-20 23:01:10 +05:00
committed by Peter Steinberger
parent 43734b1dbd
commit 8ef356d5c3
2 changed files with 142 additions and 2 deletions

View File

@@ -322,7 +322,7 @@ describe("handshake auth helpers", () => {
).toBe("remote");
});
it("keeps non-CLI clients remote when only the Docker CLI fallback conditions match", () => {
it("classifies non-CLI Docker-published loopback clients as shared_secret_loopback_local when auth is token/password", () => {
const connectParams = {
client: {
id: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT,
@@ -340,7 +340,7 @@ describe("handshake auth helpers", () => {
sharedAuthOk: true,
authMethod: "token",
}),
).toBe("remote");
).toBe("shared_secret_loopback_local");
});
it("skips backend self-pairing only for direct-local backend clients", () => {
@@ -441,4 +441,111 @@ describe("handshake auth helpers", () => {
}),
).toBe(false);
});
it("classifies non-CLI loopback + shared-secret clients as shared_secret_loopback_local", () => {
const connectParams = {
client: {
id: GATEWAY_CLIENT_IDS.NODE_HOST,
mode: GATEWAY_CLIENT_MODES.NODE,
},
} as ConnectParams;
expect(
resolvePairingLocality({
connectParams,
isLocalClient: false,
requestHost: "127.0.0.1:18789",
remoteAddress: "127.0.0.1",
hasProxyHeaders: false,
hasBrowserOriginHeader: false,
sharedAuthOk: true,
authMethod: "token",
}),
).toBe("shared_secret_loopback_local");
});
it("keeps non-CLI loopback clients remote without shared-secret auth", () => {
const connectParams = {
client: {
id: GATEWAY_CLIENT_IDS.NODE_HOST,
mode: GATEWAY_CLIENT_MODES.NODE,
},
} as ConnectParams;
const base = {
connectParams,
isLocalClient: false,
requestHost: "127.0.0.1:18789",
remoteAddress: "127.0.0.1",
hasProxyHeaders: false,
hasBrowserOriginHeader: false,
} as const;
expect(
resolvePairingLocality({
...base,
sharedAuthOk: false,
authMethod: "token",
}),
).toBe("remote");
expect(
resolvePairingLocality({
...base,
sharedAuthOk: true,
authMethod: "device-token",
}),
).toBe("remote");
expect(
resolvePairingLocality({
...base,
remoteAddress: "192.168.1.10",
sharedAuthOk: true,
authMethod: "token",
}),
).toBe("remote");
expect(
resolvePairingLocality({
...base,
hasProxyHeaders: true,
sharedAuthOk: true,
authMethod: "token",
}),
).toBe("remote");
expect(
resolvePairingLocality({
...base,
hasBrowserOriginHeader: true,
sharedAuthOk: true,
authMethod: "token",
}),
).toBe("remote");
});
it("allows silent scope-upgrade for shared_secret_loopback_local", () => {
expect(
shouldAllowSilentLocalPairing({
locality: "shared_secret_loopback_local",
hasBrowserOriginHeader: false,
isControlUi: false,
isWebchat: false,
reason: "scope-upgrade",
}),
).toBe(true);
expect(
shouldAllowSilentLocalPairing({
locality: "shared_secret_loopback_local",
hasBrowserOriginHeader: false,
isControlUi: false,
isWebchat: false,
reason: "role-upgrade",
}),
).toBe(true);
expect(
shouldAllowSilentLocalPairing({
locality: "shared_secret_loopback_local",
hasBrowserOriginHeader: false,
isControlUi: false,
isWebchat: false,
reason: "metadata-upgrade",
}),
).toBe(false);
});
});

View File

@@ -20,6 +20,7 @@ export type PairingLocalityKind =
| "direct_local"
| "cli_container_local"
| "browser_container_local"
| "shared_secret_loopback_local"
| "remote";
export type HandshakeBrowserSecurityContext = {
@@ -111,6 +112,26 @@ function isCliContainerLocalEquivalent(params: {
);
}
function isSharedSecretLoopbackLocalEquivalent(params: {
requestHost?: string;
remoteAddress?: string;
hasProxyHeaders: boolean;
hasBrowserOriginHeader: boolean;
sharedAuthOk: boolean;
authMethod: GatewayAuthResult["method"];
}): boolean {
const usesSharedSecretAuth =
params.authMethod === "token" || params.authMethod === "password";
return (
params.sharedAuthOk &&
usesSharedSecretAuth &&
!params.hasProxyHeaders &&
!params.hasBrowserOriginHeader &&
isLoopbackAddress(params.remoteAddress) &&
isPrivateOrLoopbackHost(resolveHostName(params.requestHost))
);
}
function resolveOriginHost(origin?: string): string {
const trimmed = origin?.trim();
if (!trimmed) {
@@ -190,6 +211,18 @@ export function resolvePairingLocality(params: {
) {
return "cli_container_local";
}
if (
isSharedSecretLoopbackLocalEquivalent({
requestHost: params.requestHost,
remoteAddress: params.remoteAddress,
hasProxyHeaders: params.hasProxyHeaders,
hasBrowserOriginHeader: params.hasBrowserOriginHeader,
sharedAuthOk: params.sharedAuthOk,
authMethod: params.authMethod,
})
) {
return "shared_secret_loopback_local";
}
return "remote";
}