fix: treat tailnet host as local for pairing

This commit is contained in:
Peter Steinberger
2026-01-21 00:14:09 +00:00
parent e5ea8a0d22
commit c33c0629ec
3 changed files with 62 additions and 5 deletions

41
src/gateway/net.test.ts Normal file
View File

@@ -0,0 +1,41 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
const testTailnetIPv4 = { value: undefined as string | undefined };
const testTailnetIPv6 = { value: undefined as string | undefined };
vi.mock("../infra/tailnet.js", () => ({
pickPrimaryTailnetIPv4: () => testTailnetIPv4.value,
pickPrimaryTailnetIPv6: () => testTailnetIPv6.value,
}));
import { isLocalGatewayAddress } from "./net.js";
describe("gateway net", () => {
beforeEach(() => {
testTailnetIPv4.value = undefined;
testTailnetIPv6.value = undefined;
});
test("treats loopback as local", () => {
expect(isLocalGatewayAddress("127.0.0.1")).toBe(true);
expect(isLocalGatewayAddress("127.0.1.1")).toBe(true);
expect(isLocalGatewayAddress("::1")).toBe(true);
expect(isLocalGatewayAddress("::ffff:127.0.0.1")).toBe(true);
});
test("treats local tailnet IPv4 as local", () => {
testTailnetIPv4.value = "100.64.0.1";
expect(isLocalGatewayAddress("100.64.0.1")).toBe(true);
expect(isLocalGatewayAddress("::ffff:100.64.0.1")).toBe(true);
});
test("ignores non-matching tailnet IPv4", () => {
testTailnetIPv4.value = "100.64.0.1";
expect(isLocalGatewayAddress("100.64.0.2")).toBe(false);
});
test("treats local tailnet IPv6 as local", () => {
testTailnetIPv6.value = "fd7a:115c:a1e0::123";
expect(isLocalGatewayAddress("fd7a:115c:a1e0::123")).toBe(true);
});
});

View File

@@ -1,6 +1,6 @@
import net from "node:net";
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
import { pickPrimaryTailnetIPv4, pickPrimaryTailnetIPv6 } from "../infra/tailnet.js";
export function isLoopbackAddress(ip: string | undefined): boolean {
if (!ip) return false;
@@ -11,6 +11,22 @@ export function isLoopbackAddress(ip: string | undefined): boolean {
return false;
}
function normalizeIPv4MappedAddress(ip: string): string {
if (ip.startsWith("::ffff:")) return ip.slice("::ffff:".length);
return ip;
}
export function isLocalGatewayAddress(ip: string | undefined): boolean {
if (isLoopbackAddress(ip)) return true;
if (!ip) return false;
const normalized = normalizeIPv4MappedAddress(ip.trim().toLowerCase());
const tailnetIPv4 = pickPrimaryTailnetIPv4();
if (tailnetIPv4 && normalized === tailnetIPv4.toLowerCase()) return true;
const tailnetIPv6 = pickPrimaryTailnetIPv6();
if (tailnetIPv6 && ip.trim().toLowerCase() === tailnetIPv6.toLowerCase()) return true;
return false;
}
/**
* Resolves gateway bind host with fallback strategy.
*

View File

@@ -25,7 +25,7 @@ import type { ResolvedGatewayAuth } from "../../auth.js";
import { authorizeGatewayConnect } from "../../auth.js";
import { loadConfig } from "../../../config/config.js";
import { buildDeviceAuthPayload } from "../../device-auth.js";
import { isLoopbackAddress } from "../../net.js";
import { isLocalGatewayAddress } from "../../net.js";
import { resolveNodeCommandAllowlist } from "../../node-command-policy.js";
import {
type ConnectParams,
@@ -347,7 +347,7 @@ export function attachGatewayWsMessageHandler(params: {
close(1008, "device signature expired");
return;
}
const nonceRequired = !isLoopbackAddress(remoteAddr);
const nonceRequired = !isLocalGatewayAddress(remoteAddr);
const providedNonce = typeof device.nonce === "string" ? device.nonce.trim() : "";
if (nonceRequired && !providedNonce) {
setHandshakeState("failed");
@@ -524,7 +524,7 @@ export function attachGatewayWsMessageHandler(params: {
role,
scopes,
remoteIp: remoteAddr,
silent: isLoopbackAddress(remoteAddr),
silent: isLocalGatewayAddress(remoteAddr),
});
const context = buildRequestContext();
if (pairing.request.silent === true) {
@@ -656,7 +656,7 @@ export function attachGatewayWsMessageHandler(params: {
if (presenceKey) {
upsertPresence(presenceKey, {
host: connectParams.client.displayName ?? connectParams.client.id ?? os.hostname(),
ip: isLoopbackAddress(remoteAddr) ? undefined : remoteAddr,
ip: isLocalGatewayAddress(remoteAddr) ? undefined : remoteAddr,
version: connectParams.client.version,
platform: connectParams.client.platform,
deviceFamily: connectParams.client.deviceFamily,