import net from "node:net"; import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js"; export function isLoopbackAddress(ip: string | undefined): boolean { if (!ip) return false; if (ip === "127.0.0.1") return true; if (ip.startsWith("127.")) return true; if (ip === "::1") return true; if (ip.startsWith("::ffff:127.")) return true; return false; } /** * Resolves gateway bind host with fallback strategy. * * 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 * - custom: User-specified IP, fallback to 0.0.0.0 if unavailable * * @returns The bind address to use (never null) */ export async function resolveGatewayBindHost( bind: import("../config/config.js").BridgeBindMode | undefined, customHost?: string, ): Promise { const mode = bind ?? "loopback"; if (mode === "loopback") { // 127.0.0.1 rarely fails, but handle gracefully if (await canBindTo("127.0.0.1")) return "127.0.0.1"; return "0.0.0.0"; // extreme fallback } if (mode === "lan") { return "0.0.0.0"; } if (mode === "custom") { const host = customHost?.trim(); if (!host) return "0.0.0.0"; // invalid config → fall back to all if (isValidIPv4(host) && (await canBindTo(host))) return host; // Custom IP failed → fall back to LAN return "0.0.0.0"; } if (mode === "auto") { const tailnetIP = pickPrimaryTailnetIPv4(); if (tailnetIP && (await canBindTo(tailnetIP))) return tailnetIP; return "0.0.0.0"; } return "0.0.0.0"; } /** * Test if we can bind to a specific host address. * Creates a temporary server, attempts to bind, then closes it. * * @param host - The host address to test * @returns True if we can successfully bind to this address */ async function canBindTo(host: string): Promise { return new Promise((resolve) => { const testServer = net.createServer(); testServer.once("error", () => { resolve(false); }); testServer.once("listening", () => { testServer.close(); resolve(true); }); // Use port 0 to let OS pick an available port for testing testServer.listen(0, host); }); } /** * Validate if a string is a valid IPv4 address. * * @param host - The string to validate * @returns True if valid IPv4 format */ function isValidIPv4(host: string): boolean { const parts = host.split("."); if (parts.length !== 4) return false; return parts.every((part) => { const n = parseInt(part, 10); return !Number.isNaN(n) && n >= 0 && n <= 255 && part === String(n); }); } export function isLoopbackHost(host: string): boolean { return isLoopbackAddress(host); }