From 719f3040093dd979e8fa78c2054d227c86eb989c Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 25 Mar 2026 09:52:07 -0700 Subject: [PATCH] fix(gateway): align trusted-proxy loopback validation --- src/gateway/auth.ts | 11 +++------ src/gateway/server-runtime-config.test.ts | 28 +++++++++++++---------- src/gateway/server-runtime-config.ts | 17 +------------- 3 files changed, 20 insertions(+), 36 deletions(-) diff --git a/src/gateway/auth.ts b/src/gateway/auth.ts index b9abb8ba97e..51d666c3e48 100644 --- a/src/gateway/auth.ts +++ b/src/gateway/auth.ts @@ -114,8 +114,8 @@ function resolveTailscaleClientIp(req?: IncomingMessage): string | undefined { export function isLocalDirectRequest( req?: IncomingMessage, - trustedProxies?: string[], - allowRealIpFallback = false, + _trustedProxies?: string[], + _allowRealIpFallback = false, ): boolean { if (!req) { return false; @@ -132,12 +132,7 @@ export function isLocalDirectRequest( if (!hasForwarded) { return isLoopbackAddress(req.socket?.remoteAddress); } - - // When forwarded headers are present, resolveRequestClientIp intentionally fails closed - // if the proxy chain is missing or invalid. Do not fall back to the raw socket address here, - // or proxied requests can be reclassified as local-direct. - const clientIp = resolveRequestClientIp(req, trustedProxies, allowRealIpFallback) ?? ""; - return isLoopbackAddress(clientIp); + return false; } function getTailscaleUser(req?: IncomingMessage): TailscaleUser | null { diff --git a/src/gateway/server-runtime-config.test.ts b/src/gateway/server-runtime-config.test.ts index 5c1354d7cd5..2fcffcea1ad 100644 --- a/src/gateway/server-runtime-config.test.ts +++ b/src/gateway/server-runtime-config.test.ts @@ -76,18 +76,6 @@ describe("resolveGatewayRuntimeConfig", () => { expectedMessage: "gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured", }, - { - name: "loopback binding without loopback trusted proxy", - cfg: { - gateway: { - bind: "loopback" as const, - auth: TRUSTED_PROXY_AUTH, - trustedProxies: ["10.0.0.1"], - }, - }, - expectedMessage: - "gateway auth mode=trusted-proxy with bind=loopback requires gateway.trustedProxies to include 127.0.0.1, ::1, or a loopback CIDR", - }, { name: "lan binding without trusted proxies", cfg: { @@ -106,6 +94,22 @@ describe("resolveGatewayRuntimeConfig", () => { expectedMessage, ); }); + + it("allows loopback binding with non-loopback trusted proxies", async () => { + const result = await resolveGatewayRuntimeConfig({ + cfg: { + gateway: { + bind: "loopback", + auth: TRUSTED_PROXY_AUTH, + trustedProxies: ["10.0.0.1"], + }, + }, + port: 18789, + }); + + expect(result.authMode).toBe("trusted-proxy"); + expect(result.bindHost).toBe("127.0.0.1"); + }); }); describe("token/password auth modes", () => { diff --git a/src/gateway/server-runtime-config.ts b/src/gateway/server-runtime-config.ts index 6262208eeaf..f7cc5d1718f 100644 --- a/src/gateway/server-runtime-config.ts +++ b/src/gateway/server-runtime-config.ts @@ -11,12 +11,7 @@ import { } from "./auth.js"; import { normalizeControlUiBasePath } from "./control-ui-shared.js"; import { resolveHooksConfig } from "./hooks.js"; -import { - isLoopbackHost, - isTrustedProxyAddress, - isValidIPv4, - resolveGatewayBindHost, -} from "./net.js"; +import { isLoopbackHost, isValidIPv4, resolveGatewayBindHost } from "./net.js"; import { mergeGatewayTailscaleConfig } from "./startup-auth.js"; export type GatewayRuntimeConfig = { @@ -152,16 +147,6 @@ export async function resolveGatewayRuntimeConfig(params: { "gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured with at least one proxy IP", ); } - if (isLoopbackHost(bindHost)) { - const hasLoopbackTrustedProxy = - isTrustedProxyAddress("127.0.0.1", trustedProxies) || - isTrustedProxyAddress("::1", trustedProxies); - if (!hasLoopbackTrustedProxy) { - throw new Error( - "gateway auth mode=trusted-proxy with bind=loopback requires gateway.trustedProxies to include 127.0.0.1, ::1, or a loopback CIDR", - ); - } - } } return {