fix(mesh): persist proxy-tunnel self-id across bridge teardown (#1060)

The proxy-mode WS handler called setProxyTunnelSelfCentralNodeId(null)
on tunnel close, dropping the central-namespace nodeId install. After
an idle close, the next inbound forwarder traffic on the peer fell back
to getDefaultNodeId() (=1 on proxy peers without SENCHO_ENROLL_TOKEN),
collided with the alias overlay's nodeId for central's own Local, and
dispatched cross-fleet aliases to the same-node path. The signature was
TCP open + no banner on the prober, with route.resolve.denied on the
peer activity log even though R1's identify install fired correctly
when the bridge was warm.

The peer's identity in central's namespace is enrollment-stable, not
per-WS. Stop null-clearing on teardown. The setter already emits a
console.warn when the install is overwritten with a different non-null
value, which is the right signal for an actual re-enrollment.

Tests: existing assertion that the install becomes null on close is
flipped to assert persistence, plus two new tests for the close-reopen
lifecycle: same nodeId is idempotent (no second identify activity
event), different nodeId emits the overwrite warn. Idempotency test
polls deterministically for the first identify event before snapshotting
the activity-count baseline so the assertion direction can't invert on
a slow CI runner.
This commit is contained in:
Anso
2026-05-15 14:33:57 -04:00
committed by GitHub
parent ce0a61522c
commit f63ffdd098
2 changed files with 90 additions and 6 deletions
@@ -157,16 +157,95 @@ describe('handleMeshProxyTunnel', () => {
const ws = await dialTunnel(srv.port, '?nodeId=14');
await new Promise((r) => setTimeout(r, 20));
expect((MeshService.getInstance() as unknown as { proxyTunnelSelfCentralNodeId: number | null }).proxyTunnelSelfCentralNodeId).toBe(14);
ws.close(1000, 'test cleanup');
await new Promise((r) => setTimeout(r, 30));
// Tunnel close clears the value.
expect((MeshService.getInstance() as unknown as { proxyTunnelSelfCentralNodeId: number | null }).proxyTunnelSelfCentralNodeId).toBeNull();
} finally {
await srv.close();
}
});
it('the install persists across WS close (enrollment-stable, not per-bridge)', async () => {
// Null-clearing on teardown previously caused handleAccept to fall
// back to getDefaultNodeId() after idle close, misdispatching
// cross-fleet aliases to the same-node path.
const srv = await startServer();
try {
const ws = await dialTunnel(srv.port, '?nodeId=14');
await new Promise((r) => setTimeout(r, 20));
ws.close(1000, 'test cleanup');
await new Promise((r) => setTimeout(r, 30));
expect((MeshService.getInstance() as unknown as { proxyTunnelSelfCentralNodeId: number | null }).proxyTunnelSelfCentralNodeId).toBe(14);
} finally {
await srv.close();
}
});
it('re-opening the tunnel with the same nodeId is idempotent (no second identify event)', async () => {
const srv = await startServer();
const svc = MeshService.getInstance();
const identifyCount = () => svc.getActivity({ limit: 200 })
.filter((e) => e.type === 'mesh.proxy_tunnel.identify').length;
const waitForCount = async (atLeast: number, timeoutMs = 500): Promise<void> => {
const start = Date.now();
while (Date.now() - start < timeoutMs && identifyCount() < atLeast) {
await new Promise((r) => setTimeout(r, 5));
}
};
try {
const wsA = await dialTunnel(srv.port, '?nodeId=14');
// Snapshot after the first identify event has deterministically
// landed, so the race window between WS-open and async install
// can't make beforeCount=0 on a slow CI.
await waitForCount(1);
const beforeCount = identifyCount();
expect(beforeCount).toBeGreaterThanOrEqual(1);
wsA.close(1000, 'test cleanup');
await new Promise((r) => setTimeout(r, 30));
const wsB = await dialTunnel(srv.port, '?nodeId=14');
// Settle window for any (incorrect) second identify; small but
// deterministic because the assertion below is "no new event"
// and a longer wait wouldn't change the answer.
await new Promise((r) => setTimeout(r, 50));
expect((svc as unknown as { proxyTunnelSelfCentralNodeId: number | null }).proxyTunnelSelfCentralNodeId).toBe(14);
expect(identifyCount()).toBe(beforeCount);
wsB.close(1000, 'test cleanup');
await new Promise((r) => setTimeout(r, 30));
} finally {
await srv.close();
}
});
it('re-opening the tunnel with a different nodeId emits the overwrite warn (re-enrollment signal)', async () => {
const srv = await startServer();
const warns: string[] = [];
const origWarn = console.warn;
console.warn = (...args: unknown[]) => { warns.push(args.map(String).join(' ')); };
try {
const wsA = await dialTunnel(srv.port, '?nodeId=14');
await new Promise((r) => setTimeout(r, 20));
wsA.close(1000, 'test cleanup');
await new Promise((r) => setTimeout(r, 30));
const wsB = await dialTunnel(srv.port, '?nodeId=15');
await new Promise((r) => setTimeout(r, 20));
const svc = MeshService.getInstance();
expect((svc as unknown as { proxyTunnelSelfCentralNodeId: number | null }).proxyTunnelSelfCentralNodeId).toBe(15);
const overwriteWarns = warns.filter((w) => w.includes('proxyTunnelSelfCentralNodeId overwritten'));
expect(overwriteWarns.length).toBeGreaterThanOrEqual(1);
expect(overwriteWarns[overwriteWarns.length - 1]).toContain('14 -> 15');
wsB.close(1000, 'test cleanup');
await new Promise((r) => setTimeout(r, 30));
} finally {
console.warn = origWarn;
await srv.close();
}
});
it('does not install or reject when the nodeId query param is missing (warns and proceeds)', async () => {
const srv = await startServer();
try {
+8 -3
View File
@@ -114,14 +114,19 @@ async function attachSwitchboard(ws: WebSocket, peerNodeId: number | null): Prom
// Install central's view of this peer's nodeId so handleAccept
// dispatches cross-node aliases correctly. Done after setReverseDialer
// succeeds so a CAS-rejected tunnel does not leak nodeId state.
//
// The install persists across bridge lifecycles: the peer's identity
// in central's namespace is stable for the enrollment, not per-WS.
// Null-clearing on teardown caused dispatch to fall back to
// getDefaultNodeId() after idle close, which collides with central's
// own nodeId for Local and misdispatches cross-fleet traffic to the
// same-node path. A subsequent install with a different nodeId
// (e.g. re-enrollment) is detected by the setter's overwrite-warn.
if (peerNodeId !== null) {
meshService.setProxyTunnelSelfCentralNodeId(peerNodeId);
}
meshServiceCleanup = () => {
meshService.setReverseDialer(null, localDialer);
if (peerNodeId !== null) {
meshService.setProxyTunnelSelfCentralNodeId(null);
}
};
} catch (err) {
if (isDebugEnabled()) {