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(
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 {

View File

@@ -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", () => {

View File

@@ -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 {