//! libp2p as a transport (`--libp2p-enable`, in builds with the `libp2p` feature): //! direct connections over TCP (Noise, Yamux) and QUIC, to addresses this node knows. //! //! A node's transport identity is its libp2p peer ID; where it is, its listen addresses //! (multiaddrs ending in the peer ID, `locations`). Seeds are such multiaddrs, or ones //! without the peer ID, which a connection finds out. Messages go one per stream of the //! `/continuum/1` protocol: their length (4 bytes, big endian), then the message as //! `wire` encodes it. A send is done once the stream is written and closed; a connection //! that can't be made, or a stream that can't be written, refuses it. A stream's protocol //! goes with its first data, without waiting for the other side to agree //! (`swarm_config`): opening a stream costs no round trip. //! //! Connected peers tell each other where they listen (libp2p's identify protocol), so a //! node learns the addresses of peers that dialled it. While connected, a peer's //! identify is all a node goes by for its direct addresses (`Book::identified`): those //! it no longer has, or no longer publishes, are forgotten, whoever still names them. A node publishes where it can be //! reached (`local_addresses`): the addresses it announces (`--libp2p-announce`, e.g. a //! public address forwarded to it), or else those it listens on, loopback left out. //! //! A node nobody can dial (behind NAT, or a closed port) can still be reached through a //! relay: a node of the swarm that relays (`--libp2p-relay`), or one configured //! (`--libp2p-relays`), on which it reserves a slot; its address through the relay //! (`/p2p-circuit/p2p/`) 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 (punched again now and then, `PUNCH_RETRIES`), //! within its limits (`RELAY_CIRCUIT_DURATION`, //! `RELAY_CIRCUIT_BYTES`; a circuit that ends is made again on the next send). Hole //! punching needs both sides to listen (`HolePunching`: at their LAN addresses too); //! 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`), //! by the side with the smaller peer ID: it keeps the direct one with the shortest round //! trip (libp2p's ping, on each connection), so a path over the LAN wins over one //! through a VPN, whichever opened first; between a relay and a node with a slot on it //! too, where a slot on a connection closed is reserved again over the one kept. A //! peer connected directly is dialled again where it might be reached better //! (`explore`): at addresses it tells of later (a new interface: identify pushes them //! at once), and at all of them when this node gains an interface. Otherwise a //! connection made over the WLAN would stay there once ethernet comes. //! //! 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 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. //! //! With `--libp2p-ignore`, address ranges are left out (VPNs): not published, not told //! peers by identify (`Identifying`), not offered for hole punching, not dialled by us //! (`Book::usable`) nor by libp2p unless with an address elsewhere too, and connections //! there refused (`Ignoring`). Ranges this node has no route to are left //! out alike, as long as it has none (`routes`): without IPv6 to the internet, peers' //! public IPv6 addresses; and with `--libp2p-internet-hosts`, its own aren't published //! then. With `--libp2p-prefer`, //! addresses in the ranges named come first, where only so many are kept //! (`Book::preference`). //! //! With `--libp2p-upnp`, a node asks its router to forward its listen ports (UPnP), and //! publishes the router's public address for them with its own (`Book::direct`): then //! it can be dialled from elsewhere, whatever the other side's NAT. //! //! With `--libp2p-mdns`, a node finds libp2p peers on its LAN (multicast DNS). Their //! LAN addresses are noted, and those that turn out to speak continuum (identify says //! so) are seeds while the node joins (`discovered_seeds`): nodes on one LAN find each //! other without configured seeds. //! //! It uses public libp2p infrastructure only with `--libp2p-public`: then it joins the //! IPFS DHT (Kademlia, through the IPFS bootstrap nodes), where it finds addresses of //! peers the member list doesn't have, and public peers that relay; those are picked //! after members that relay. Public relays keep circuits short (by default 2 minutes and //! 128 KiB): enough to punch holes through NATs, not to carry a swarm's traffic. Without //! it, relays are those configured, or members that say they relay. use std::collections::HashMap; use std::path::Path; use std::str::FromStr; use std::sync::{Arc, Mutex, MutexGuard}; use std::time::Duration; use ::libp2p::multiaddr::Protocol; use ::libp2p::swarm::dial_opts::{DialOpts, PeerCondition}; use ::libp2p::swarm::{ConnectionId, SwarmEvent}; use ::libp2p::{Multiaddr, PeerId, StreamProtocol, Swarm, identity::Keypair}; use anyhow::{Context, Result, anyhow, bail}; use async_trait::async_trait; use futures::{AsyncReadExt, AsyncWriteExt, StreamExt}; use tokio::sync::{Semaphore, mpsc, oneshot, watch}; use tracing::{debug, info, trace, warn}; use crate::locks::MutexExt; use crate::transport::stats::{ Connections, DialFailures, HolePunches, Libp2pStats, Reachability, RelaySlots, TransportStats, }; use crate::transport::{ Addressed, Connection, EncodedMessage, HandlerFn, MessageHandler, ParsedMessage, Transport, hand_on_probe, is_probe, wire, }; mod behaviour; mod book; mod config; mod driver; mod errors; mod ignore; mod relays; mod routes; use behaviour::*; use book::*; pub(crate) use config::{Libp2pConfig, check}; use driver::*; use errors::*; use ignore::*; use relays::*; use routes::*; /// The scheme of this transport's addresses (`transport::paths`). pub(crate) const SCHEME: &str = "libp2p"; const PROTOCOL: StreamProtocol = StreamProtocol::new("/continuum/1"); /// Sends taking this long are logged with where the time went (at debug). const SLOW_SEND: Duration = Duration::from_millis(5); /// The largest message read: anyone who can connect can send anything, so this bounds /// what one stream costs. Sync messages above 16 chunks travel as blobs. const MAX_MESSAGE: usize = 128 << 20; /// How long a connection may take to come up. const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); /// A connection nobody uses is closed after this at least; neighbours keep theirs busy, /// with a keepalive within one and a half intervals (`PeerState::take_keepalive_due`), /// so a long interval lengthens it (`idle_timeout`). const IDLE_TIMEOUT: Duration = Duration::from_secs(300); /// How long a connection may idle before it is closed: `IDLE_TIMEOUT`, or three /// keepalive intervals if that is longer. fn idle_timeout(keepalive: Duration) -> Duration { IDLE_TIMEOUT.max(keepalive * 3) } /// Addresses of one dial tried at once (libp2p's default: 8). Hole punching (`HolePunching`) /// dials every address both sides offer, where they listen (LAN, VPNs, public IPv6) and /// where others saw them, often more than 8, while the other side punches towards all of /// them at the same moment: an address dialled only after others failed (each after /// QUIC's 5 s handshake timeout) misses its punch, and the LAN's is often among the /// last. Dials of the member list's addresses (`ADDRESSES_PER_PEER`) are unchanged. const DIAL_CONCURRENCY: std::num::NonZeroU8 = std::num::NonZeroU8::new(32).unwrap(); /// How long a stream may take to open and to carry a message, before what its size adds /// (`stream_timeout`). const STREAM_TIMEOUT: Duration = Duration::from_secs(10); /// The slowest a message is expected to go, in bytes per second. const MIN_RATE: u64 = 1 << 20; /// Inbound streams read at once. const READS: usize = 32; /// Addresses kept per peer, and peers with addresses at most. const ADDRESSES_PER_PEER: usize = 8; /// Addresses known not to be a peer's (`Book::wrong`), per peer at most. const WRONG_PER_PEER: usize = 32; /// Of this node's own, the direct ones (the best first) and those through relays it /// publishes at most (`local_addresses`): a node listening everywhere has many direct /// ones, and those through a relay are what reaches it from behind a NAT. const PUBLISHED_DIRECT: usize = 4; const PUBLISHED_RELAYED: usize = 2; const MAX_PEERS: usize = 4096; /// A relayed connection (circuit) through this node lasts at most this long and carries /// at most this much: generous, since the swarm's own relays may carry sync traffic for /// good where hole punching fails (libp2p's defaults, 2 minutes and 128 KiB, are for /// 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 (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); /// Relays picked among the members, at most, and how often a node checks whether it /// needs them. const AUTO_RELAYS: usize = 2; const RELAY_CHECK: Duration = Duration::from_secs(30); /// 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 directly connected peer's addresses dialled at once for a better path (`explore`). const EXPLORED_AT_ONCE: usize = 4; /// 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 /// mentioned every few seconds (`swarm::topology`). Checked every `PRUNE_EVERY`. const STALE_AFTER: Duration = Duration::from_secs(600); const PRUNE_EVERY: Duration = Duration::from_secs(60); /// Several connections to one peer (both dialled at once, AutoNAT dialling back to see /// whether it can, or a better path explored) are cut to one, the fastest /// (`Book::duplicates`), this long after the last one opened: long enough for what it /// was dialled for, and for each to be pinged since (a new one at once, as it opens; /// the others within libp2p's ping interval, 15 s). Looked for every `DEDUPE_EVERY`. const DUPLICATE_GRACE: Duration = Duration::from_secs(20); /// The same while a peer is connected only through relays: a new relayed connection is /// a hole punch under way (`PUNCH_RETRIES`), which takes its tries (up to a QUIC /// handshake's 5 s each, three times) on that connection. const PUNCH_GRACE: Duration = Duration::from_secs(60); /// A slot lost this soon after its relay's duplicate connections were closed moved to /// the one kept (`Book::rehoming`). const REHOMING_WITHIN: Duration = Duration::from_secs(10); /// After libp2p's hole punching gave up on a peer (three tries, as the relayed /// connection opened), it tries again after these, the last one over and over: what /// failed may have been passing (a NAT's mapping, a busy link). Each time through a new /// connection through the relay, on which the peer starts the punch (`retry_punches`). const PUNCH_RETRIES: [Duration; 3] = [ Duration::from_secs(60), Duration::from_secs(300), Duration::from_secs(1800), ]; const DEDUPE_EVERY: Duration = Duration::from_secs(5); /// How long a message of `bytes` may take on a stream. fn stream_timeout(bytes: usize) -> Duration { STREAM_TIMEOUT + Duration::from_millis(bytes as u64 * 1000 / MIN_RATE) } pub(crate) struct Libp2pTransport { local_id: String, peer_id: PeerId, control: libp2p_stream::Control, commands: mpsc::UnboundedSender, book: Arc>, incoming: tokio::sync::Mutex>, stopped: watch::Receiver, } impl Libp2pTransport { /// Builds the swarm, listens, and runs it in a task of its own. pub(crate) fn start(config: Libp2pConfig) -> Result { let peer_id = config.key.public().to_peer_id(); let features = Features::of(&config); let mut swarm = if config.memory { memory_swarm(config.key, features, config.ignore.clone())? } else { network_swarm(config.key, features, config.ignore.clone())? }; for address in config.listen { swarm .listen_on(address.clone()) .with_context(|| format!("Listening on {address}"))?; } // Told peers whatever range they are in (`Identifying`). swarm.behaviour_mut().identify.announced = config.announce.clone(); for address in &config.announce { // Identify tells peers these. swarm.add_external_address(address.clone()); } let mut control = swarm.behaviour().stream.new_control(); let incoming = if config.continuum { Some( control .accept(PROTOCOL) .map_err(|error| anyhow!("Accepting {PROTOCOL}: {error}"))?, ) } else { None }; let (commands, receiver) = mpsc::unbounded_channel(); let mut book = Book { announced: config.announce, ignored: config.ignore.clone(), preferred: config.prefer.clone(), routes: Routes::asking(config.internet_hosts.clone()), serving: config.relay, ..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, receiver, commands.clone(), Arc::clone(&book), Relaying { serving: config.relay, public: config.public, local: peer_id, }, stopping, )); info!(peer_id = %peer_id, "Transport started"); Ok(Self { local_id: peer_id.to_string(), peer_id, control, commands, book, incoming: tokio::sync::Mutex::new(incoming), stopped, }) } /// Connected to `peer`, if it can be had in time. async fn connect(&self, peer: PeerId) -> Result<()> { let addresses = { let mut book = lock(&self.book); let known = book.usable(&peer, self.peer_id); if known.is_empty() { book.through_relays(peer, self.peer_id) } else { known } }; let (done, connected) = oneshot::channel(); self.commands .send(Command::Connect { peer, addresses, done, }) .map_err(|_| anyhow!("the libp2p swarm stopped"))?; match tokio::time::timeout(CONNECT_TIMEOUT, connected).await { Ok(Ok(Ok(()))) => Ok(()), Ok(Ok(Err(error))) => bail!("couldn't connect to {peer}: {error}"), Ok(Err(_)) => bail!("the libp2p swarm stopped"), Err(_) => bail!("connecting to {peer} took longer than {CONNECT_TIMEOUT:?}"), } } } /// One message from an inbound stream. async fn read_message(mut stream: ::libp2p::Stream) -> Result> { let mut length = [0u8; 4]; tokio::time::timeout(STREAM_TIMEOUT, stream.read_exact(&mut length)) .await .map_err(|_| anyhow!("no length within {STREAM_TIMEOUT:?}"))??; let length = u32::from_be_bytes(length) as usize; if length > MAX_MESSAGE { bail!("a message of {length} bytes; at most {MAX_MESSAGE} are read"); } let mut body = vec![0u8; length]; tokio::time::timeout(stream_timeout(length), stream.read_exact(&mut body)) .await .map_err(|_| { anyhow!( "{length} bytes took longer than {:?}", stream_timeout(length) ) })??; Ok(body) } #[async_trait] impl Transport for Libp2pTransport { fn local_id(&self) -> &str { &self.local_id } fn is_peer_id(&self, id: &str) -> bool { PeerId::from_str(id).is_ok() } /// The longest a message may take on a stream. fn delivery_limit(&self) -> Duration { stream_timeout(MAX_MESSAGE) } /// Where we can be reached, each with our peer ID: the announced addresses, or else /// those we listen on (`published`), and those through relays; at most /// `PUBLISHED_DIRECT` and `PUBLISHED_RELAYED` of them, so the direct ones never /// crowd out the relayed ones. A relay adds each direct one again, ending in /// `/p2p-circuit`: where to reserve a slot. fn local_addresses(&self) -> Vec { let book = lock(&self.book); let direct: Vec = book .direct() .into_iter() .filter_map(|address| address.with_p2p(self.peer_id).ok()) .take(PUBLISHED_DIRECT) .collect(); let relayed = book .listening .iter() .filter(|address| is_relayed(address)) .filter_map(|address| address.clone().with_p2p(self.peer_id).ok()) .take(PUBLISHED_RELAYED); let offers: Vec = if book.serving { direct .iter() .map(|address| address.clone().with(Protocol::P2pCircuit)) .collect() } else { Vec::new() }; direct .into_iter() .chain(relayed) .chain(offers) .map(|address| address.to_string()) .collect() } /// A peer ID, or a multiaddr ending in one (noted as where that peer is). fn locate(&self, target: &str) -> Option { if let Ok(peer) = PeerId::from_str(target) { return Some(peer.to_string()); } let mut address = Multiaddr::from_str(target).ok()?; // A relay saying where to reserve a slot: `
/p2p//p2p-circuit`. if let Some(Protocol::P2pCircuit) = address.iter().last() { address.pop(); let relay = split_peer(&address)?; if relay != self.peer_id { let new = { let mut book = lock(&self.book); // One in a range left out is no use (`Book::left_out`). let new = book.relays.get(&relay) != Some(&address) && !book.left_out(&address); if new && (book.relays.len() < MAX_PEERS || book.relays.contains_key(&relay)) { book.relays.insert(relay, address.clone()); } book.remember(relay, address); new }; if new { let _ = self.commands.send(Command::CheckRelays); } } return Some(relay.to_string()); } let peer = split_peer(&address)?; 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()) } /// 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 { let peer = PeerId::from_str(target).ok()?; lock(&self.book).connection(&peer) } fn punching(&self, target: &str) -> Option { let peer = PeerId::from_str(target).ok()?; let (failed, next) = *lock(&self.book).punching.get(&peer)?; Some(crate::transport::Punching { failed, next_in_secs: next .saturating_duration_since(tokio::time::Instant::now()) .as_secs(), }) } fn location(&self, target: &str) -> Option { let peer = PeerId::from_str(target).ok()?; let address = lock(&self.book).location(&peer)?.clone(); Some( address .with_p2p(peer) .unwrap_or_else(|address| address) .to_string(), ) } fn stats(&self) -> TransportStats { TransportStats { libp2p: Some(lock(&self.book).stats()), ..TransportStats::default() } } fn on_seeds_found(&self, found: Arc) { lock(&self.book).seeds_found = Some(found); } fn on_reachable(&self, reachable: Arc) { lock(&self.book).reachable = Some(reachable); } /// Nodes found on the LAN (mDNS) that speak continuum. fn discovered_seeds(&self) -> Vec { std::mem::take(&mut lock(&self.book).discovered) .iter() .map(Multiaddr::to_string) .collect() } /// Where `target` is, each address with its peer ID. fn locations(&self, target: &str) -> Vec { let Ok(peer) = PeerId::from_str(target) else { return Vec::new(); }; lock(&self.book) .addresses .get(&peer) .into_iter() .flatten() .filter_map(|address| address.clone().with_p2p(peer).ok()) .map(|address| address.to_string()) .collect() } /// 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 { 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/`): reached where it's known, or through the // relays we know. if let [Protocol::P2p(peer)] = address.iter().collect::>()[..] { let mut 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}, and no relay to reach it through; \ give a multiaddr, or a relay (libp2p-relays)" ); } if let Some(peer) = self.locate(seed) { return Ok(peer); } let (done, identified) = oneshot::channel(); self.commands .send(Command::Identify { address, done }) .map_err(|_| anyhow!("the libp2p swarm stopped"))?; match tokio::time::timeout(CONNECT_TIMEOUT, identified).await { Ok(Ok(Ok(peer))) => Ok(peer.to_string()), Ok(Ok(Err(error))) => bail!("couldn't connect to {seed}: {error}"), Ok(Err(_)) => bail!("the libp2p swarm stopped"), Err(_) => bail!("connecting to {seed} took longer than {CONNECT_TIMEOUT:?}"), } } fn encode(&self, peer: &str, message: &ParsedMessage) -> Result { wire::encoded(peer, message) } async fn send_encoded(&self, message: &EncodedMessage) -> Result<()> { let peer = PeerId::from_str(&message.peer) .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 \ (/p2p-circuit/p2p/, or /p2p/ with libp2p-relays)" ); } bail!("{peer} doesn't speak continuum"); } let connected = started.elapsed(); let mut control = self.control.clone(); let body = message.body.clone(); let write = async move { let mut stream = control.open_stream(peer, PROTOCOL).await?; let opened = started.elapsed(); 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()); let opened = tokio::time::timeout(timeout, write) .await .map_err(|_| anyhow!("sending to {peer} took longer than {timeout:?}"))? .with_context(|| format!("Sending to {peer}"))?; let took = started.elapsed(); if took >= SLOW_SEND { // Where a send's time goes: the swarm task (connecting), the stream's protocol // negotiation (opening), or the connection (writing). debug!( peer = %Addressed(SCHEME, &peer), kind = ?message.kind, bytes = message.body.len(), connect_ms = connected.as_secs_f64() * 1000.0, open_ms = (opened - connected).as_secs_f64() * 1000.0, write_ms = (took - opened).as_secs_f64() * 1000.0, "Slow send" ); } Ok(()) } /// Reads inbound streams (`READS` at once) and handles their messages one at a /// time, in the order they were read; PINGs and PONGs at once (`is_probe`). async fn start_consumer(&self, handler: MessageHandler) -> Result<()> { let Some(mut incoming) = self.incoming.lock().await.take() else { bail!("libp2p consumer already started, or this node only relays"); }; let handler: Arc = Arc::from(handler); let (sender, mut received) = mpsc::channel::<(PeerId, ParsedMessage, usize)>(READS); let reads = Arc::new(Semaphore::new(READS)); let probes = Arc::clone(&handler); tokio::spawn(async move { while let Some((peer, stream)) = incoming.next().await { let Ok(permit) = Arc::clone(&reads).acquire_owned().await else { break; }; let sender = sender.clone(); let probes = Arc::clone(&probes); tokio::spawn(async move { match read_message(stream).await { Ok(body) => match wire::decode(&body) { Ok(message) if is_probe(&message) => { hand_on_probe( SCHEME, &probes, peer.to_string(), message, body.len(), ); } Ok(message) => { let _ = sender.send((peer, message, body.len())).await; } Err(error) => { debug!(peer = %Addressed(SCHEME, &peer), error = %error, "Dropping a message that doesn't decode"); } }, Err(error) => { debug!(peer = %Addressed(SCHEME, &peer), error = %format!("{error:#}"), "Dropping an unreadable stream"); } } drop(permit); }); } }); while let Some((peer, message, payload_len)) = received.recv().await { if let Err(error) = handler(peer.to_string(), message, payload_len).await { warn!(peer = %Addressed(SCHEME, &peer), error = %format!("{error:#}"), "Failed to process inbound message"); } } Ok(()) } async fn wait_for_exit(&self) -> std::io::Result { let mut stopped = self.stopped.clone(); let _ = stopped.wait_for(|stopped| *stopped).await; Err(std::io::Error::other("the libp2p swarm stopped")) } } #[cfg(test)] mod tests;