fix(bus): dial replies over a candidate set, drop scopeless link-local (#61) - #64
Merged
hartsock merged 1 commit intoJul 15, 2026
Merged
Conversation
#61) 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: Claude Opus 4.8 <noreply@anthropic.com>
4 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this PR does
Fixes the scopeless link-local dial-back bug (#61) by retiring the
single-address reply route from #21 in favour of fingerprint-keyed
candidate resolution — the shape
docs/decisions/floating_identity.mdmandates (locations are candidates, never load-bearing).
reply_dial_candidates(source)— builds the reply dial set: the recvsource path (its
recvmsgscope id already intact) plus same-hostv4/v6 loopback at the source port. Never a single load-bearing address;
never empty for a same-host peer.
dial_reply_peernow dials this set(iroh races the paths) instead of
endpoint.dial(pubkey, [one_addr]).is_scopeless_link_local(addr)—fe80::/10withscope_id == 0.Such an address is undialable and, left in an iroh dial set, fails
sendmsgwithInvalidInput, aborting the whole race. Filtered out atboth dial sites (the reply path and the mDNS
dial_peerset), sothe scopeless
fe80::that mDNS surfaces can no longer poison the dial.Hand-rolled because
Ipv6Addr::is_unicast_link_localis unstable onour MSRV (1.75).
Why a quiet-bind peer was affected:
announce:falsemakes itmDNS-invisible, so dial-back was its only route — the single scopeless
address was a true single point of failure. Loopback keeps the set
non-empty, so its identity stays reachable same-host.
Spike that de-risked the fix
Traced iroh 0.98.2: an inbound datagram's source scope id is
preserved end-to-end —
noq-udpdecode_recvreadssin6_scope_id(
unix.rs),MultipathMappedAddr::Ipkeeps the fullSocketAddr, andIncoming::remote_addr()returns it verbatim. So the recv source path isdialable as-is; the scopeless
fe80::poison enters via mDNSrecords (which carry no scope), which is why the filter lives at the
candidate-set layer, not the recv-capture layer. No upstream iroh change
needed.
Test plan
agent-mesh-bus/src/bus.rs(gate every PR):
scopeless_link_local_source_dropped_loopback_survives— the bus: reply dial-back to a scopeless link-local IPv6 source fails; quiet-bind clients become unreachable #61regression: scopeless
fe80::source → set drops it, keeps bothloopbacks, never empty. Verified RED against a single-address stub
before the fix.
scoped_link_local_source_is_kept— afe80::%Nsource (real scope)stays a candidate.
routable_source_yields_source_plus_loopback— source + bothloopbacks; length ≥ 2 (never one address). Uses RFC 5737 TEST-NET-1.
loopback_source_not_duplicated— dedup.scopeless_link_local_predicate_boundaries—fe80::/10edges,scoped vs scopeless, loopback/global/IPv4/
fec0::negatives.just checkclean:cargo fmt --check,clippy --workspace --all-targets -D warnings(zero warnings),cargo test --workspace(all suites green;
agent-mesh-buslib 54 passed).just cov-cipasses the 75% floor (line coverage clears it even withthe excluded pyo3 modules counted).
Out of scope
#[ignore]d onthe real-LAN tier (per the repo's existing convention that real-mDNS
round-trips are flaky on hosted CI); the deterministic unit tests above
are the per-PR gate.
quiet_client_gets_replies_via_dial_backis being split into a mockedper-PR unit tier + a live iroh smoke lane under newt-agent#1190;
this fix greens that smoke lane at the source. Separate repo, separate
PR.
interface; cross-host peers announce routable addrs over mDNS, so this
is not a regression.
Fixes #61. Supersedes the #62 WIP handoff (close #62 when this lands).
Stacked on
docs/floating-identity(the doctrine this implements);retarget to
mainonce that base merges.