diff options
Diffstat (limited to 'src/transport/libp2p.rs')
| -rw-r--r-- | src/transport/libp2p.rs | 438 |
1 files changed, 366 insertions, 72 deletions
diff --git a/src/transport/libp2p.rs b/src/transport/libp2p.rs index d621c8e..b5cee5f 100644 --- a/src/transport/libp2p.rs +++ b/src/transport/libp2p.rs @@ -21,7 +21,9 @@ //! (`<relay>/p2p-circuit/p2p/<it>`) is published with the others. Over a relayed connection, both sides then try to //! connect directly (libp2p's DCUtR, hole punching); the relay carries the traffic until //! that works, or for good if it doesn't, within its limits (`RELAY_CIRCUIT_DURATION`, -//! `RELAY_CIRCUIT_BYTES`; a circuit that ends is made again on the next send). +//! `RELAY_CIRCUIT_BYTES`; a circuit that ends is made again on the next send). Hole +//! punching needs both sides to listen; where only one does, the other dials it +//! directly once it knows where (`upgrade`). //! //! Of several connections to one peer (both dialled at once, or AutoNAT dialled back), //! all but one are closed a few seconds later (`DUPLICATE_GRACE`, `Book::duplicates`), @@ -103,9 +105,13 @@ const MAX_PEERS: usize = 4096; /// the punching only). const RELAY_CIRCUIT_DURATION: Duration = Duration::from_secs(3600); const RELAY_CIRCUIT_BYTES: u64 = 1 << 30; -/// Circuits through this node at once, and per peer. -const RELAY_CIRCUITS: usize = 64; -const RELAY_CIRCUITS_PER_PEER: usize = 8; +/// Circuits through this node at once, and per peer (either end: a node behind the +/// relay is an end of every circuit to it). libp2p's rate limits on circuits and slots +/// (a burst, then one every minute or two, per peer and per IP address: every node +/// behind one NAT shares one) are left out: they're for public relays serving +/// strangers, and refused circuits used them up too. +const RELAY_CIRCUITS: usize = 1024; +const RELAY_CIRCUITS_PER_PEER: usize = 64; /// A configured relay that couldn't be reached to find out its peer ID is tried again /// after this. const RELAY_RETRY: Duration = Duration::from_secs(30); @@ -113,10 +119,16 @@ const RELAY_RETRY: Duration = Duration::from_secs(30); /// needs them. const AUTO_RELAYS: usize = 2; const RELAY_CHECK: Duration = Duration::from_secs(30); -/// A relay that lost the slot before granting it (it doesn't answer, or another peer -/// answers at its address: a relay restarted without its libp2p key) isn't picked again -/// for this long. +/// A relay that refused a slot (`relay_refused`: none left, not allowed, not a relay, +/// another peer at its address: a relay restarted without its libp2p key) isn't picked +/// again for this long. One that didn't grant it for another reason (it doesn't answer, +/// the connection closed) isn't for `RELAY_RETRY`; a slot lost is reserved again at +/// once. const RELAY_PAUSE: Duration = Duration::from_secs(600); +/// A peer connected only through a relay is dialled directly where it can be reached +/// (`upgrade`): first at once, then after a failure no sooner than this, doubling up to +/// `RELAY_PAUSE`. +const UPGRADE_RETRY: Duration = Duration::from_secs(30); /// A peer nobody mentioned for this long (the member list, identify, mDNS, a /// connection) is forgotten, unless connected: mostly old peer IDs of nodes restarted /// without their libp2p key, whose new ones the member list has since. Members are @@ -248,12 +260,15 @@ struct Book { /// With `Libp2pConfig::public`: other peers that relay (identify says so), and /// where. public_relays: HashMap<PeerId, Multiaddr>, - /// Relays picked that never granted a slot, and when (`RELAY_PAUSE`). + /// Relays not to be picked, until when (`RELAY_PAUSE`, `RELAY_RETRY`). failed_relays: HashMap<PeerId, tokio::time::Instant>, /// Relays that granted us a slot we hold. slots: std::collections::HashSet<PeerId>, /// Peers holding a slot on this node, as a relay. slots_here: std::collections::HashSet<PeerId>, + /// Peers connected only through a relay, dialled directly (`upgrade`): when to again + /// at the soonest, and how long to wait after that. + upgrades: HashMap<PeerId, (tokio::time::Instant, Duration)>, /// Open connections: to whom, whether through a relay, and the remote address. connections: HashMap<ConnectionId, (PeerId, bool, Multiaddr)>, /// What AutoNAT says, and the address it confirmed while public. @@ -267,6 +282,9 @@ struct Book { lan: std::collections::HashSet<PeerId>, /// Peers that speak continuum (identify says so). continuum: std::collections::HashSet<PeerId>, + /// Peers identify says don't speak continuum, and whether they relay: not sent to + /// (`send_encoded`), so that a seed that's no node says so. + not_nodes: HashMap<PeerId, bool>, /// Peers found on the LAN that speak continuum, with their address: seeds not /// handed out yet (`discovered_seeds`). discovered: Vec<Multiaddr>, @@ -347,6 +365,22 @@ fn relay_failure_text(debug: &str, innermost: &str) -> String { innermost.to_owned() } +/// Whether a slot ended, or wasn't granted, because the relay refused it (its error's +/// debug form and innermost cause, as for `relay_failure_text`), rather than because it +/// didn't answer or the connection closed: only then is it worth not asking again for a +/// while. +fn relay_refused(debug: &str, innermost: &str) -> bool { + let refusals = [ + "NoAddressesInReservation", + "ResourceLimitExceeded", + "PermissionDenied", + "connection gated", + "UnsupportedProtocol", + "WrongPeerId", + ]; + refusals.iter().any(|pattern| debug.contains(pattern)) || innermost == "Failed" +} + /// The innermost of `error`'s causes, each distinct one once: those libp2p lists in a /// "Multiple dial errors occurred:" text (one per line, ` - <cause>: <its cause>...`), /// or the last of the source chain. @@ -459,7 +493,7 @@ impl Book { paused: self .failed_relays .values() - .filter(|at| at.elapsed() < RELAY_PAUSE) + .filter(|until| tokio::time::Instant::now() < **until) .count(), granted: self.counts.slots_granted, lost: self.counts.slots_lost, @@ -501,8 +535,10 @@ impl Book { self.relays.remove(peer); self.public_relays.remove(peer); self.continuum.remove(peer); + self.not_nodes.remove(peer); self.lan.remove(peer); self.failed_relays.remove(peer); + self.upgrades.remove(peer); self.seen.remove(peer); } stale.len() @@ -666,6 +702,32 @@ impl Book { .collect() } + /// Where `peer` may be reached through the relays we know (named, then members that + /// relay): for a peer known by its ID only (a seed `/p2p/<peer ID>`), which may hold + /// a slot on one of them. `local`: this node, never its own relay. + fn through_relays(&self, peer: PeerId, local: PeerId) -> Vec<Multiaddr> { + let mut relays: Vec<(PeerId, Multiaddr)> = self + .named_relays + .iter() + .map(|(relay, named)| (*relay, named.at.clone())) + .collect(); + relays.sort_by_key(|(_, address)| address.to_string()); + let mut members: Vec<(PeerId, Multiaddr)> = self + .relays + .iter() + .filter(|(relay, _)| !self.named_relays.contains_key(relay)) + .map(|(relay, address)| (*relay, address.clone())) + .collect(); + members.sort_by_key(|(_, address)| address.to_string()); + relays + .into_iter() + .chain(members) + .filter(|(relay, _)| *relay != local && *relay != peer) + .map(|(_, address)| address.with(Protocol::P2pCircuit).with(Protocol::P2p(peer))) + .take(ADDRESSES_PER_PEER) + .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 { @@ -817,11 +879,19 @@ impl Libp2pTransport { /// Connected to `peer`, if it can be had in time. async fn connect(&self, peer: PeerId) -> Result<()> { - let addresses = lock(&self.book) - .addresses - .get(&peer) - .map(|known| dialable(known, self.peer_id)) - .unwrap_or_default(); + let addresses = { + let book = lock(&self.book); + let known = book + .addresses + .get(&peer) + .map(|known| dialable(known, self.peer_id)) + .unwrap_or_default(); + if known.is_empty() { + book.through_relays(peer, self.peer_id) + } else { + known + } + }; let (done, connected) = oneshot::channel(); self.commands .send(Command::Connect { @@ -924,6 +994,8 @@ impl Behaviour { max_circuits_per_peer: RELAY_CIRCUITS_PER_PEER, max_circuit_duration: RELAY_CIRCUIT_DURATION, max_circuit_bytes: RELAY_CIRCUIT_BYTES, + reservation_rate_limiters: Vec::new(), + circuit_src_rate_limiters: Vec::new(), ..Default::default() }, ) @@ -1080,8 +1152,8 @@ fn pick_relays( } let (direct, configured, listed, members, public) = { let mut book = lock(book); - book.failed_relays - .retain(|_, at| at.elapsed() < RELAY_PAUSE); + let now = tokio::time::Instant::now(); + book.failed_relays.retain(|_, until| now < *until); let failed = book.failed_relays.clone(); let usable = |offers: &HashMap<PeerId, Multiaddr>| -> HashMap<PeerId, Multiaddr> { offers @@ -1126,6 +1198,50 @@ fn pick_relays( } } +/// Dials `peer` directly if it's connected only through a relay and is known elsewhere +/// too (the member list, identify, mDNS), since the first connection, made through a +/// relay (a relayed seed), would otherwise be kept for good: a peer connected isn't +/// dialled again, and hole punching needs both sides to listen. The direct connection, +/// once open, wins over the relayed one (`Book::duplicates`). +fn upgrade(swarm: &mut Swarm<Behaviour>, book: &Mutex<Book>, peer: PeerId, local: PeerId) { + let direct: Vec<Multiaddr> = { + let mut book = lock(book); + if book.connection(&peer) != Some(Connection::Relayed) { + return; + } + let direct: Vec<Multiaddr> = book + .addresses + .get(&peer) + .map(|known| dialable(known, local)) + .unwrap_or_default() + .into_iter() + .filter(|address| !is_relayed(address)) + .collect(); + if direct.is_empty() { + return; + } + let now = tokio::time::Instant::now(); + let (due, wait) = book + .upgrades + .get(&peer) + .copied() + .unwrap_or((now, UPGRADE_RETRY)); + if now < due { + return; + } + book.upgrades + .insert(peer, (now + wait, (wait * 2).min(RELAY_PAUSE))); + direct + }; + debug!(peer = %Addressed(SCHEME, &peer), addresses = ?direct, "Connected only through a relay; dialling directly"); + let _ = swarm.dial( + DialOpts::peer_id(peer) + .addresses(direct) + .condition(PeerCondition::NotDialing) + .build(), + ); +} + /// Asks the DHT (with `--libp2p-public`) where `peer` is, for the next attempt. fn find_on_dht(swarm: &mut Swarm<Behaviour>, peer: PeerId) { if let Some(dht) = swarm.behaviour_mut().kad.as_mut() { @@ -1210,6 +1326,7 @@ async fn run( Some(Command::Connect { peer, addresses, done }) => { if swarm.is_connected(&peer) { let _ = done.send(Ok(())); + upgrade(&mut swarm, &book, peer, relaying.local); continue; } let waiting = connecting.entry(peer).or_default(); @@ -1285,6 +1402,9 @@ async fn run( connection_id, (peer_id, relayed, endpoint.get_remote_address().clone()), ); + if !relayed { + book.upgrades.remove(&peer_id); + } if relaying.local < peer_id && book.connections.values().filter(|(connected, _, _)| *connected == peer_id).count() > 1 { @@ -1363,31 +1483,50 @@ async fn run( // A relay picked: the next check picks again, another one if this one // never granted the slot. if let Some(relay) = picked.remove(&listener_id) { - let lost = { + let refused = match &reason { + Ok(()) => false, + Err(error) => relay_refused(&format!("{error:?}"), &innermost_causes(error)), + }; + let (lost, pause) = { let mut book = lock(&book); let lost = book.slots.remove(&relay); + let pause = match (refused, lost) { + (true, _) => Some(RELAY_PAUSE), + (false, true) => None, + (false, false) => Some(RELAY_RETRY), + }; if lost { book.counts.slots_lost += 1; } else { book.counts.slots_not_granted += 1; - book.failed_relays.insert(relay, tokio::time::Instant::now()); // Named without its peer ID: maybe another one now. if let Some(address) = book.find_again(relay) { let _ = again.send(Command::FindRelay { address }); } } - lost + if let Some(pause) = pause { + book.failed_relays.insert(relay, tokio::time::Instant::now() + pause); + } + (lost, pause) }; + // Again at once, or others meanwhile. + let _ = again.send(Command::CheckRelays); let reason = relay_failure(&reason); - if lost { - debug!(relay = %Addressed(SCHEME, &relay), reason = %reason, "Lost the slot on a relay"); - } else { - info!( - relay = %Addressed(SCHEME, &relay), - pause = ?RELAY_PAUSE, - reason = %reason, - "A relay didn't grant a slot; picking others for a while" - ); + match (lost, pause) { + (true, None) => { + info!(relay = %Addressed(SCHEME, &relay), reason = %reason, "Lost the slot on a relay; reserving again"); + } + (true, Some(pause)) => { + info!(relay = %Addressed(SCHEME, &relay), pause = ?pause, reason = %reason, "Lost the slot on a relay; picking others for a while"); + } + (false, pause) => { + info!( + relay = %Addressed(SCHEME, &relay), + pause = ?pause.unwrap_or_default(), + reason = %reason, + "A relay didn't grant a slot; picking others for a while" + ); + } } } } @@ -1456,43 +1595,54 @@ async fn run( SwarmEvent::Behaviour(BehaviourEvent::Identify( ::libp2p::identify::Event::Received { connection_id, peer_id, info }, )) => { - let mut book = lock(&book); - let listen = published(&info.listen_addrs); - // Every new connection identifies again: logged when it says something new. - if book.knows(peer_id, &listen) { - trace!(peer = %Addressed(SCHEME, &peer_id), connection = %connection_id, "Peer identified again"); - } else { - debug!( - peer = %Addressed(SCHEME, &peer_id), - connection = %connection_id, - listen = ?info.listen_addrs, - observed = %info.observed_addr, - agent = %info.agent_version, - "Peer identified" - ); - } - // Last first: `remember` puts each in front. - for address in listen.into_iter().rev() { - book.remember(peer_id, address); - } - if relaying.public - && info.protocols.contains(&::libp2p::relay::HOP_PROTOCOL_NAME) - && (book.public_relays.len() < MAX_PEERS - || book.public_relays.contains_key(&peer_id)) - && let Some(address) = book - .addresses - .get(&peer_id) - .and_then(|known| known.first()) - .and_then(|address| address.clone().with_p2p(peer_id).ok()) { - book.public_relays.insert(peer_id, address); - } - if info.protocols.contains(&PROTOCOL) { - book.continuum.insert(peer_id); - if book.lan.remove(&peer_id) { - book.discover(peer_id); + let mut book = lock(&book); + let listen = published(&info.listen_addrs); + // Every new connection identifies again: logged when it says something new. + if book.knows(peer_id, &listen) { + trace!(peer = %Addressed(SCHEME, &peer_id), connection = %connection_id, "Peer identified again"); + } else { + debug!( + peer = %Addressed(SCHEME, &peer_id), + connection = %connection_id, + listen = ?info.listen_addrs, + observed = %info.observed_addr, + agent = %info.agent_version, + "Peer identified" + ); + } + // Last first: `remember` puts each in front. + for address in listen.into_iter().rev() { + book.remember(peer_id, address); + } + if relaying.public + && info.protocols.contains(&::libp2p::relay::HOP_PROTOCOL_NAME) + && (book.public_relays.len() < MAX_PEERS + || book.public_relays.contains_key(&peer_id)) + && let Some(address) = book + .addresses + .get(&peer_id) + .and_then(|known| known.first()) + .and_then(|address| address.clone().with_p2p(peer_id).ok()) + { + book.public_relays.insert(peer_id, address); + } + if !info.protocols.contains(&PROTOCOL) + && (book.not_nodes.len() < MAX_PEERS + || book.not_nodes.contains_key(&peer_id)) + { + let relays = info.protocols.contains(&::libp2p::relay::HOP_PROTOCOL_NAME); + book.not_nodes.insert(peer_id, relays); + } + if info.protocols.contains(&PROTOCOL) { + book.not_nodes.remove(&peer_id); + book.continuum.insert(peer_id); + if book.lan.remove(&peer_id) { + book.discover(peer_id); + } } } + upgrade(&mut swarm, &book, peer_id, relaying.local); } SwarmEvent::Behaviour(BehaviourEvent::Mdns( ::libp2p::mdns::Event::Discovered(found), @@ -1507,6 +1657,7 @@ async fn run( book.remember(peer, address.clone()); book.continuum.contains(&peer) }; + upgrade(&mut swarm, &book, peer, relaying.local); if continuum { lock(&book).discover(peer); } else if lock(&book).lan.len() < MAX_PEERS { @@ -1623,7 +1774,9 @@ impl Transport for Libp2pTransport { return Some(relay.to_string()); } let peer = split_peer(&address)?; - if peer != self.peer_id { + address.pop(); + // A peer ID only says nothing about where. + if peer != self.peer_id && !address.is_empty() { lock(&self.book).remember(peer, address); } Some(peer.to_string()) @@ -1693,14 +1846,26 @@ impl Transport for Libp2pTransport { /// A multiaddr ending in a peer ID is that peer; one without is connected to, to /// find out. A bare peer ID resolves if this transport knows where it is. async fn resolve_seed(&self, seed: &str) -> Result<String> { - if let Ok(peer) = PeerId::from_str(seed) { - if lock(&self.book).addresses.contains_key(&peer) { + let address = match PeerId::from_str(seed) { + Ok(peer) => Multiaddr::empty().with(Protocol::P2p(peer)), + Err(_) => { + Multiaddr::from_str(seed).with_context(|| format!("{seed} is no multiaddr"))? + } + }; + // A peer ID only (`/p2p/<peer ID>`): reached where it's known, or through the + // relays we know. + if let [Protocol::P2p(peer)] = address.iter().collect::<Vec<_>>()[..] { + let book = lock(&self.book); + if book.addresses.contains_key(&peer) + || !book.through_relays(peer, self.peer_id).is_empty() + { return Ok(peer.to_string()); } - bail!("no address known for libp2p peer {peer}; give a multiaddr"); + bail!( + "no address known for libp2p peer {peer}, and no relay to reach it through; \ + give a multiaddr, or a relay (libp2p-relays)" + ); } - let address = - Multiaddr::from_str(seed).with_context(|| format!("{seed} is no multiaddr"))?; if let Some(peer) = self.locate(seed) { return Ok(peer); } @@ -1733,6 +1898,17 @@ impl Transport for Libp2pTransport { .with_context(|| format!("{} is no libp2p peer ID", message.peer))?; let started = std::time::Instant::now(); self.connect(peer).await?; + // The protocol goes with the first data (`swarm_config`), so a peer that doesn't + // speak it seems to take the message: once identify has told, say so. + if let Some(relays) = lock(&self.book).not_nodes.get(&peer).copied() { + if relays { + bail!( + "{peer} is a relay, not a node: give a node behind it as the seed \ + (<relay>/p2p-circuit/p2p/<peer ID>, or /p2p/<peer ID> with libp2p-relays)" + ); + } + bail!("{peer} doesn't speak continuum"); + } let connected = started.elapsed(); let mut control = self.control.clone(); let body = message.body.clone(); @@ -1742,6 +1918,23 @@ impl Transport for Libp2pTransport { stream.write_all(&(body.len() as u32).to_be_bytes()).await?; stream.write_all(&body).await?; stream.close().await?; + // Kept until the peer has closed its side too. Over QUIC, dropping a stream + // stops its reading half (STOP_SENDING), and the peer's answer to the + // protocol (sent with the first data, `swarm_config`) then fails, and with it + // the stream, before the peer has read the message: identify, which reads + // its answers, works, while every message is lost. Yamux (TCP) doesn't mind. + // Off the send, which is done. + tokio::spawn(async move { + let mut rest = [0u8; 64]; + let _ = tokio::time::timeout(STREAM_TIMEOUT, async { + while let Ok(read) = stream.read(&mut rest).await { + if read == 0 { + break; + } + } + }) + .await; + }); Ok::<_, anyhow::Error>(opened) }; let timeout = stream_timeout(message.body.len()); @@ -2066,6 +2259,67 @@ mod tests { assert!(a.connection(b.local_id()).is_some()); } + /// A seed that's a peer ID only is reached through the relays the node knows. + #[tokio::test] + async fn a_peer_id_seed_is_reached_through_a_relay() { + let (_r, r_address) = memory_node_with(true, Vec::new(), true); + let (b, _) = memory_node_with(false, vec![r_address.clone()], false); + let b = Arc::new(b); + let mut at_b = received(Arc::clone(&b)); + assert!( + eventually(|| !b.local_addresses().is_empty()).await, + "b got no slot on the relay" + ); + let (a, _) = memory_node_with(true, vec![r_address.clone()], false); + let seed = format!("/p2p/{}", b.local_id()); + assert_eq!(a.resolve_seed(&seed).await.unwrap(), b.local_id()); + assert_eq!(a.resolve_seed(b.local_id()).await.unwrap(), b.local_id()); + // Nothing to dial is remembered. + assert!(!lock(&a.book).addresses.contains_key(&b.peer_id)); + a.send_message(b.local_id(), &status()).await.unwrap(); + let (from, _) = at_b.recv().await.unwrap(); + assert_eq!(from, a.local_id()); + + // Without a relay, there's nowhere to look. + let (c, _) = memory_node(); + assert!(c.resolve_seed(&seed).await.is_err()); + } + + /// b listens nowhere; a, which can be dialled, reaches it through the relay r: b + /// then dials a directly (identify told it where), and both keep that. + #[tokio::test] + async fn a_relayed_connection_is_upgraded_where_one_side_can_be_dialled() { + let (_r, r_address) = memory_node_with(true, Vec::new(), true); + let (b, _) = memory_node_with(false, vec![r_address.clone()], false); + let b = Arc::new(b); + let mut at_b = received(Arc::clone(&b)); + assert!( + eventually(|| !b.local_addresses().is_empty()).await, + "b got no slot on the relay" + ); + let (a, _) = memory_node(); + let relayed = b.local_addresses()[0].clone(); + assert_eq!(a.resolve_seed(&relayed).await.unwrap(), b.local_id()); + a.send_message(b.local_id(), &status()).await.unwrap(); + at_b.recv().await.unwrap(); + assert!( + eventually(|| b.connection(a.local_id()) == Some(Connection::Direct)).await, + "b didn't dial a directly" + ); + assert_eq!(a.connection(b.local_id()), Some(Connection::Direct)); + // The relayed one goes (`DUPLICATE_GRACE`); the direct one stays. + let deadline = tokio::time::Instant::now() + DUPLICATE_GRACE * 2; + while a.stats().libp2p.unwrap().connections.relayed > 0 { + assert!( + tokio::time::Instant::now() < deadline, + "the relayed connection stayed" + ); + tokio::time::sleep(Duration::from_millis(100)).await; + } + b.send_message(a.local_id(), &status()).await.unwrap(); + assert_eq!(a.connection(b.local_id()), Some(Connection::Direct)); + } + /// A relay that is no node (`--relay-only`) relays, and takes no messages itself. #[tokio::test] async fn a_relay_that_is_no_node() { @@ -2100,15 +2354,24 @@ mod tests { let (from, _) = at_b.recv().await.unwrap(); assert_eq!(from, a.local_id()); - // The relay itself doesn't speak continuum: it takes no messages (a send to it - // isn't refused, though: the protocol goes with the first data, `swarm_config`, - // so the relay's refusal comes too late; nodes never send to it, as it never - // proves itself a node). + // The relay itself doesn't speak continuum: it takes no messages, and a send to + // it is refused once identify has told (the relay's own refusal comes too late: + // the protocol goes with the first data, `swarm_config`), saying what to give + // as a seed instead. assert_eq!( a.resolve_seed(&r_address.to_string()).await.unwrap(), r.local_id() ); assert!( + eventually(|| lock(&a.book).not_nodes.get(&r.peer_id) == Some(&true)).await, + "identify didn't tell" + ); + let refused = a.send_message(r.local_id(), &status()).await.unwrap_err(); + assert!( + refused.to_string().contains("is a relay, not a node"), + "{refused:#}" + ); + assert!( r.start_consumer(Box::new(|_, _, _| Box::pin(async { Ok(()) }))) .await .is_err() @@ -2308,6 +2571,37 @@ mod tests { assert_eq!(relay_failure(&Ok(())), "closed"); } + /// Only refusals pause a relay; a slot that ended otherwise is asked for again. + #[test] + fn only_refusals_pause_relays() { + let refused = [ + ("Reservation(Protocol(NoAddressesInReservation))", "x"), + ("Reservation(Status(ResourceLimitExceeded))", "x"), + ("Reservation(Status(PermissionDenied))", "x"), + ("Denied { cause: \"connection gated\" }", "x"), + ("Upgrade(UnsupportedProtocol)", "x"), + ("Dial(WrongPeerId { obtained: … })", "x"), + ( + "Codec(Error(Custom { kind: Other, error: Failed }))", + "Failed", + ), + ]; + for (debug, innermost) in refused { + assert!(relay_refused(debug, innermost), "{debug}"); + } + let not = [ + ("Io(Custom { kind: Other, error: Timeout })", "Timeout"), + ( + "Io(Custom { kind: Other, error: Right(Closed) })", + "connection is closed", + ), + ("Io(…)", "connection reset"), + ]; + for (debug, innermost) in not { + assert!(!relay_refused(debug, innermost), "{debug}"); + } + } + /// libp2p's nested dial errors come out as each address's cause, once. #[test] fn dial_errors_are_short() { |
