refactor(gateway): harden proxy client ip resolution

This commit is contained in:
Peter Steinberger
2026-02-21 13:32:25 +01:00
parent 8b1fe0d1e2
commit be7f825006
15 changed files with 246 additions and 87 deletions

View File

@@ -2043,6 +2043,8 @@ See [Plugins](/tools/plugin).
// password: "your-password",
},
trustedProxies: ["10.0.0.1"],
// Optional. Default false.
allowRealIpFallback: false,
tools: {
// Additional /tools/invoke HTTP denies
deny: ["browser"],
@@ -2068,6 +2070,7 @@ See [Plugins](/tools/plugin).
- `remote.transport`: `ssh` (default) or `direct` (ws/wss). For `direct`, `remote.url` must be `ws://` or `wss://`.
- `gateway.remote.token` is for remote CLI calls only; does not enable local gateway auth.
- `trustedProxies`: reverse proxy IPs that terminate TLS. Only list proxies you control.
- `allowRealIpFallback`: when `true`, the gateway accepts `X-Real-IP` if `X-Forwarded-For` is missing. Default `false` for fail-closed behavior.
- `gateway.tools.deny`: extra tool names blocked for HTTP `POST /tools/invoke` (extends default deny list).
- `gateway.tools.allow`: remove tool names from the default HTTP deny list.

View File

@@ -168,18 +168,34 @@ keys so you can review them in one place (for example
If you run the Gateway behind a reverse proxy (nginx, Caddy, Traefik, etc.), you should configure `gateway.trustedProxies` for proper client IP detection.
When the Gateway detects proxy headers (`X-Forwarded-For` or `X-Real-IP`) from an address that is **not** in `trustedProxies`, it will **not** treat connections as local clients. If gateway auth is disabled, those connections are rejected. This prevents authentication bypass where proxied connections would otherwise appear to come from localhost and receive automatic trust.
When the Gateway detects proxy headers from an address that is **not** in `trustedProxies`, it will **not** treat connections as local clients. If gateway auth is disabled, those connections are rejected. This prevents authentication bypass where proxied connections would otherwise appear to come from localhost and receive automatic trust.
```yaml
gateway:
trustedProxies:
- "127.0.0.1" # if your proxy runs on localhost
# Optional. Default false.
# Only enable if your proxy cannot provide X-Forwarded-For.
allowRealIpFallback: false
auth:
mode: password
password: ${OPENCLAW_GATEWAY_PASSWORD}
```
When `trustedProxies` is configured, the Gateway will use `X-Forwarded-For` headers to determine the real client IP for local client detection. Make sure your proxy overwrites (not appends to) incoming `X-Forwarded-For` headers to prevent spoofing.
When `trustedProxies` is configured, the Gateway uses `X-Forwarded-For` to determine the client IP. `X-Real-IP` is ignored by default unless `gateway.allowRealIpFallback: true` is explicitly set.
Good reverse proxy behavior (overwrite incoming forwarding headers):
```nginx
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
```
Bad reverse proxy behavior (append/preserve untrusted forwarding headers):
```nginx
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
```
## Local session logs live on disk

View File

@@ -310,10 +310,15 @@ export type GatewayConfig = {
nodes?: GatewayNodesConfig;
/**
* IPs of trusted reverse proxies (e.g. Traefik, nginx). When a connection
* arrives from one of these IPs, the Gateway trusts `x-forwarded-for` (or
* `x-real-ip`) to determine the client IP for local pairing and HTTP checks.
* arrives from one of these IPs, the Gateway trusts `x-forwarded-for`
* to determine the client IP for local pairing and HTTP checks.
*/
trustedProxies?: string[];
/**
* Allow `x-real-ip` as a fallback only when `x-forwarded-for` is missing.
* Default: false (safer fail-closed behavior).
*/
allowRealIpFallback?: boolean;
/** Tool access restrictions for HTTP /tools/invoke endpoint. */
tools?: GatewayToolsConfig;
/**

View File

@@ -450,6 +450,7 @@ export const OpenClawSchema = z
.strict()
.optional(),
trustedProxies: z.array(z.string()).optional(),
allowRealIpFallback: z.boolean().optional(),
tools: z
.object({
deny: z.array(z.string()).optional(),

View File

@@ -301,6 +301,45 @@ describe("gateway auth", () => {
expect(limiter.recordFailure).toHaveBeenCalledWith("203.0.113.10", "shared-secret");
});
it("ignores X-Real-IP fallback by default for rate-limit checks", async () => {
const limiter = createLimiterSpy();
const res = await authorizeGatewayConnect({
auth: { mode: "token", token: "secret", allowTailscale: false },
connectAuth: { token: "wrong" },
req: {
socket: { remoteAddress: "127.0.0.1" },
headers: { "x-real-ip": "203.0.113.77" },
} as never,
trustedProxies: ["127.0.0.1"],
rateLimiter: limiter,
});
expect(res.ok).toBe(false);
expect(res.reason).toBe("token_mismatch");
expect(limiter.check).toHaveBeenCalledWith("127.0.0.1", "shared-secret");
expect(limiter.recordFailure).toHaveBeenCalledWith("127.0.0.1", "shared-secret");
});
it("uses X-Real-IP when fallback is explicitly enabled", async () => {
const limiter = createLimiterSpy();
const res = await authorizeGatewayConnect({
auth: { mode: "token", token: "secret", allowTailscale: false },
connectAuth: { token: "wrong" },
req: {
socket: { remoteAddress: "127.0.0.1" },
headers: { "x-real-ip": "203.0.113.77" },
} as never,
trustedProxies: ["127.0.0.1"],
allowRealIpFallback: true,
rateLimiter: limiter,
});
expect(res.ok).toBe(false);
expect(res.reason).toBe("token_mismatch");
expect(limiter.check).toHaveBeenCalledWith("203.0.113.77", "shared-secret");
expect(limiter.recordFailure).toHaveBeenCalledWith("203.0.113.77", "shared-secret");
});
it("passes custom rate-limit scope to limiter operations", async () => {
const limiter = createLimiterSpy();
const res = await authorizeGatewayConnect({

View File

@@ -15,8 +15,7 @@ import {
isLoopbackAddress,
isTrustedProxyAddress,
resolveHostName,
parseForwardedForClientIp,
resolveGatewayClientIp,
resolveClientIp,
} from "./net.js";
export type ResolvedGatewayAuthMode = "none" | "token" | "password" | "trusted-proxy";
@@ -71,6 +70,8 @@ export type AuthorizeGatewayConnectParams = {
clientIp?: string;
/** Optional limiter scope; defaults to shared-secret auth scope. */
rateLimitScope?: string;
/** Trust X-Real-IP only when explicitly enabled. */
allowRealIpFallback?: boolean;
};
type TailscaleUser = {
@@ -89,34 +90,45 @@ function headerValue(value: string | string[] | undefined): string | undefined {
return Array.isArray(value) ? value[0] : value;
}
const TAILSCALE_TRUSTED_PROXIES = ["127.0.0.1", "::1"] as const;
function resolveTailscaleClientIp(req?: IncomingMessage): string | undefined {
if (!req) {
return undefined;
}
const forwardedFor = headerValue(req.headers?.["x-forwarded-for"]);
return forwardedFor ? parseForwardedForClientIp(forwardedFor) : undefined;
return resolveClientIp({
remoteAddr: req.socket?.remoteAddress ?? "",
forwardedFor: headerValue(req.headers?.["x-forwarded-for"]),
trustedProxies: [...TAILSCALE_TRUSTED_PROXIES],
});
}
function resolveRequestClientIp(
req?: IncomingMessage,
trustedProxies?: string[],
allowRealIpFallback = false,
): string | undefined {
if (!req) {
return undefined;
}
return resolveGatewayClientIp({
return resolveClientIp({
remoteAddr: req.socket?.remoteAddress ?? "",
forwardedFor: headerValue(req.headers?.["x-forwarded-for"]),
realIp: headerValue(req.headers?.["x-real-ip"]),
trustedProxies,
allowRealIpFallback,
});
}
export function isLocalDirectRequest(req?: IncomingMessage, trustedProxies?: string[]): boolean {
export function isLocalDirectRequest(
req?: IncomingMessage,
trustedProxies?: string[],
allowRealIpFallback = false,
): boolean {
if (!req) {
return false;
}
const clientIp = resolveRequestClientIp(req, trustedProxies) ?? "";
const clientIp = resolveRequestClientIp(req, trustedProxies, allowRealIpFallback) ?? "";
if (!isLoopbackAddress(clientIp)) {
return false;
}
@@ -351,7 +363,11 @@ export async function authorizeGatewayConnect(
const tailscaleWhois = params.tailscaleWhois ?? readTailscaleWhoisIdentity;
const authSurface = params.authSurface ?? "http";
const allowTailscaleHeaderAuth = shouldAllowTailscaleHeaderAuth(authSurface);
const localDirect = isLocalDirectRequest(req, trustedProxies);
const localDirect = isLocalDirectRequest(
req,
trustedProxies,
params.allowRealIpFallback === true,
);
if (auth.mode === "trusted-proxy") {
if (!auth.trustedProxy) {
@@ -379,7 +395,9 @@ export async function authorizeGatewayConnect(
const limiter = params.rateLimiter;
const ip =
params.clientIp ?? resolveRequestClientIp(req, trustedProxies) ?? req?.socket?.remoteAddress;
params.clientIp ??
resolveRequestClientIp(req, trustedProxies, params.allowRealIpFallback === true) ??
req?.socket?.remoteAddress;
const rateLimitScope = params.rateLimitScope ?? AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET;
if (limiter) {
const rlCheck: RateLimitCheckResult = limiter.check(ip, rateLimitScope);

View File

@@ -9,6 +9,7 @@ export async function authorizeGatewayBearerRequestOrReply(params: {
res: ServerResponse;
auth: ResolvedGatewayAuth;
trustedProxies?: string[];
allowRealIpFallback?: boolean;
rateLimiter?: AuthRateLimiter;
}): Promise<boolean> {
const token = getBearerToken(params.req);
@@ -17,6 +18,7 @@ export async function authorizeGatewayBearerRequestOrReply(params: {
connectAuth: token ? { token, password: token } : null,
req: params.req,
trustedProxies: params.trustedProxies,
allowRealIpFallback: params.allowRealIpFallback,
rateLimiter: params.rateLimiter,
});
if (!authResult.ok) {

View File

@@ -12,6 +12,7 @@ export async function handleGatewayPostJsonEndpoint(
auth: ResolvedGatewayAuth;
maxBodyBytes: number;
trustedProxies?: string[];
allowRealIpFallback?: boolean;
rateLimiter?: AuthRateLimiter;
},
): Promise<false | { body: unknown } | undefined> {
@@ -30,6 +31,7 @@ export async function handleGatewayPostJsonEndpoint(
res,
auth: opts.auth,
trustedProxies: opts.trustedProxies,
allowRealIpFallback: opts.allowRealIpFallback,
rateLimiter: opts.rateLimiter,
});
if (!authorized) {

View File

@@ -5,7 +5,7 @@ import {
isSecureWebSocketUrl,
isTrustedProxyAddress,
pickPrimaryLanIPv4,
resolveGatewayClientIp,
resolveClientIp,
resolveGatewayListenHosts,
resolveHostName,
} from "./net.js";
@@ -132,49 +132,74 @@ describe("isTrustedProxyAddress", () => {
});
});
describe("resolveGatewayClientIp", () => {
it("returns remote IP when the remote is not a trusted proxy", () => {
const ip = resolveGatewayClientIp({
describe("resolveClientIp", () => {
it.each([
{
name: "returns remote IP when remote is not trusted proxy",
remoteAddr: "203.0.113.10",
forwardedFor: "10.0.0.2",
trustedProxies: ["127.0.0.1"],
});
expect(ip).toBe("203.0.113.10");
});
it("returns forwarded client IP when the remote is a trusted proxy", () => {
const ip = resolveGatewayClientIp({
remoteAddr: "127.0.0.1",
forwardedFor: "127.0.0.1, 10.0.0.2",
trustedProxies: ["127.0.0.1"],
});
expect(ip).toBe("10.0.0.2");
});
it("does not trust the left-most X-Forwarded-For value when behind a trusted proxy", () => {
const ip = resolveGatewayClientIp({
expected: "203.0.113.10",
},
{
name: "uses right-most untrusted X-Forwarded-For hop",
remoteAddr: "127.0.0.1",
forwardedFor: "198.51.100.99, 10.0.0.9, 127.0.0.1",
trustedProxies: ["127.0.0.1"],
});
expect(ip).toBe("10.0.0.9");
});
it("fails closed when trusted proxy headers are missing", () => {
const ip = resolveGatewayClientIp({
expected: "10.0.0.9",
},
{
name: "fails closed when all X-Forwarded-For hops are trusted proxies",
remoteAddr: "127.0.0.1",
forwardedFor: "127.0.0.1, ::1",
trustedProxies: ["127.0.0.1", "::1"],
expected: undefined,
},
{
name: "fails closed when trusted proxy omits forwarding headers",
remoteAddr: "127.0.0.1",
trustedProxies: ["127.0.0.1"],
});
expect(ip).toBeUndefined();
});
it("supports IPv6 client IP forwarded by a trusted proxy", () => {
const ip = resolveGatewayClientIp({
expected: undefined,
},
{
name: "ignores invalid X-Forwarded-For entries",
remoteAddr: "127.0.0.1",
forwardedFor: "garbage, 10.0.0.999",
trustedProxies: ["127.0.0.1"],
expected: undefined,
},
{
name: "does not trust X-Real-IP by default",
remoteAddr: "127.0.0.1",
realIp: "[2001:db8::5]",
trustedProxies: ["127.0.0.1"],
expected: undefined,
},
{
name: "uses X-Real-IP only when explicitly enabled",
remoteAddr: "127.0.0.1",
realIp: "[2001:db8::5]",
trustedProxies: ["127.0.0.1"],
allowRealIpFallback: true,
expected: "2001:db8::5",
},
{
name: "ignores invalid X-Real-IP even when fallback enabled",
remoteAddr: "127.0.0.1",
realIp: "not-an-ip",
trustedProxies: ["127.0.0.1"],
allowRealIpFallback: true,
expected: undefined,
},
])("$name", (testCase) => {
const ip = resolveClientIp({
remoteAddr: testCase.remoteAddr,
forwardedFor: testCase.forwardedFor,
realIp: testCase.realIp,
trustedProxies: testCase.trustedProxies,
allowRealIpFallback: testCase.allowRealIpFallback,
});
expect(ip).toBe("2001:db8::5");
expect(ip).toBe(testCase.expected);
});
});

View File

@@ -146,45 +146,51 @@ function stripOptionalPort(ip: string): string {
return ip;
}
export function parseForwardedForClientIp(
forwardedFor?: string,
trustedProxies?: string[],
): string | undefined {
const entries = forwardedFor
?.split(",")
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
if (!entries?.length) {
function parseIpLiteral(raw: string | undefined): string | undefined {
const trimmed = raw?.trim();
if (!trimmed) {
return undefined;
}
if (!trustedProxies?.length) {
const raw = entries.at(-1);
if (!raw) {
return undefined;
}
return normalizeIp(stripOptionalPort(raw));
const stripped = stripOptionalPort(trimmed);
const normalized = normalizeIp(stripped);
if (!normalized || net.isIP(normalized) === 0) {
return undefined;
}
for (let index = entries.length - 1; index >= 0; index -= 1) {
const normalized = normalizeIp(stripOptionalPort(entries[index]));
if (!normalized) {
continue;
}
if (!isTrustedProxyAddress(normalized, trustedProxies)) {
return normalized;
}
}
return undefined;
return normalized;
}
function parseRealIp(realIp?: string): string | undefined {
const raw = realIp?.trim();
if (!raw) {
return parseIpLiteral(realIp);
}
function resolveForwardedClientIp(params: {
forwardedFor?: string;
trustedProxies?: string[];
}): string | undefined {
const { forwardedFor, trustedProxies } = params;
if (!trustedProxies?.length) {
return undefined;
}
return normalizeIp(stripOptionalPort(raw));
const forwardedChain: string[] = [];
for (const entry of forwardedFor?.split(",") ?? []) {
const normalized = parseIpLiteral(entry);
if (normalized) {
forwardedChain.push(normalized);
}
}
if (forwardedChain.length === 0) {
return undefined;
}
// Walk right-to-left and return the first untrusted hop.
for (let index = forwardedChain.length - 1; index >= 0; index -= 1) {
const hop = forwardedChain[index];
if (!isTrustedProxyAddress(hop, trustedProxies)) {
return hop;
}
}
return undefined;
}
/**
@@ -252,11 +258,13 @@ export function isTrustedProxyAddress(ip: string | undefined, trustedProxies?: s
});
}
export function resolveGatewayClientIp(params: {
export function resolveClientIp(params: {
remoteAddr?: string;
forwardedFor?: string;
realIp?: string;
trustedProxies?: string[];
/** Default false: only trust X-Real-IP when explicitly enabled. */
allowRealIpFallback?: boolean;
}): string | undefined {
const remote = normalizeIp(params.remoteAddr);
if (!remote) {
@@ -268,10 +276,17 @@ export function resolveGatewayClientIp(params: {
// Fail closed when traffic comes from a trusted proxy but client-origin headers
// are missing or invalid. Falling back to the proxy's own IP can accidentally
// treat unrelated requests as local/trusted.
return (
parseForwardedForClientIp(params.forwardedFor, params.trustedProxies) ??
parseRealIp(params.realIp)
);
const forwardedIp = resolveForwardedClientIp({
forwardedFor: params.forwardedFor,
trustedProxies: params.trustedProxies,
});
if (forwardedIp) {
return forwardedIp;
}
if (params.allowRealIpFallback) {
return parseRealIp(params.realIp);
}
return undefined;
}
export function isLocalGatewayAddress(ip: string | undefined): boolean {

View File

@@ -20,6 +20,7 @@ type OpenAiHttpOptions = {
auth: ResolvedGatewayAuth;
maxBodyBytes?: number;
trustedProxies?: string[];
allowRealIpFallback?: boolean;
rateLimiter?: AuthRateLimiter;
};
@@ -162,6 +163,7 @@ export async function handleOpenAiHttpRequest(
pathname: "/v1/chat/completions",
auth: opts.auth,
trustedProxies: opts.trustedProxies,
allowRealIpFallback: opts.allowRealIpFallback,
rateLimiter: opts.rateLimiter,
maxBodyBytes: opts.maxBodyBytes ?? 1024 * 1024,
});

View File

@@ -55,6 +55,7 @@ type OpenResponsesHttpOptions = {
maxBodyBytes?: number;
config?: GatewayHttpResponsesConfig;
trustedProxies?: string[];
allowRealIpFallback?: boolean;
rateLimiter?: AuthRateLimiter;
};
@@ -343,6 +344,7 @@ export async function handleOpenResponsesHttpRequest(
pathname: "/v1/responses",
auth: opts.auth,
trustedProxies: opts.trustedProxies,
allowRealIpFallback: opts.allowRealIpFallback,
rateLimiter: opts.rateLimiter,
maxBodyBytes,
});

View File

@@ -133,17 +133,26 @@ async function authorizeCanvasRequest(params: {
req: IncomingMessage;
auth: ResolvedGatewayAuth;
trustedProxies: string[];
allowRealIpFallback: boolean;
clients: Set<GatewayWsClient>;
canvasCapability?: string;
malformedScopedPath?: boolean;
rateLimiter?: AuthRateLimiter;
}): Promise<GatewayAuthResult> {
const { req, auth, trustedProxies, clients, canvasCapability, malformedScopedPath, rateLimiter } =
params;
const {
req,
auth,
trustedProxies,
allowRealIpFallback,
clients,
canvasCapability,
malformedScopedPath,
rateLimiter,
} = params;
if (malformedScopedPath) {
return { ok: false, reason: "unauthorized" };
}
if (isLocalDirectRequest(req, trustedProxies)) {
if (isLocalDirectRequest(req, trustedProxies, allowRealIpFallback)) {
return { ok: true };
}
@@ -155,6 +164,7 @@ async function authorizeCanvasRequest(params: {
connectAuth: { token, password: token },
req,
trustedProxies,
allowRealIpFallback,
rateLimiter,
});
if (authResult.ok) {
@@ -497,6 +507,7 @@ export function createGatewayHttpServer(opts: {
try {
const configSnapshot = loadConfig();
const trustedProxies = configSnapshot.gateway?.trustedProxies ?? [];
const allowRealIpFallback = configSnapshot.gateway?.allowRealIpFallback === true;
const scopedCanvas = normalizeCanvasScopedUrl(req.url ?? "/");
if (scopedCanvas.malformedScopedPath) {
sendGatewayAuthFailure(res, { ok: false, reason: "unauthorized" });
@@ -513,6 +524,7 @@ export function createGatewayHttpServer(opts: {
await handleToolsInvokeHttpRequest(req, res, {
auth: resolvedAuth,
trustedProxies,
allowRealIpFallback,
rateLimiter,
})
) {
@@ -532,6 +544,7 @@ export function createGatewayHttpServer(opts: {
connectAuth: token ? { token, password: token } : null,
req,
trustedProxies,
allowRealIpFallback,
rateLimiter,
});
if (!authResult.ok) {
@@ -549,6 +562,7 @@ export function createGatewayHttpServer(opts: {
auth: resolvedAuth,
config: openResponsesConfig,
trustedProxies,
allowRealIpFallback,
rateLimiter,
})
) {
@@ -560,6 +574,7 @@ export function createGatewayHttpServer(opts: {
await handleOpenAiHttpRequest(req, res, {
auth: resolvedAuth,
trustedProxies,
allowRealIpFallback,
rateLimiter,
})
) {
@@ -572,6 +587,7 @@ export function createGatewayHttpServer(opts: {
req,
auth: resolvedAuth,
trustedProxies,
allowRealIpFallback,
clients,
canvasCapability: scopedCanvas.capability,
malformedScopedPath: scopedCanvas.malformedScopedPath,
@@ -648,10 +664,12 @@ export function attachGatewayUpgradeHandler(opts: {
if (url.pathname === CANVAS_WS_PATH) {
const configSnapshot = loadConfig();
const trustedProxies = configSnapshot.gateway?.trustedProxies ?? [];
const allowRealIpFallback = configSnapshot.gateway?.allowRealIpFallback === true;
const ok = await authorizeCanvasRequest({
req,
auth: resolvedAuth,
trustedProxies,
allowRealIpFallback,
clients,
canvasCapability: scopedCanvas.capability,
malformedScopedPath: scopedCanvas.malformedScopedPath,

View File

@@ -41,7 +41,7 @@ import {
mintCanvasCapabilityToken,
} from "../../canvas-capability.js";
import { buildDeviceAuthPayload } from "../../device-auth.js";
import { isLoopbackAddress, isTrustedProxyAddress, resolveGatewayClientIp } from "../../net.js";
import { isLoopbackAddress, isTrustedProxyAddress, resolveClientIp } from "../../net.js";
import { resolveHostName } from "../../net.js";
import { resolveNodeCommandAllowlist } from "../../node-command-policy.js";
import { checkBrowserOrigin } from "../../origin-check.js";
@@ -176,7 +176,14 @@ export function attachGatewayWsMessageHandler(params: {
const configSnapshot = loadConfig();
const trustedProxies = configSnapshot.gateway?.trustedProxies ?? [];
const clientIp = resolveGatewayClientIp({ remoteAddr, forwardedFor, realIp, trustedProxies });
const allowRealIpFallback = configSnapshot.gateway?.allowRealIpFallback === true;
const clientIp = resolveClientIp({
remoteAddr,
forwardedFor,
realIp,
trustedProxies,
allowRealIpFallback,
});
// 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
@@ -189,7 +196,7 @@ export function attachGatewayWsMessageHandler(params: {
const hostIsLocal = hostName === "localhost" || hostName === "127.0.0.1" || hostName === "::1";
const hostIsTailscaleServe = hostName.endsWith(".ts.net");
const hostIsLocalish = hostIsLocal || hostIsTailscaleServe;
const isLocalClient = isLocalDirectRequest(upgradeReq, trustedProxies);
const isLocalClient = isLocalDirectRequest(upgradeReq, trustedProxies, allowRealIpFallback);
const reportedClientIp =
isLocalClient || hasUntrustedProxyHeaders
? undefined
@@ -389,6 +396,7 @@ export function attachGatewayWsMessageHandler(params: {
connectAuth: connectParams.auth,
req: upgradeReq,
trustedProxies,
allowRealIpFallback,
rateLimiter: hasDeviceTokenCandidate ? undefined : rateLimiter,
clientIp,
rateLimitScope: AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET,
@@ -424,6 +432,7 @@ export function attachGatewayWsMessageHandler(params: {
connectAuth: connectParams.auth,
req: upgradeReq,
trustedProxies,
allowRealIpFallback,
// Shared-auth probe only; rate-limit side effects are handled in
// the primary auth flow (or deferred for device-token candidates).
rateLimitScope: AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET,

View File

@@ -131,6 +131,7 @@ export async function handleToolsInvokeHttpRequest(
auth: ResolvedGatewayAuth;
maxBodyBytes?: number;
trustedProxies?: string[];
allowRealIpFallback?: boolean;
rateLimiter?: AuthRateLimiter;
},
): Promise<boolean> {
@@ -151,6 +152,7 @@ export async function handleToolsInvokeHttpRequest(
connectAuth: token ? { token, password: token } : null,
req,
trustedProxies: opts.trustedProxies ?? cfg.gateway?.trustedProxies,
allowRealIpFallback: opts.allowRealIpFallback ?? cfg.gateway?.allowRealIpFallback,
rateLimiter: opts.rateLimiter,
});
if (!authResult.ok) {