fix(mesh): bind every alias port on every node, not just local-owned (#1004)

syncForwarderListeners filtered the bind set to ports owned by the
local node. That broke cross-node mesh routing end to end: meshed
containers' extra_hosts: <alias>:host-gateway entries resolve to the
SOURCE node's gateway, so the source node is where the inbound TCP
connection lands. With the filter, the source node never bound the
remote alias's port and the connection went nowhere.

The architecture model documented in docs/internal/architecture/
mesh.md (data flow step 3) is explicit: monolith's Sencho process
accepts the connection on its MeshForwarder listener bound to port
5432 — where monolith is the source and opsix is the target. Every
meshed node binds every alias port. handleAccept then resolves the
alias and dispatches to openSameNode (when target.nodeId equals
the local node) or openCrossNode (otherwise). Both branches were
already correct; only the bind filter was wrong.

Fleet-wide port collisions remain blocked at opt-in time
(optInStack checks aliasByPort), so binding every alias port is
unambiguous. New vitest case asserts the want-set includes both
local-owned and remote-owned ports.

Discovered while running PR 0 verification on v0.75.0 against
Local + sencho-pilot-test: connections from a Local prober to the
pilot's alias port hit Local's host with no listener.
This commit is contained in:
Anso
2026-05-08 16:57:13 -04:00
committed by GitHub
parent e4411f3247
commit 10a469ecb4
2 changed files with 65 additions and 8 deletions
+14 -8
View File
@@ -188,18 +188,24 @@ export class MeshService extends EventEmitter implements MeshForwarderHost {
}
/**
* Bind the forwarder's listeners to the local-owned alias ports and
* release any listeners no longer in the alias set. Called from
* Bind the forwarder's listeners to every alias port across the fleet
* and release any listeners no longer in the alias set. Called from
* `start`, after each `refreshAliasCache` tick, and after every
* opt-in / opt-out / disable on the local node so the bound port set
* follows the DB state.
* opt-in / opt-out / disable so the bound port set follows the DB
* state.
*
* Every meshed node binds every alias port — not just ports it owns —
* because meshed containers' `extra_hosts: <alias>:host-gateway`
* entries resolve to the SOURCE node's gateway, so the source node is
* where the inbound TCP connection lands. `handleAccept` then
* dispatches to the same-node fast path or the cross-node bridge based
* on the resolved alias's owner. Fleet-wide port collisions are
* blocked at opt-in time (`optInStack` checks `aliasByPort`), so
* binding every alias port is unambiguous.
*/
private async syncForwarderListeners(): Promise<void> {
const localNodeId = NodeRegistry.getInstance().getDefaultNodeId();
const wantPorts = new Set<number>();
for (const alias of this.aliasByPort.values()) {
if (alias.nodeId === localNodeId) wantPorts.add(alias.port);
}
const wantPorts = new Set<number>(this.aliasByPort.keys());
const havePorts = new Set(this.forwarder.getListenerPorts());
for (const port of havePorts) {
if (!wantPorts.has(port)) {