fix(gateway): align trusted-proxy loopback validation

This commit is contained in:
Vincent Koc
2026-03-25 09:52:07 -07:00
parent 07a5e809b5
commit 719f304009
3 changed files with 20 additions and 36 deletions

View File

@@ -114,8 +114,8 @@ function resolveTailscaleClientIp(req?: IncomingMessage): string | undefined {
export function isLocalDirectRequest( export function isLocalDirectRequest(
req?: IncomingMessage, req?: IncomingMessage,
trustedProxies?: string[], _trustedProxies?: string[],
allowRealIpFallback = false, _allowRealIpFallback = false,
): boolean { ): boolean {
if (!req) { if (!req) {
return false; return false;
@@ -132,12 +132,7 @@ export function isLocalDirectRequest(
if (!hasForwarded) { if (!hasForwarded) {
return isLoopbackAddress(req.socket?.remoteAddress); return isLoopbackAddress(req.socket?.remoteAddress);
} }
return false;
// 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);
} }
function getTailscaleUser(req?: IncomingMessage): TailscaleUser | null { function getTailscaleUser(req?: IncomingMessage): TailscaleUser | null {

View File

@@ -76,18 +76,6 @@ describe("resolveGatewayRuntimeConfig", () => {
expectedMessage: expectedMessage:
"gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured", "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", name: "lan binding without trusted proxies",
cfg: { cfg: {
@@ -106,6 +94,22 @@ describe("resolveGatewayRuntimeConfig", () => {
expectedMessage, 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", () => { describe("token/password auth modes", () => {

View File

@@ -11,12 +11,7 @@ import {
} from "./auth.js"; } from "./auth.js";
import { normalizeControlUiBasePath } from "./control-ui-shared.js"; import { normalizeControlUiBasePath } from "./control-ui-shared.js";
import { resolveHooksConfig } from "./hooks.js"; import { resolveHooksConfig } from "./hooks.js";
import { import { isLoopbackHost, isValidIPv4, resolveGatewayBindHost } from "./net.js";
isLoopbackHost,
isTrustedProxyAddress,
isValidIPv4,
resolveGatewayBindHost,
} from "./net.js";
import { mergeGatewayTailscaleConfig } from "./startup-auth.js"; import { mergeGatewayTailscaleConfig } from "./startup-auth.js";
export type GatewayRuntimeConfig = { 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", "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 { return {