From 5c96698de2c1d239985f3c0672b41f29a95ca13c Mon Sep 17 00:00:00 2001 From: tv Date: Tue, 3 Nov 2026 01:54:10 +0100 Subject: swarm: links by whom we reach --- src/simtest/links.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++++ src/swarm/links.rs | 11 +++++++---- src/swarm/topology/mod.rs | 26 +++++++++++++++++-------- 3 files changed, 73 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/simtest/links.rs b/src/simtest/links.rs index 44836bd..17c7ced 100644 --- a/src/simtest/links.rs +++ b/src/simtest/links.rs @@ -103,3 +103,51 @@ async fn links_are_relayed_to_members_out_of_reach() { .await; } } + +/// a, b and c all on `sim`, but nothing passes between a and c (as behind NAT): once a +/// judges c unreachable, it asks c's links through b, though it has c's address. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn links_are_relayed_to_members_that_dont_answer() { + let sim = Sim::with_identity(64); + let cut = crate::transport::sim::LinkParams { + loss: 1.0, + ..Default::default() + }; + sim.net.set_link("a", "c", cut.clone()); + sim.net.set_link("c", "a", cut); + let b = sim.start_on("b", &["sim"], &[], Start::Genesis); + let a = sim.start_on("a", &["sim"], &["b"], Start::Join); + let c = sim.start_on("c", &["sim"], &["b"], Start::Join); + sim.wait_ready(&[&a, &b, &c], SOON).await; + let (b_id, c_id) = (b.local_id().to_owned(), c.local_id().to_owned()); + { + let (a, c_id) = (&a, c_id.clone()); + sim.wait_until( + "a knows c, and that it doesn't answer", + SOON * 3, + move || { + let c_id = c_id.clone(); + async move { + let listed = members::members(&a.doc.read().await.doc).contains(&c_id); + listed && a.transport.can_reach(&c_id) && !topology::reaches(a, &c_id).await + } + }, + ) + .await; + } + links::ask_all( + &a, + vec![b_id.clone(), c_id.clone()], + Duration::from_secs(10), + ) + .await; + { + let (a, b_id, c_id) = (&a, b_id.clone(), c_id.clone()); + sim.wait_until("a has c's links, relayed by b", SOON, move || { + let (b_id, c_id) = (b_id.clone(), c_id.clone()); + async move { has_links(a, &c_id, &b_id) } + }) + .await; + } + assert!(!c.links.subscribers().contains(&a.local_id().to_owned())); +} diff --git a/src/swarm/links.rs b/src/swarm/links.rs index 7f8f0b0..14c5cf6 100644 --- a/src/swarm/links.rs +++ b/src/swarm/links.rs @@ -17,7 +17,9 @@ //! renewing) or a LINKS with 0 ends it. //! //! A member the asker can't reach (on none of its transports: one on Tor alone, asked -//! from one on libp2p alone) is asked through a peer that can (`relay_for`): a LINKS +//! from one on libp2p alone; or one that doesn't answer it, as behind NAT: as choosing +//! neighbours judges it, `topology::reaches`) is asked through a peer that can +//! (`relay_for`): a LINKS //! naming the members (`of`) asks the receiver to ask them on the sender's behalf, and //! to pass their links on (`relayed`). The receiver asks each itself, or through a peer //! of its own while hops are left (`hops`), for as long as the sender asked, and holds @@ -362,7 +364,7 @@ async fn split( if member == state.local_id() || Some(member.as_str()) == exclude { continue; } - if state.transport.can_reach(&member) { + if topology::reaches(state, &member).await { direct.push(member); } else if let Some(relay) = relay_for(state, &member, exclude).await { through.entry(relay).or_default().push(member); @@ -372,7 +374,7 @@ async fn split( } /// The peer to ask `member` through, which we can't reach: an active member we reach -/// that, by the member list, is on a transport `member` is on; else one on a transport +/// (that answers us) that, by the member list, is on a transport `member` is on; else one on a transport /// we aren't on (it may know one that is). Of several, the one with the lowest pair score /// with `member` (`topology::pair_score`), so askers pick alike. Not `exclude`. async fn relay_for(state: &AppState, member: &str, exclude: Option<&str>) -> Option { @@ -385,7 +387,8 @@ async fn relay_for(state: &AppState, member: &str, exclude: Option<&str>) -> Opt } { let peer_state = peer_state.lock().await; - if peer_state.liveness.is_dormant() + if peer_state.is_unreachable() + || peer_state.liveness.is_dormant() || !matches!(peer_state.standing, crate::state::Standing::Member { .. }) { continue; diff --git a/src/swarm/topology/mod.rs b/src/swarm/topology/mod.rs index e3479e3..992c448 100644 --- a/src/swarm/topology/mod.rs +++ b/src/swarm/topology/mod.rs @@ -142,6 +142,19 @@ pub(crate) fn intended_graph(state: &AppState) -> Option bool { + if !state.transport.can_reach(peer) { + return false; + } + match state.get_peer_state(peer).await { + Some(peer_state) => !peer_state.lock().await.is_unreachable(), + None => true, + } +} + /// The transports `member` is on, by the member list (the schemes of its addresses); /// ours as we are on them. None known: empty. pub(crate) fn transports_of(state: &AppState, member: &str) -> BTreeSet { @@ -251,18 +264,15 @@ pub(crate) async fn refresh(state: &AppState) -> Option { if member == state.local_id() { continue; } - if !state.transport.can_reach(member) { + if !reaches(state, member).await { unreachable.insert(member.clone()); continue; } - let (gone, heard) = match state.get_peer_state(member).await { - Some(peer_state) => { - let peer_state = peer_state.lock().await; - (peer_state.is_unreachable(), peer_state.last_seen.is_some()) - } - None => (false, false), + let heard = match state.get_peer_state(member).await { + Some(peer_state) => peer_state.lock().await.last_seen.is_some(), + None => false, }; - if gone || (!heard && !alive.contains(member)) { + if !heard && !alive.contains(member) { unreachable.insert(member.clone()); } } -- cgit v1.3.1