summaryrefslogtreecommitdiffstats
path: root/src/transport
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-28 17:38:16 +0100
committertv <tv@krebsco.de>2026-10-28 17:38:16 +0100
commitd7959009790a422ebb17d4c4ab1d9be3d48dc511 (patch)
treee1409432709b0c856a05fb7fd0c0b31895878bcf /src/transport
parent0c77de0d960de62f770a26204e0ef730d0a07b08 (diff)
list libp2p relays in document
Diffstat (limited to 'src/transport')
-rw-r--r--src/transport/identity.rs5
-rw-r--r--src/transport/libp2p.rs264
-rw-r--r--src/transport/mod.rs7
-rw-r--r--src/transport/paths.rs13
-rw-r--r--src/transport/traffic.rs4
5 files changed, 252 insertions, 41 deletions
diff --git a/src/transport/identity.rs b/src/transport/identity.rs
index 432348e..c597d77 100644
--- a/src/transport/identity.rs
+++ b/src/transport/identity.rs
@@ -540,6 +540,11 @@ impl Transport for IdentityTransport {
self.shared.paths.transports()
}
+ /// Each transport those with its scheme, without it (`Paths::learn_relays`).
+ fn learn_relays(&self, addresses: &[String]) {
+ self.shared.paths.learn_relays(addresses);
+ }
+
/// The address `encode` picks, the transport's connection there, and every path
/// to `peer` as paths measures it, best first.
fn destination(&self, peer: &str) -> Option<Destination> {
diff --git a/src/transport/libp2p.rs b/src/transport/libp2p.rs
index 63f6335..a6778c6 100644
--- a/src/transport/libp2p.rs
+++ b/src/transport/libp2p.rs
@@ -30,7 +30,8 @@
//! A relay says so where it publishes its addresses: next to each, the same address
//! ending in `/p2p-circuit` (where to reserve a slot), which other nodes find in the
//! member list. A node that needs a relay picks up to `AUTO_RELAYS` relays
-//! (`relay_check`): those configured first, then members that relay. It needs one when
+//! (`relay_check`): those configured first, then those listed in the document
+//! (`learn_relays`), then members that relay. It needs one when
//! it has no address others could dial, or when AutoNAT says it can't be dialled:
//! AutoNAT asks connected peers (nodes of the swarm) to dial it back. Once AutoNAT says
//! it can be dialled, it lets the relays it picked go.
@@ -190,10 +191,40 @@ enum Command {
},
/// See whether relays are needed now (one was just learned of).
CheckRelays,
- /// Find out which peer relays at `address`, configured without the relay's peer ID.
+ /// Find out which peer relays at `address`, named without the relay's peer ID
+ /// (`Book::finding`).
FindRelay { address: Multiaddr },
}
+/// Who named a relay to pick, in the order they are picked.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+enum Named {
+ /// `Libp2pConfig::relays`.
+ Configured,
+ /// The relays listed in the document (`Transport::learn_relays`).
+ Listed,
+}
+
+impl Named {
+ fn as_str(self) -> &'static str {
+ match self {
+ Self::Configured => "configured",
+ Self::Listed => "listed",
+ }
+ }
+}
+
+/// A relay named to pick.
+#[derive(Clone, Debug, PartialEq, Eq)]
+struct NamedRelay {
+ /// Where, with the relay's peer ID.
+ at: Multiaddr,
+ /// As named: without the peer ID, it is found out again when the relay doesn't
+ /// grant a slot (it may have a new one).
+ given: Multiaddr,
+ named: Named,
+}
+
/// Where peers are, and where we listen.
#[derive(Default)]
struct Book {
@@ -203,12 +234,12 @@ struct Book {
announced: Vec<Multiaddr>,
/// `Libp2pConfig::relay`: this node relays, and says so.
serving: bool,
- /// Relays configured (`Libp2pConfig::relays`), and where (with the relay's peer ID):
- /// picked first.
- configured_relays: HashMap<PeerId, Multiaddr>,
- /// Of those, the ones configured without the relay's peer ID, as configured: found
- /// out again when the relay doesn't grant a slot (it may have a new peer ID).
- unnamed_relays: HashMap<PeerId, Multiaddr>,
+ /// Relays named to pick (configured, listed), by peer: picked first.
+ named_relays: HashMap<PeerId, NamedRelay>,
+ /// Relays named without their peer ID, being found out (`Command::FindRelay`).
+ finding: HashMap<Multiaddr, Named>,
+ /// The relays listed in the document, as last learned (`learn_listed`).
+ listed: Vec<Multiaddr>,
/// Members that say they relay, and where (with the relay's peer ID).
relays: HashMap<PeerId, Multiaddr>,
/// With `Libp2pConfig::public`: other peers that relay (identify says so), and
@@ -481,6 +512,107 @@ fn published(all: &[Multiaddr]) -> Vec<Multiaddr> {
}
impl Book {
+ /// `named` names the relay at `given`; returns it if its peer is to be found out.
+ /// `local`: this node, never its own relay.
+ fn name_relay(&mut self, given: Multiaddr, named: Named, local: PeerId) -> Option<Multiaddr> {
+ match split_peer(&given) {
+ Some(peer) if peer == local => None,
+ Some(peer) => {
+ // Configured wins over listed.
+ if self
+ .named_relays
+ .get(&peer)
+ .is_none_or(|known| known.named == Named::Listed || named == Named::Configured)
+ {
+ self.named_relays.insert(
+ peer,
+ NamedRelay {
+ at: given.clone(),
+ given,
+ named,
+ },
+ );
+ }
+ None
+ }
+ None => {
+ let known = self.finding.contains_key(&given)
+ || self.named_relays.values().any(|relay| relay.given == given);
+ if known {
+ return None;
+ }
+ self.finding.insert(given.clone(), named);
+ Some(given)
+ }
+ }
+ }
+
+ /// The relay named at `given` (without a peer ID) is `peer`: unless it is no longer
+ /// named (or is this node). Returns whether that's news.
+ fn relay_found(&mut self, given: Multiaddr, peer: PeerId, local: PeerId) -> bool {
+ let Some(named) = self.finding.remove(&given) else {
+ return false;
+ };
+ let Ok(at) = given.clone().with_p2p(peer) else {
+ return false;
+ };
+ if peer == local
+ || (named == Named::Listed
+ && self
+ .named_relays
+ .get(&peer)
+ .is_some_and(|known| known.named == Named::Configured))
+ {
+ return false;
+ }
+ self.named_relays
+ .insert(peer, NamedRelay { at, given, named });
+ true
+ }
+
+ /// `relay` didn't grant a slot: if it was named without its peer ID, its address is
+ /// to be found out again, returned.
+ fn find_again(&mut self, relay: PeerId) -> Option<Multiaddr> {
+ if self
+ .named_relays
+ .get(&relay)
+ .is_none_or(|known| split_peer(&known.given).is_some())
+ {
+ return None;
+ }
+ let known = self.named_relays.remove(&relay)?;
+ self.finding.insert(known.given.clone(), known.named);
+ Some(known.given)
+ }
+
+ /// The relays listed in the document are `listed` now: returns those to find out,
+ /// None if nothing changed.
+ fn learn_listed(&mut self, listed: Vec<Multiaddr>, local: PeerId) -> Option<Vec<Multiaddr>> {
+ if listed == self.listed {
+ return None;
+ }
+ self.named_relays
+ .retain(|_, relay| relay.named != Named::Listed || listed.contains(&relay.given));
+ self.finding
+ .retain(|given, named| *named != Named::Listed || listed.contains(given));
+ self.listed = listed.clone();
+ Some(
+ listed
+ .into_iter()
+ .filter_map(|given| self.name_relay(given, Named::Listed, local))
+ .collect(),
+ )
+ }
+
+ /// The named relays `named` names, by peer, where.
+ fn named(&self, named: Named) -> HashMap<PeerId, Multiaddr> {
+ self.named_relays
+ .iter()
+ .filter(|(_, relay)| relay.named == named)
+ .map(|(peer, relay)| (*peer, relay.at.clone()))
+ .collect()
+ }
+
/// Whether all of `addresses` are known of `peer` (`remember`).
fn knows(&self, peer: PeerId, addresses: &[Multiaddr]) -> bool {
let Some(known) = self.addresses.get(&peer) else {
@@ -588,24 +720,17 @@ impl Libp2pTransport {
.accept(PROTOCOL)
.map_err(|error| anyhow!("Accepting {PROTOCOL}: {error}"))?;
let (commands, receiver) = mpsc::unbounded_channel();
- let mut configured_relays = HashMap::new();
- for relay in config.relays {
- match split_peer(&relay) {
- Some(peer) if peer != peer_id => {
- configured_relays.insert(peer, relay);
- }
- Some(_) => {} // This node.
- None => {
- let _ = commands.send(Command::FindRelay { address: relay });
- }
- }
- }
- let book = Arc::new(Mutex::new(Book {
+ let mut book = Book {
announced: config.announce,
serving: config.relay,
- configured_relays,
..Book::default()
- }));
+ };
+ for relay in config.relays {
+ if let Some(address) = book.name_relay(relay, Named::Configured, peer_id) {
+ let _ = commands.send(Command::FindRelay { address });
+ }
+ }
+ let book = Arc::new(Mutex::new(book));
let (stopping, stopped) = watch::channel(false);
tokio::spawn(run(
swarm,
@@ -875,8 +1000,8 @@ fn relay_check(
candidates
}
-/// Reserves slots on the relays `relay_check` picks (configured ones first, then
-/// members, then public ones), and lets those picked go once AutoNAT says this node can
+/// Reserves slots on the relays `relay_check` picks (configured ones first, then listed
+/// ones, members, public ones), and lets those picked go once AutoNAT says this node can
/// be dialled.
fn pick_relays(
swarm: &mut Swarm<Behaviour>,
@@ -894,7 +1019,7 @@ fn pick_relays(
}
return;
}
- let (direct, configured, members, public) = {
+ let (direct, configured, listed, members, public) = {
let mut book = lock(book);
book.failed_relays
.retain(|_, at| at.elapsed() < RELAY_PAUSE);
@@ -913,7 +1038,8 @@ fn pick_relays(
};
(
!book.direct().is_empty(),
- usable(&book.configured_relays),
+ usable(&book.named(Named::Configured)),
+ usable(&book.named(Named::Listed)),
usable(&book.relays),
public,
)
@@ -921,7 +1047,8 @@ fn pick_relays(
let mut using: Vec<PeerId> = picked.values().copied().collect();
// Each kind for what the ones before left.
for (kind, offers) in [
- ("configured", configured),
+ (Named::Configured.as_str(), configured),
+ (Named::Listed.as_str(), listed),
("member", members),
("public", public),
] {
@@ -1059,19 +1186,16 @@ async fn run(
}
match tokio::time::timeout(CONNECT_TIMEOUT, identified).await {
Ok(Ok(Ok(peer))) => {
- if peer == local {
- return; // This node.
- }
- if let Ok(relay) = address.clone().with_p2p(peer) {
- debug!(relay = %Addressed(SCHEME, &relay), "Found a configured relay");
- let mut known = lock(&book);
- known.configured_relays.insert(peer, relay);
- known.unnamed_relays.insert(peer, address);
+ if lock(&book).relay_found(address.clone(), peer, local) {
+ debug!(relay = %Addressed(SCHEME, &address), peer = %Addressed(SCHEME, &peer), "Found a named relay");
+ let _ = again.send(Command::CheckRelays);
}
- let _ = again.send(Command::CheckRelays);
}
_ => {
- warn!(relay = %Addressed(SCHEME, &address), "Couldn't reach a configured relay; trying again later");
+ if !lock(&book).finding.contains_key(&address) {
+ return; // No longer named.
+ }
+ warn!(relay = %Addressed(SCHEME, &address), "Couldn't reach a named relay; trying again later");
tokio::time::sleep(RELAY_RETRY).await;
let _ = again.send(Command::FindRelay { address });
}
@@ -1187,9 +1311,8 @@ async fn run(
} else {
book.counts.slots_not_granted += 1;
book.failed_relays.insert(relay, tokio::time::Instant::now());
- // Configured without its peer ID: maybe another one now.
- if let Some(address) = book.unnamed_relays.remove(&relay) {
- book.configured_relays.remove(&relay);
+ // Named without its peer ID: maybe another one now.
+ if let Some(address) = book.find_again(relay) {
let _ = again.send(Command::FindRelay { address });
}
}
@@ -1430,6 +1553,21 @@ impl Transport for Libp2pTransport {
Some(peer.to_string())
}
+ /// Multiaddrs, with or without the relay's peer ID; others are ignored.
+ fn learn_relays(&self, targets: &[String]) {
+ let listed = targets
+ .iter()
+ .filter_map(|target| Multiaddr::from_str(target).ok())
+ .collect();
+ let Some(found) = lock(&self.book).learn_listed(listed, self.peer_id) else {
+ return;
+ };
+ for address in found {
+ let _ = self.commands.send(Command::FindRelay { address });
+ }
+ let _ = self.commands.send(Command::CheckRelays);
+ }
+
fn connection(&self, target: &str) -> Option<Connection> {
let peer = PeerId::from_str(target).ok()?;
lock(&self.book).connection(&peer)
@@ -1936,6 +2074,50 @@ mod tests {
}
#[test]
+ fn named_relays() {
+ let (local, r1, r2) = (peer(), peer(), peer());
+ let at = |port: u16| -> Multiaddr { format!("/ip6/fd00::1/tcp/{port}").parse().unwrap() };
+ let mut book = Book::default();
+ // With a peer ID: known at once; without: to be found out, once.
+ assert_eq!(
+ book.name_relay(at(1).with_p2p(r1).unwrap(), Named::Configured, local),
+ None
+ );
+ assert_eq!(
+ book.name_relay(at(2), Named::Configured, local),
+ Some(at(2))
+ );
+ assert_eq!(book.name_relay(at(2), Named::Configured, local), None);
+ assert_eq!(
+ book.name_relay(at(3).with_p2p(local).unwrap(), Named::Configured, local),
+ None
+ );
+ assert!(book.relay_found(at(2), r2, local));
+ assert_eq!(book.named(Named::Configured).len(), 2);
+ // A relay named with its peer ID isn't found out again; one without is.
+ assert_eq!(book.find_again(r1), None);
+ assert_eq!(book.find_again(r2), Some(at(2)));
+ assert!(!book.named(Named::Configured).contains_key(&r2));
+
+ // Listed: configured wins; unlisted ones go, unless also configured.
+ let r3 = peer();
+ let listed = vec![
+ at(1).with_p2p(r1).unwrap(),
+ at(4).with_p2p(r3).unwrap(),
+ at(5),
+ ];
+ assert_eq!(book.learn_listed(listed.clone(), local), Some(vec![at(5)]));
+ assert_eq!(book.learn_listed(listed, local), None);
+ assert_eq!(book.named_relays[&r1].named, Named::Configured);
+ assert_eq!(book.named(Named::Listed).keys().collect::<Vec<_>>(), [&r3]);
+ assert_eq!(book.learn_listed(Vec::new(), local), Some(Vec::new()));
+ assert!(book.named(Named::Listed).is_empty());
+ assert!(!book.finding.contains_key(&at(5)));
+ assert!(!book.relay_found(at(5), peer(), local)); // No longer listed.
+ assert_eq!(book.named(Named::Configured).len(), 1);
+ }
+
+ #[test]
fn known_addresses() {
let mut book = Book::default();
let (relay, us) = (peer(), peer());
diff --git a/src/transport/mod.rs b/src/transport/mod.rs
index 7cd976b..fb13ad9 100644
--- a/src/transport/mod.rs
+++ b/src/transport/mod.rs
@@ -683,6 +683,13 @@ pub(crate) trait Transport: Send + Sync {
let _ = (peer, addresses);
}
+ /// The relays listed for the swarm (`swarm::members::relays`), as addresses: under
+ /// the identity layer, each transport is given those with its scheme, without it.
+ /// By default nothing to learn.
+ fn learn_relays(&self, addresses: &[String]) {
+ let _ = addresses;
+ }
+
/// Where messages to `peer` go now. None if nowhere, and by default, for a
/// transport whose identities are addresses (`peer` is where they go).
fn destination(&self, peer: &str) -> Option<Destination> {
diff --git a/src/transport/paths.rs b/src/transport/paths.rs
index 2fe534a..3baa092 100644
--- a/src/transport/paths.rs
+++ b/src/transport/paths.rs
@@ -480,6 +480,19 @@ impl Paths {
.map(Link::own_address)
}
+ /// Relays listed for the swarm (`Transport::learn_relays`): to each transport the
+ /// addresses with its scheme, without it.
+ pub(crate) fn learn_relays(&self, addresses: &[String]) {
+ for link in &self.links {
+ let targets: Vec<String> = addresses
+ .iter()
+ .filter_map(|address| address.strip_prefix(link.scheme)?.strip_prefix(':'))
+ .map(str::to_owned)
+ .collect();
+ link.inner.learn_relays(&targets);
+ }
+ }
+
/// This node's own addresses, one per transport: `<scheme>:<its identity there>`.
pub(crate) fn own_addresses(&self) -> Vec<String> {
self.links.iter().map(Link::own_address).collect()
diff --git a/src/transport/traffic.rs b/src/transport/traffic.rs
index 32f7257..b237db2 100644
--- a/src/transport/traffic.rs
+++ b/src/transport/traffic.rs
@@ -180,6 +180,10 @@ impl Transport for CountingTransport {
self.inner.transports()
}
+ fn learn_relays(&self, addresses: &[String]) {
+ self.inner.learn_relays(addresses)
+ }
+
fn destination(&self, peer: &str) -> Option<crate::transport::Destination> {
self.inner.destination(peer)
}