diff --git a/packaging/macos/build-pkg.sh b/packaging/macos/build-pkg.sh index 37b4531..fd25a78 100755 --- a/packaging/macos/build-pkg.sh +++ b/packaging/macos/build-pkg.sh @@ -121,9 +121,9 @@ cp "${PACKAGING_DIR}/common/hosts" "${STAGING_DIR}/usr/local/etc/fips/hosts.defa # LaunchDaemon plist cp "${SCRIPT_DIR}/com.fips.daemon.plist" "${STAGING_DIR}/Library/LaunchDaemons/" -# DNS resolver +# DNS resolver. Must match the daemon's dns.bind_addr (defaults to ::1). cat > "${STAGING_DIR}/etc/resolver/fips" < root_sent_before, + "root should re-push its TreeAnnounce when a peer advertises a worse root \ + (sent before={}, after={})", + root_sent_before, + root_sent_after + ); + + // End state: the child has re-attached to the root rather than remaining a + // stranded self-root. + assert!( + !nodes[child_idx].node.tree_state().is_root(), + "child should have re-attached, not remained a self-root" + ); + assert_eq!( + nodes[child_idx].node.tree_state().root(), + &root_addr, + "child should have re-converged to the root" + ); + let _ = child_addr; + + cleanup_nodes(&mut nodes).await; +} diff --git a/src/node/tree.rs b/src/node/tree.rs index 6cf862c..43b48d8 100644 --- a/src/node/tree.rs +++ b/src/node/tree.rs @@ -224,6 +224,41 @@ impl Node { "Processed TreeAnnounce" ); + // Re-push our current position when the announcing peer advertises a + // strictly worse (higher NodeAddr) root than ours. Root election is + // smallest-NodeAddr-wins, so a peer on a higher root has a stale or + // pre-attachment view and can attach to (or re-attach through) us; + // reply with our current declaration so it does so without waiting + // for the next periodic re-broadcast cadence. + // + // Only the better-rooted side echoes. If the peer's root is lower + // (better) than ours, WE are the stale side: the peer would ignore + // our worse root anyway, and we converge via the parent re-evaluation + // below, so echoing back is pure waste — and during a root change or + // partition merge it would double announce traffic in the learning + // direction. Equal roots are already converged. Restricting to `>` + // keeps the echo to the one direction that helps. + // + // This closes a convergence wedge on a single-uplink node: its only + // peer pushes the attaching announce once at promotion time, and if + // that datagram is lost the single-uplink node cannot self-correct + // (its own periodic parent re-evaluation is disabled below two peers) + // and is stranded as a self-root until the parent's next periodic + // re-broadcast (~reeval_interval_secs later). Echoing on root + // disagreement makes tree-position exchange self-healing on the + // receive path and is naturally bounded by the per-peer 500 ms + // tree-announce rate limiter, so it does not storm during normal + // convergence (it stops as soon as the peer adopts our root). + if *announce.ancestry.root_id() > *self.tree_state.root() + && let Err(e) = self.send_tree_announce_to_peer(from).await + { + debug!( + peer = %self.peer_display_name(from), + error = %e, + "Failed to re-push TreeAnnounce on root disagreement" + ); + } + // Bloom filter exchange initiation is handled at handshake completion // ([handshake.rs] mark_update_needed on the new peer) and on actual // content changes via [bloom.rs::handle_filter_announce]'s