fix: add explicit tailnet gateway bind

This commit is contained in:
Peter Steinberger
2026-01-21 20:35:39 +00:00
parent 45c1ccdfcf
commit b5fd66c92d
30 changed files with 143 additions and 71 deletions

View File

@@ -72,14 +72,14 @@ describe("callGateway url resolution", () => {
closeReason = "";
});
it("uses tailnet IP when local bind is auto and tailnet is present", async () => {
it("keeps loopback when local bind is auto even if tailnet is present", async () => {
loadConfig.mockReturnValue({ gateway: { mode: "local", bind: "auto" } });
resolveGatewayPort.mockReturnValue(18800);
pickPrimaryTailnetIPv4.mockReturnValue("100.64.0.1");
await callGateway({ method: "health" });
expect(lastClientOptions?.url).toBe("ws://100.64.0.1:18800");
expect(lastClientOptions?.url).toBe("ws://127.0.0.1:18800");
});
it("falls back to loopback when local bind is auto without tailnet IP", async () => {
@@ -92,6 +92,16 @@ describe("callGateway url resolution", () => {
expect(lastClientOptions?.url).toBe("ws://127.0.0.1:18800");
});
it("uses tailnet IP when local bind is tailnet and tailnet is present", async () => {
loadConfig.mockReturnValue({ gateway: { mode: "local", bind: "tailnet" } });
resolveGatewayPort.mockReturnValue(18800);
pickPrimaryTailnetIPv4.mockReturnValue("100.64.0.1");
await callGateway({ method: "health" });
expect(lastClientOptions?.url).toBe("ws://100.64.0.1:18800");
});
it("uses url override in remote mode even when remote url is missing", async () => {
loadConfig.mockReturnValue({
gateway: { mode: "remote", bind: "loopback", remote: {} },

View File

@@ -63,7 +63,7 @@ export function buildGatewayConnectionDetails(
const localPort = resolveGatewayPort(config);
const tailnetIPv4 = pickPrimaryTailnetIPv4();
const bindMode = config.gateway?.bind ?? "loopback";
const preferTailnet = bindMode === "auto" && !!tailnetIPv4;
const preferTailnet = bindMode === "tailnet" && !!tailnetIPv4;
const scheme = tlsEnabled ? "wss" : "ws";
const localUrl =
preferTailnet && tailnetIPv4

View File

@@ -33,7 +33,8 @@ export function isLocalGatewayAddress(ip: string | undefined): boolean {
* Modes:
* - loopback: 127.0.0.1 (rarely fails, but handled gracefully)
* - lan: always 0.0.0.0 (no fallback)
* - auto: Tailnet IPv4 if available, else 0.0.0.0
* - tailnet: Tailnet IPv4 if available, else loopback
* - auto: Loopback if available, else 0.0.0.0
* - custom: User-specified IP, fallback to 0.0.0.0 if unavailable
*
* @returns The bind address to use (never null)
@@ -50,6 +51,13 @@ export async function resolveGatewayBindHost(
return "0.0.0.0"; // extreme fallback
}
if (mode === "tailnet") {
const tailnetIP = pickPrimaryTailnetIPv4();
if (tailnetIP && (await canBindTo(tailnetIP))) return tailnetIP;
if (await canBindTo("127.0.0.1")) return "127.0.0.1";
return "0.0.0.0";
}
if (mode === "lan") {
return "0.0.0.0";
}
@@ -64,8 +72,7 @@ export async function resolveGatewayBindHost(
}
if (mode === "auto") {
const tailnetIP = pickPrimaryTailnetIPv4();
if (tailnetIP && (await canBindTo(tailnetIP))) return tailnetIP;
if (await canBindTo("127.0.0.1")) return "127.0.0.1";
return "0.0.0.0";
}

View File

@@ -97,7 +97,7 @@ export type GatewayServerOptions = {
* - loopback: 127.0.0.1
* - lan: 0.0.0.0
* - tailnet: bind only to the Tailscale IPv4 address (100.64.0.0/10)
* - auto: prefer tailnet, else LAN
* - auto: prefer loopback, else LAN
*/
bind?: import("../config/config.js").GatewayBindMode;
/**