From f9b775346a796ef1e7b35e7f151700c8ee2ab124 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Wed, 9 Sep 2026 23:29:06 +0100 Subject: [PATCH 1/2] fix: read the OS-assigned port back from the listener in becomeCoordinator becomeCoordinator stored the port parameter it was called with instead of reading server.address() the way addListener already does. This only matters when called with port 0 for an OS-assigned free port: the listener's actual bound port was silently discarded and listListeners() reported 0 instead of the real port, breaking anything that reads the listener list to find where to connect. Applies to both TlsTransport and TcpTransport; WsTransport has no equivalent listener-list bookkeeping to be affected. --- src/core/tcp-transport.ts | 5 ++++- src/core/tls-transport.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/core/tcp-transport.ts b/src/core/tcp-transport.ts index 602ec01..d833d3e 100644 --- a/src/core/tcp-transport.ts +++ b/src/core/tcp-transport.ts @@ -314,12 +314,15 @@ export class TcpTransport implements MeshTransport { }); server.listen(port, host, () => { + const addr = server.address(); + const actualPort = + typeof addr === "object" && addr !== null ? addr.port : port; this._isCoordinator = true; this.coordinatorListeners.set(id, { server, policy: "full", host, - port, + port: actualPort, isDefault: true, }); this.defaultListenerId = id; diff --git a/src/core/tls-transport.ts b/src/core/tls-transport.ts index 3822bd2..42388c8 100644 --- a/src/core/tls-transport.ts +++ b/src/core/tls-transport.ts @@ -405,12 +405,15 @@ export class TlsTransport { }); server.listen(port, host, () => { + const addr = server.address(); + const actualPort = + typeof addr === "object" && addr !== null ? addr.port : port; this._isCoordinator = true; this.coordinatorListeners.set(id, { server, policy: "full", host, - port, + port: actualPort, isDefault: true, }); this.defaultListenerId = id; From 6cb003477ace199318abe4ee7c24e1a0b63a2ba7 Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Wed, 9 Sep 2026 23:29:12 +0100 Subject: [PATCH 2/2] test: cover becomeCoordinator's reported port against an OS-assigned bind Calls becomeCoordinator("127.0.0.1", 0) directly on both TlsTransport and TcpTransport, then asserts listListeners() reports a non-zero port and that a real connection to that reported port succeeds. --- package.json | 2 +- ...oordinator-actual-port.integration.test.ts | 122 ++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/test/become-coordinator-actual-port.integration.test.ts diff --git a/package.json b/package.json index fd1a319..ffc5cc7 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "generate:server-json": "tsx scripts/sync-release-metadata.ts", "publish:mcp-registry": "tsx scripts/publish-mcp-registry.ts", "lint": "eslint .", - "test": "node --test --test-concurrency=1 dist/test/coordinator-socket-error.integration.test.js dist/test/identity-store.test.js dist/test/identity-cert.test.js dist/test/broadcast-window.integration.test.js dist/test/identity-restart.integration.test.js dist/test/mesh-e2e.integration.test.js dist/test/ws-broadcast-window.integration.test.js dist/test/tls-transport.integration.test.js dist/test/peer-id-verification.integration.test.js dist/test/state-sync-convergence.test.js dist/test/downtime-replay.test.js dist/test/downtime-replay.integration.test.js dist/test/filestore.test.js", + "test": "node --test --test-concurrency=1 dist/test/coordinator-socket-error.integration.test.js dist/test/identity-store.test.js dist/test/identity-cert.test.js dist/test/broadcast-window.integration.test.js dist/test/identity-restart.integration.test.js dist/test/mesh-e2e.integration.test.js dist/test/ws-broadcast-window.integration.test.js dist/test/tls-transport.integration.test.js dist/test/peer-id-verification.integration.test.js dist/test/become-coordinator-actual-port.integration.test.js dist/test/state-sync-convergence.test.js dist/test/downtime-replay.test.js dist/test/downtime-replay.integration.test.js dist/test/filestore.test.js", "test:visibility": "node --test dist/test/visibility.integration.test.js", "test:delivery": "node dist/test/delivery-receipt.runner.js", "test:federation": "node dist/test/federation.integration.test.js", diff --git a/src/test/become-coordinator-actual-port.integration.test.ts b/src/test/become-coordinator-actual-port.integration.test.ts new file mode 100644 index 0000000..734524c --- /dev/null +++ b/src/test/become-coordinator-actual-port.integration.test.ts @@ -0,0 +1,122 @@ +/** + * becomeCoordinator actual-port regression test (#42) — becomeCoordinator(host, 0) must report the OS-assigned port it actually bound, not the literal 0 it was called with, on every transport whose listener bookkeeping goes through listListeners(). + * + * Run: node dist/test/become-coordinator-actual-port.integration.test.js [test-name] With no argument, every scenario runs in order. + */ + +import * as net from "node:net"; +import * as tls from "node:tls"; +import * as assert from "node:assert/strict"; +import { TlsTransport } from "../core/tls-transport.js"; +import { TcpTransport } from "../core/tcp-transport.js"; +import { generateIdentity } from "../core/identity.js"; +import type { TransportEvents } from "../core/transport.js"; + +function noopEvents(): TransportEvents { + return { + onMessage: () => undefined, + onPeerConnected: () => undefined, + onPeerDisconnected: () => undefined, + onIntroduction: () => undefined, + onConnectionRequest: () => undefined, + onPeerList: () => undefined, + onPeerJoined: () => undefined, + onBecomeCoordinator: () => undefined, + }; +} + +async function testTlsReportsActualPort(): Promise { + const identity = generateIdentity(); + const transport = new TlsTransport(noopEvents(), identity); + + await transport.becomeCoordinator("127.0.0.1", 0); + const [listener] = transport.listListeners(); + assert.ok(listener); + assert.notStrictEqual(listener.port, 0); + + await new Promise((resolve, reject) => { + const socket = tls.connect( + { host: "127.0.0.1", port: listener.port, rejectUnauthorized: false }, + () => { + socket.destroy(); + resolve(); + }, + ); + socket.once("error", reject); + }); + console.log( + ` ✓ TlsTransport reported and bound the same port (${listener.port})`, + ); + + await transport.shutdown(); +} + +async function testTcpReportsActualPort(): Promise { + const transport = new TcpTransport(noopEvents()); + + await transport.becomeCoordinator("127.0.0.1", 0); + const [listener] = transport.listListeners(); + assert.ok(listener); + assert.notStrictEqual(listener.port, 0); + + await new Promise((resolve, reject) => { + const socket = net.connect( + { host: "127.0.0.1", port: listener.port }, + () => { + socket.destroy(); + resolve(); + }, + ); + socket.once("error", reject); + }); + console.log( + ` ✓ TcpTransport reported and bound the same port (${listener.port})`, + ); + + await transport.shutdown(); +} + +// --------------------------------------------------------------------------- +// Runner +// --------------------------------------------------------------------------- + +const testName = process.argv[2]; + +const tests: Record Promise> = { + "tls-reports-actual-port": testTlsReportsActualPort, + "tcp-reports-actual-port": testTcpReportsActualPort, +}; + +const selected = + testName === undefined + ? Object.entries(tests) + : Object.entries(tests).filter(([name]) => name === testName); +if (selected.length === 0) { + console.error(`Unknown test: ${testName}`); + console.error(`Available: ${Object.keys(tests).join(", ")}`); + process.exit(1); +} + +async function run(): Promise { + for (const [name, fn] of selected) { + console.log(`Running ${name}:`); + await fn(); + } + + const maxWait = 2000; + const start = Date.now(); + while ( + (( + process as unknown as { _getActiveHandles?: () => unknown[] } + )._getActiveHandles?.()?.length ?? 0) > 0 && + Date.now() - start < maxWait + ) { + await new Promise((resolve) => setTimeout(resolve, 50)); + } + process.exit(0); +} + +run().catch((err: unknown) => { + console.error(`FAIL [${testName ?? "all"}]:`, err); + process.exit(1); +});