fix: bridge tailnet bind also listens on loopback

This commit is contained in:
Peter Steinberger
2025-12-25 01:37:47 +00:00
parent dc93350e0a
commit 81e11c1d91
4 changed files with 92 additions and 9 deletions

View File

@@ -49,6 +49,17 @@ function sendLine(socket: net.Socket, obj: unknown) {
describe("node bridge server", () => {
let baseDir = "";
const pickNonLoopbackIPv4 = () => {
const ifaces = os.networkInterfaces();
for (const entries of Object.values(ifaces)) {
for (const info of entries ?? []) {
if (info.family === "IPv4" && info.internal === false)
return info.address;
}
}
return null;
};
beforeAll(async () => {
process.env.CLAWDIS_ENABLE_BRIDGE_IN_TESTS = "1";
baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-bridge-test-"));
@@ -77,6 +88,31 @@ describe("node bridge server", () => {
await server.close();
});
it("also listens on loopback when bound to a non-loopback host", async () => {
const host = pickNonLoopbackIPv4();
if (!host) return;
const server = await startNodeBridgeServer({
host,
port: 0,
pairingBaseDir: baseDir,
});
const socket = net.connect({ host: "127.0.0.1", port: server.port });
await new Promise<void>((resolve, reject) => {
socket.once("connect", resolve);
socket.once("error", reject);
});
const readLine = createLineReader(socket);
sendLine(socket, { type: "hello", nodeId: "n-loopback" });
const line = await readLine();
const msg = JSON.parse(line) as { type: string; code?: string };
expect(msg.type).toBe("error");
expect(msg.code).toBe("NOT_PAIRED");
socket.destroy();
await server.close();
});
it("pairs after approval and then accepts hello", async () => {
const server = await startNodeBridgeServer({
host: "127.0.0.1",

View File

@@ -209,7 +209,21 @@ export async function startNodeBridgeServer(
const connections = new Map<string, ConnectionState>();
const server = net.createServer((socket) => {
const shouldAlsoListenOnLoopback = (host: string | undefined) => {
const h = String(host ?? "")
.trim()
.toLowerCase();
if (!h) return false; // default listen() already includes loopback
if (h === "0.0.0.0" || h === "::") return false; // already includes loopback
if (h === "localhost") return false;
if (h === "127.0.0.1" || h.startsWith("127.")) return false;
if (h === "::1") return false;
return true;
};
const loopbackHost = "127.0.0.1";
const onConnection = (socket: net.Socket) => {
socket.setNoDelay(true);
let buffer = "";
@@ -656,17 +670,45 @@ export async function startNodeBridgeServer(
socket.on("error", () => {
// close handler will run after close
});
});
};
const servers: net.Server[] = [];
const primary = net.createServer(onConnection);
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen(opts.port, opts.host, () => resolve());
const onError = (err: Error) => reject(err);
primary.once("error", onError);
primary.listen(opts.port, opts.host, () => {
primary.off("error", onError);
resolve();
});
});
servers.push(primary);
const address = server.address();
const address = primary.address();
const port =
typeof address === "object" && address ? address.port : opts.port;
if (shouldAlsoListenOnLoopback(opts.host)) {
const loopback = net.createServer(onConnection);
try {
await new Promise<void>((resolve, reject) => {
const onError = (err: Error) => reject(err);
loopback.once("error", onError);
loopback.listen(port, loopbackHost, () => {
loopback.off("error", onError);
resolve();
});
});
servers.push(loopback);
} catch {
try {
loopback.close();
} catch {
/* ignore */
}
}
}
return {
port,
close: async () => {
@@ -678,8 +720,13 @@ export async function startNodeBridgeServer(
}
}
connections.clear();
await new Promise<void>((resolve, reject) =>
server.close((err) => (err ? reject(err) : resolve())),
await Promise.all(
servers.map(
(s) =>
new Promise<void>((resolve, reject) =>
s.close((err) => (err ? reject(err) : resolve())),
),
),
);
},
listConnected: () => [...connections.values()].map((c) => c.nodeInfo),