fix(mesh): fix pilot handleAccept dispatch by deriving self nodeId from enrollment token (#1023)

On a pilot node, handleAccept compared target.nodeId (from central's
perspective via pilotAliasOverlay) against NodeRegistry.getDefaultNodeId()
which always returns 1. This inverted same-node vs cross-node dispatch:

- central's alias (nodeId=1) matched localNodeId=1 -> openSameNode on pilot
  (no container -> silent close, 0 bytes)
- pilot's own alias (nodeId=14) != 1 -> openCrossNode -> tunnel loop

Fix: add resolveSelfCentralNodeId() that decodes the nodeId claim from
SENCHO_ENROLL_TOKEN (present on every pilot; payload decode only, no
signature verification). Returns getDefaultNodeId() on central (env unset).
Cached in selfCentralNodeId at start(); handleAccept reads the cache.

After this fix the reverse direction (pilot-prober -> central echo) routes
through openCrossNode (reverse tunnel) and the same-node path (pilot-prober
-> pilot echo) routes through openSameNode (local Dockerode dial). Closes
BUG-3 banner data-path regression.

Tests: 5 new cases covering resolveSelfCentralNodeId token extraction,
fallback paths, and handleAccept dispatch routing on a simulated pilot.
This commit is contained in:
Anso
2026-05-10 03:09:53 -04:00
committed by GitHub
parent 4d4856536b
commit 41a1df279d
2 changed files with 124 additions and 3 deletions
+29 -3
View File
@@ -189,6 +189,12 @@ export class MeshService extends EventEmitter implements MeshForwarderHost {
private senchoIp: string | null = null;
private meshSubnet: string = DEFAULT_MESH_SUBNET;
private networkSetupError: string | null = null;
// On a pilot node, central's DB id for this node (e.g. 14). Used by
// handleAccept to decide same-node vs cross-node; the pilotAliasOverlay
// carries nodeIds from central's perspective, so comparing against the
// pilot's own local DB id (always 1) inverts dispatch. Null on central
// (fallback to getDefaultNodeId()). Populated from SENCHO_ENROLL_TOKEN.
private selfCentralNodeId: number | null = null;
private constructor() {
super();
@@ -201,6 +207,24 @@ export class MeshService extends EventEmitter implements MeshForwarderHost {
return MeshService.instance;
}
private resolveSelfCentralNodeId(): number {
const tok = process.env.SENCHO_ENROLL_TOKEN;
if (tok) {
try {
// Extract payload only — signature verification not needed here;
// we only need the nodeId claim, not auth.
const [, b64] = tok.split('.');
const payload = JSON.parse(
Buffer.from(b64, 'base64url').toString('utf8'),
) as Record<string, unknown>;
if (typeof payload.nodeId === 'number') return payload.nodeId;
} catch {
// Malformed token; fall through to local default.
}
}
return NodeRegistry.getInstance().getDefaultNodeId();
}
public async start(): Promise<void> {
if (this.started) return;
this.started = true;
@@ -227,6 +251,8 @@ export class MeshService extends EventEmitter implements MeshForwarderHost {
});
});
this.selfCentralNodeId = this.resolveSelfCentralNodeId();
await this.setupMeshNetwork();
try {
await this.refreshAliasCache();
@@ -259,7 +285,7 @@ export class MeshService extends EventEmitter implements MeshForwarderHost {
const dataPlane = this.senchoIp ? 'ok' : `unavailable (${this.networkSetupError ?? 'unknown'})`;
this.logActivity({
source: 'mesh', level: this.senchoIp ? 'info' : 'warn', type: 'mesh.enable',
message: `MeshService started (data plane ${dataPlane})`,
message: `MeshService started (data plane ${dataPlane}, self nodeId ${this.selfCentralNodeId})`,
});
}
@@ -1208,8 +1234,8 @@ export class MeshService extends EventEmitter implements MeshForwarderHost {
try { src.destroy(); } catch { /* ignore */ }
return;
}
const localNodeId = NodeRegistry.getInstance().getDefaultNodeId();
if (target.nodeId === localNodeId) {
const selfNodeId = this.selfCentralNodeId ?? NodeRegistry.getInstance().getDefaultNodeId();
if (target.nodeId === selfNodeId) {
await this.openSameNode(target, src);
} else {
this.openCrossNode(target, src);