Skip to content

WIP handoff (do NOT merge): candidate dial-back fix for scopeless link-local IPv6 — #61 - #62

Closed
hartsock wants to merge 1 commit into
mainfrom
fix/dial-back-loopback-linklocal
Closed

WIP handoff (do NOT merge): candidate dial-back fix for scopeless link-local IPv6 — #61#62
hartsock wants to merge 1 commit into
mainfrom
fix/dial-back-loopback-linklocal

Conversation

@hartsock

Copy link
Copy Markdown
Member

⚠️ DRAFT / HANDOFF — do not merge. A coordination signal, not a verified fix. The transport is your area; handing this to you with a diagnosis + candidate + why I stopped. See #61.

The bug (diagnosed — #61)

dial_reply_peer (from #21) dials the request's single source addr. When that's a scopeless link-local IPv6 (fe80::…, what mDNS surfaces on CI / most Linux hosts), QUIC sendmsg fails (InvalidInput). A quiet-bind (announce:false) client is mDNS-invisible → dial-back is its only route → reply lost. Red on newt-agent's nightly Mesh-integration; blocks unrelated newt-agent PRs that trip the newt-core/** path filter.

Candidate here (UNVERIFIED)

Augment dial_reply_peer with loopback at the source port (mirrors dial_peer).

  • ✅ Fixes isolated quiet_client_gets_replies_via_dial_back.
  • ❌ Racy in the full serial suite (the fe80:: addr stays in the iroh race, can abort the dial).
  • Dropping fe80:: instead regressed the isolated test → iroh seems to need/reuse the source path, so the real fix is likely upstream: preserve the recvmsg scope_id on the source SocketAddrV6, not a dial_reply_peer patch.
  • Dev-host real-mDNS state degrades across runs; needs your iroh-path context + a clean runner.

Ask

Take it from here (scope_id is probably right); close this draft when the real fix lands. Shawn authorized me + is looping you in. No overlap with #58/#59/#60.

@hartsock hartsock left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This "feels" right. And it's probably worth noting my intention with using a "mesh" instead of direct dial or TCP here is that "identity floats around" as I've seen when developing other "moving" identities in multi-cloud environments. The whole point here is to allow conversational contexts to "float" like a "cloud container" does between "pods" the individual harnesses become "containers" for the conversational context that "floats over" the top of them all.

Provided we ratchet the design properly this will eventually enable "commodity intelligence" or something buzzwordy ... some marketing person will come up with a name I'm sure.

Anyway. It's my old play book. Crawl into a container. Walk over to a new computer. Run in the cloud. I think I've been doing the exact same strategy for 20 years at this point.

@hartsock

Copy link
Copy Markdown
Member Author

Building on Shawn's review — the design steer for whoever lands the real fix:

The bug is a location leak in an identity system. #21 made the reply dial the request's source socket address — pinning a conversation to a location, which is exactly what the floating-identity thesis says must never be load-bearing. The empirical results in #61 line up perfectly with that reading:

  • Augmenting candidate paths helped (isolated test passed): an identity reachable via several paths (source addr + loopback) survives one path being garbage (fe80:: without scope).
  • Dropping the source path regressed: the quiet-bind peer is mDNS-invisible, so the source was its only resolution — remove it and the identity has no location at all.

So the fix shape that matches the thesis: reply-routing resolves the sender's fingerprint (identity), and socket addresses are only ever candidates — the recv path's source (with its recvmsg scope_id preserved so link-local is actually dialable), loopback for same-host, mDNS when announced. Never exactly one, never load-bearing individually. The dial_reply_peer(pubkey, [single addr]) shape is the thing to retire, not patch.

That also makes this draft's loopback-augment an honest stepping stone (more candidates) rather than the destination (identity-resolved reply routing). Handing the destination to the transport owner per #61 — this framing is just so the ratchet points the right way.

@hartsock

Copy link
Copy Markdown
Member Author

Real fix is up: #64 (stacked on docs/floating-identity, the doctrine it implements). It retires the single-address dial_reply_peer shape for fingerprint-keyed candidate resolution (source path + same-host loopback) and filters scopeless fe80:: at both dial sites so it can't sendmsg-abort the iroh race — which was the racy full-suite symptom noted here. Spike confirmed the recv-source scope id survives end-to-end through iroh's noq-udp fork, so the poison is the scopeless mDNS candidate, handled at the candidate-set layer. Close this draft once #64 merges.

hartsock added a commit that referenced this pull request Jul 15, 2026
#61) (#64)

Retire the single-address reply dial-back introduced in #21:
`dial_reply_peer` dialed the request's one source `SocketAddr`, so a
scopeless link-local IPv6 (`fe80::` with no scope id — what mDNS surfaces
on CI and most Linux hosts) became an undialable single point of failure.
QUIC `sendmsg` returned `InvalidInput`; a quiet-bind (`announce:false`)
peer, being mDNS-invisible, had dial-back as its only route, so its reply
was lost.

Per the floating-identity doctrine (docs/decisions/floating_identity.md):
a location is only ever a candidate, never load-bearing.

- `reply_dial_candidates`: the source path (its recvmsg scope id already
  intact — verified preserved through iroh's noq-udp fork) plus same-host
  loopback at the source port. Never one address, never empty.
- `is_scopeless_link_local`: hand-rolled `fe80::/10` + `scope_id == 0`
  test, because std's `is_unicast_link_local` is unstable on MSRV 1.75.
  Applied to BOTH dial sites — the reply path and the mDNS `dial_peer`
  candidate set — so a scopeless `fe80::` can no longer `sendmsg`-abort
  the iroh dial race (the racy full-suite failure seen while diagnosing
  the bug).

Regression tests are pure and deterministic, so they gate every PR; the
real dial-back-over-iroh proof stays #[ignore]d on the real-LAN tier.

Supersedes the #62 WIP handoff.

Co-authored-by: Shawn Hartsock <hartsock@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
hartsock added a commit that referenced this pull request Jul 19, 2026
…ad-bearing (#63)

* docs: floating-identity doctrine — locations are candidates, never load-bearing

Elevates Shawn's PR #62 review from thread prose to binding design law:
identity (fingerprint) is the routing key; socket addresses are only ever
candidate paths; a quiet peer must never resolve to zero paths;
conversations outlive locations. Includes the #61 dial-back case study
(adding candidates fixed, removing the only path regressed — the bug is the
SINGULAR address, so dial_reply_peer(pubkey, [one addr]) is the shape to
retire) and the reviewer checklist + ratchet direction (harnesses fungible,
conversational context durable — the container/cloud playbook one layer up).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(bus): dial replies over a candidate set, drop scopeless link-local (#61) (#64)

Retire the single-address reply dial-back introduced in #21:
`dial_reply_peer` dialed the request's one source `SocketAddr`, so a
scopeless link-local IPv6 (`fe80::` with no scope id — what mDNS surfaces
on CI and most Linux hosts) became an undialable single point of failure.
QUIC `sendmsg` returned `InvalidInput`; a quiet-bind (`announce:false`)
peer, being mDNS-invisible, had dial-back as its only route, so its reply
was lost.

Per the floating-identity doctrine (docs/decisions/floating_identity.md):
a location is only ever a candidate, never load-bearing.

- `reply_dial_candidates`: the source path (its recvmsg scope id already
  intact — verified preserved through iroh's noq-udp fork) plus same-host
  loopback at the source port. Never one address, never empty.
- `is_scopeless_link_local`: hand-rolled `fe80::/10` + `scope_id == 0`
  test, because std's `is_unicast_link_local` is unstable on MSRV 1.75.
  Applied to BOTH dial sites — the reply path and the mDNS `dial_peer`
  candidate set — so a scopeless `fe80::` can no longer `sendmsg`-abort
  the iroh dial race (the racy full-suite failure seen while diagnosing
  the bug).

Regression tests are pure and deterministic, so they gate every PR; the
real dial-back-over-iroh proof stays #[ignore]d on the real-LAN tier.

Supersedes the #62 WIP handoff.

Co-authored-by: Shawn Hartsock <hartsock@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* docs(floating-identity): trim thesis prose, tighten throughout

Cut the 20-year-playbook framing + blockquote and the "commodity intelligence" flourish; compress each section to its load-bearing point. The four design laws are unchanged in substance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(floating-identity): law 5 — first contact is a ceremony; cite prior art

Add the one gap the Zero-Trust research pass found: keyless first contact is a TOFU leap of faith (RFC 7401's own admission). The pin decision is exposed as data and rendered by the consumer (#65). Add a terse prior-art section (Saltzer RFC 1498, HIP, NIST 800-207, SPIFFE, iroh) so reviewers cite the canon instead of rearguing it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Shawn Hartsock <hartsock@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hartsock hartsock closed this Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant