Skip to content

Populate real candidate addresses (including overlay-network ones) in gossip #38

Description

@Mearman

Parent: #28 (relates to #35)

Not a design gap — the schema already supports advertising real candidate addresses (gossip's addresses field, currently shipped empty by deliberate placeholder). This is an implementation task: populate real candidate addresses and attempt direct connection against them before falling back to relay.


PR #92 opened: createMeshSession/acceptMeshSession both gain an addresses parameter, threaded through to the self-advert they already send. This is the primitive this issue needs, not the whole thing -- populating it from a real listener's own bound address, and attempting a direct connection against a gossiped candidate before falling back to relay, remain open follow-up work once this lands.


Investigated (2026-09-14) the second half specifically -- attempting a direct connection before falling back to relay -- against the real current sendManageRequest, since that's the exact place the fallback lives today (targetDevice !== undefined unconditionally calls ensureRelayPairing, with no check of the directory's own gossiped addresses first). Confirmed it's genuinely buildable without restructuring MeshSession's own single-connection-per-session design: a direct attempt doesn't need to replace the session's existing connection to its own counterparty (typically a relay hub), it only needs a throwaway connection scoped to the one request -- dial the target's gossiped address, send the manage-request-frame over it directly, await the correlated manage-response, close it, falling back to the existing relay path on dial failure or a bounded timeout.

Two real design forks this surfaces, not yet resolved, both genuine enough to need a decision before implementing rather than picking one silently:

  1. Handshake or not, on the throwaway connection. Every existing connection in this codebase does handshake + self-advert negotiation before anything else (wireUpConnection). A one-off direct-request connection could skip that entirely, since the actual security boundary is the capability token carried in the manage-request itself, not the handshake -- but every receiving peer's own Transport.listen()onConnection handler in this codebase today unconditionally calls acceptMeshSession, which performs that handshake as a matter of course. Either the receiving side needs a way to accept a bare, handshake-less single-request connection (a real protocol addition), or the throwaway connection pays the handshake's own round-trip cost too, undercutting some of the latency win a direct connection is for.
  2. No existing "listens and participates" reference implementation to build the dialing half against. wire-mesh-node's own main() runs a dedicated RelayHub only (hub.handleConnection) and never constructs a MeshSession at all -- it's a pure facilitator with no gossip identity of its own, not a participant. web-console participates (createMeshSession) but is dial-only, never listens. Populating addresses from "a real listener's own bound address" (the first open item above) therefore has no real consumer anywhere in this repo today to wire it into and prove correct end-to-end; agent-comms (a separate repo) is the actual concrete "both listens and dials" case, per the migration plan's own P1 prerequisite work.

Recommend resolving fork 1 first (it's the one that actually blocks writing the direct-attempt code at all), then building the direct-attempt mechanism as a real, tested addition to sendManageRequest, with fork 2's own "populate from a real listener" half staying explicitly out of scope here until a real listening-and-participating consumer exists to validate it against (agent-comms' own P1 work, or a dedicated wire-mesh reference app, whichever lands first).


Fork 1 resolved (2026-09-14), checked directly against the real dispatch code, not assumed. No handshake is needed on the throwaway connection, and no wire-protocol change is needed either.

Traced applyFrame/applyManageRequest in mesh-session.ts: neither has any dependency on handshake state, the directory, or a prior self-advert. applyManageRequest just constructs an IncomingManageRequest (command/scope/token/respond) and hands it to the caller's own incomingManageRequests consumer -- respond sends a manage-response-frame straight back over the connection (or relay-wrapped, if viaRelay). Separately, Connection.peerDeviceId is already "cryptographically authenticated by the adapter itself" per the port's own doc comment -- a TLS-based transport authenticates the peer's device-id at the connection layer, independent of any application-level handshake frame. So the handshake+self-advert exchange wireUpConnection performs is for domain negotiation and directory population, not identity authentication, and a manage-request's own capability token is the actual authorization boundary either way -- exactly the reasoning this issue already gave, now confirmed against the code rather than assumed.

Concrete design, requiring zero CDDL/wire change:

  • Dial side: skip createMeshSession/wireUpConnection for a direct attempt. transport.connect(candidateAddress), check connection.peerDeviceId against the expected target device-id when present (mismatch is a spoofing attempt, refuse per the port's own documented obligation), send exactly one manage-request-frame with the token already attached, await the correlated manage-response-frame from connection.receive() under a bounded timeout, close. No handshake frame, no gossip frame, ever sent on this connection.
  • Accept side: a new function, acceptDirectManageRequest(connection): Promise<IncomingManageRequest | null> -- reads exactly one frame; if it's manage-request, constructs the identical IncomingManageRequest shape applyManageRequest already builds (so every existing application-level handler -- room-protocol.ts, capability-request.ts, anything consuming session.incomingManageRequests today -- needs zero changes to also handle one arriving this way), with respond sending manage-response-frame directly and closing the connection; any other first frame (or the connection closing with none) resolves null and the caller closes. Transport.listen()'s onConnection handler peeks the first frame to decide: handshake -> today's acceptMeshSession path; manage-request -> this new one-off path; anything else -> close, nothing useful to do.

Fork 2 (no real listening-and-participating consumer in this repo yet to validate "populate addresses from a real listener" against) stays exactly as scoped before -- out of this issue, picked up once agent-comms' own migration (a real bidirectional listen-and-dial consumer) or a dedicated wire-mesh reference app reaches that point.

Ready to implement: acceptDirectManageRequest in mesh-session.ts, plus the direct-attempt-before-relay-fallback path in sendManageRequest.


wire-mesh#135 merged (2026-09-14): acceptDirectManageRequest/sendDirectManageRequest landed as designed above. Fork 1 is fully resolved and built. Fork 2 (populating addresses from a real listener) remains open, blocked on a real listen-and-dial consumer as already scoped.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions