fix: treat tailnet host as local for pairing
This commit is contained in:
41
src/gateway/net.test.ts
Normal file
41
src/gateway/net.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import net from "node:net";
|
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 {
|
export function isLoopbackAddress(ip: string | undefined): boolean {
|
||||||
if (!ip) return false;
|
if (!ip) return false;
|
||||||
@@ -11,6 +11,22 @@ export function isLoopbackAddress(ip: string | undefined): boolean {
|
|||||||
return false;
|
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.
|
* Resolves gateway bind host with fallback strategy.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import type { ResolvedGatewayAuth } from "../../auth.js";
|
|||||||
import { authorizeGatewayConnect } from "../../auth.js";
|
import { authorizeGatewayConnect } from "../../auth.js";
|
||||||
import { loadConfig } from "../../../config/config.js";
|
import { loadConfig } from "../../../config/config.js";
|
||||||
import { buildDeviceAuthPayload } from "../../device-auth.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 { resolveNodeCommandAllowlist } from "../../node-command-policy.js";
|
||||||
import {
|
import {
|
||||||
type ConnectParams,
|
type ConnectParams,
|
||||||
@@ -347,7 +347,7 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
close(1008, "device signature expired");
|
close(1008, "device signature expired");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nonceRequired = !isLoopbackAddress(remoteAddr);
|
const nonceRequired = !isLocalGatewayAddress(remoteAddr);
|
||||||
const providedNonce = typeof device.nonce === "string" ? device.nonce.trim() : "";
|
const providedNonce = typeof device.nonce === "string" ? device.nonce.trim() : "";
|
||||||
if (nonceRequired && !providedNonce) {
|
if (nonceRequired && !providedNonce) {
|
||||||
setHandshakeState("failed");
|
setHandshakeState("failed");
|
||||||
@@ -524,7 +524,7 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
role,
|
role,
|
||||||
scopes,
|
scopes,
|
||||||
remoteIp: remoteAddr,
|
remoteIp: remoteAddr,
|
||||||
silent: isLoopbackAddress(remoteAddr),
|
silent: isLocalGatewayAddress(remoteAddr),
|
||||||
});
|
});
|
||||||
const context = buildRequestContext();
|
const context = buildRequestContext();
|
||||||
if (pairing.request.silent === true) {
|
if (pairing.request.silent === true) {
|
||||||
@@ -656,7 +656,7 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
if (presenceKey) {
|
if (presenceKey) {
|
||||||
upsertPresence(presenceKey, {
|
upsertPresence(presenceKey, {
|
||||||
host: connectParams.client.displayName ?? connectParams.client.id ?? os.hostname(),
|
host: connectParams.client.displayName ?? connectParams.client.id ?? os.hostname(),
|
||||||
ip: isLoopbackAddress(remoteAddr) ? undefined : remoteAddr,
|
ip: isLocalGatewayAddress(remoteAddr) ? undefined : remoteAddr,
|
||||||
version: connectParams.client.version,
|
version: connectParams.client.version,
|
||||||
platform: connectParams.client.platform,
|
platform: connectParams.client.platform,
|
||||||
deviceFamily: connectParams.client.deviceFamily,
|
deviceFamily: connectParams.client.deviceFamily,
|
||||||
|
|||||||
Reference in New Issue
Block a user