//! The swarm task — owns the libp2p `Swarm`, the HTTP command channel, //! and the kad-DHT/bootstrap/peer-directory state, and drives them all //! from one `tokio::select!` loop. //! //! Refactored from the previous monolithic `swarm::spawn` closure into a //! `SwarmTask` struct with per-arm handler methods. The select! itself //! stays in [`SwarmTask::run`] because tokio's macro needs all arm futures //! to be constructed in one scope — `swarm` and `rx` are passed in as //! locals (not held on `self`) so each handler method can borrow //! `&mut self` cleanly without conflicting with the arm futures. use std::collections::HashMap; use std::time::Duration; use futures::StreamExt; use libp2p::core::ConnectedPoint; use libp2p::kad::{self, GetProvidersOk, QueryResult, RecordKey}; use libp2p::mdns; use libp2p::swarm::SwarmEvent; use libp2p::{identify, Multiaddr, PeerId, Swarm}; use tokio::sync::mpsc; use tracing::{debug, info, trace, warn}; use crate::gateway_discovery::{advertises_gateways, fetch_gateway_caps}; use super::bootstrap::{addr_targets_match, redial_bootstrap, BootstrapEntry}; use super::bitswap_client::BitswapInflight; use super::identify_agent::parse_base_url; use super::kad_discovery::{is_dialable_addr, namespace_key, peer_id_from_multiaddr, KadConfig}; use super::peer_directory::PeerDirectory; use super::{Behaviour, BehaviourEvent, Command, GatewayCapsConfig, PeersInfo}; /// How often [`SwarmTask::on_gateway_keepalive_tick`] re-dials dropped /// gateways and re-stamps live ones. Half meta-search's 60 s: an NZB play is /// blocked for as long as the gateway is missing (no one else can redeem). const GATEWAY_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(30); /// A gateway we've identified and now pin for keep-alive: its API base URL (for /// the periodic caps refresh that keeps it routable) plus dialable addresses /// (for re-pinning the connection if it drops). Persists across TTL expiry and /// disconnect — unlike the read-side directory entry, which `forget_peer` drops /// on the last close — so a gateway is never permanently forgotten once seen. /// /// Ported from meta-search. Without it a gateway that restarts under the same /// peer id is lost for good: its mDNS record never expires, so `Discovered` /// never fires again, and the gateway itself never dials consumers /// (watch.nsl.sh 2026-10-04: every NZB answered "no gateway can redeem it" /// until this container was restarted). #[derive(Clone, Debug, PartialEq)] struct GatewayPin { base_url: Option, addrs: Vec, } /// The pin an identify from a gateway yields, or `None` for any other peer. /// Only dialable addresses are kept: a pin is retried for the life of the /// process, so a loopback / unspecified listener would be a permanent cost. fn gateway_pin(agent_version: &str, listen_addrs: &[Multiaddr], base_url: Option) -> Option { if !advertises_gateways(agent_version) { return None; } Some(GatewayPin { base_url, addrs: listen_addrs.iter().filter(|a| is_dialable_addr(a)).cloned().collect(), }) } /// All the state that lives across iterations of the event loop. `swarm` /// and the channel receivers stay as locals in [`SwarmTask::run`] so the /// `tokio::select!` arms can borrow them without conflicting with /// `&mut self` calls in the arm bodies. pub(super) struct SwarmTask { local_peer_id: PeerId, /// Bootstrap targets we keep alive across reconnects. bootstrap: Vec, /// Shared peer→info directory (populated from identify events). peer_directory: PeerDirectory, /// kad-DHT discovery state. kad_namespace: String, ns_key: RecordKey, kad_target_peer_count: usize, kad_fast_discovery_interval: Duration, kad_discovery_interval: Duration, kad_provide_interval: Duration, /// Whether to publish the namespace provider record at all — see /// [`KadConfig::provide_enabled`]. False when this peer isn't publicly /// reachable. kad_provide_enabled: bool, kad_bootstrap_peers: Vec, /// Rate-limit floor for reactive kad kicks on `ConnectionClosed`. last_kad_query_at: tokio::time::Instant, /// Auto-redial cadence; zero / empty bootstrap effectively disables. redial_interval: Duration, /// In-flight `bitswap.get(cid)` calls awaiting block delivery. /// Populated by `Command::BitswapGet`, drained by the /// `BehaviourEvent::Bitswap` arm of `on_swarm_event`. bitswap_inflight: BitswapInflight, /// Process-wide HTTP client, for the gateway capability fetch off identify. http: reqwest::Client, /// Timeout + debounce for that fetch. gateway_caps: GatewayCapsConfig, /// Gateways kept connected + routable by the keep-alive tick. See /// [`GatewayPin`]. pinned_gateways: HashMap, } impl SwarmTask { #[allow(clippy::too_many_arguments)] pub(super) fn new( local_peer_id: PeerId, bootstrap_peers: Vec, redial_interval: Duration, kad_config: KadConfig, peer_directory: PeerDirectory, http: reqwest::Client, gateway_caps: GatewayCapsConfig, ) -> Self { let bootstrap: Vec = bootstrap_peers.into_iter().map(BootstrapEntry::new).collect(); let ns_key = namespace_key(&kad_config.namespace); // Initialise far enough in the past that the very first reactive kick // can fire (subject to the fast-discovery rate-limit floor). let last_kad_query_at = tokio::time::Instant::now() .checked_sub(kad_config.fast_discovery_interval) .unwrap_or_else(tokio::time::Instant::now); Self { local_peer_id, bootstrap, peer_directory, kad_namespace: kad_config.namespace, ns_key, kad_target_peer_count: kad_config.target_peer_count, kad_fast_discovery_interval: kad_config.fast_discovery_interval, kad_discovery_interval: kad_config.discovery_interval, kad_provide_interval: kad_config.provide_interval, kad_provide_enabled: kad_config.provide_enabled, kad_bootstrap_peers: kad_config.bootstrap_peers, last_kad_query_at, redial_interval, bitswap_inflight: BitswapInflight::new(), http, gateway_caps, pinned_gateways: HashMap::new(), } } /// Drive the swarm task to completion. Owns `swarm` and the channel /// receivers as locals — the `tokio::select!` arms borrow these /// directly, and the arm bodies are free to call `&mut self` methods /// because the arm futures have completed (and their borrows /// released) by the time a body runs. pub(super) async fn run( mut self, mut swarm: Swarm, mut rx: mpsc::Receiver, ) { // Initial bootstrap dials. for entry in &self.bootstrap { match swarm.dial(entry.addr.clone()) { Ok(()) => info!(addr = %entry.addr, "bootstrap dial issued"), Err(e) => warn!(addr = %entry.addr, error = %e, "bootstrap dial failed"), } } // Seed kad's routing table with the configured bootstrap multiaddrs. // Each entry must include `/p2p/` so we can register the // peer-id without an extra round-trip; entries without are skipped // with a warning (the kad routing table is keyed by peer-id). let mut kad_seeded = 0usize; for addr in &self.kad_bootstrap_peers { match peer_id_from_multiaddr(addr) { Some(pid) => { swarm.behaviour_mut().kad.add_address(&pid, addr.clone()); kad_seeded += 1; debug!(%pid, %addr, "kad: bootstrap address registered"); } None => warn!(%addr, "kad: bootstrap multiaddr lacks /p2p/; skipped"), } } if kad_seeded > 0 { match swarm.behaviour_mut().kad.bootstrap() { Ok(qid) => info!(?qid, kad_seeded, "kad: bootstrap query issued"), Err(e) => warn!(error = %e, "kad: bootstrap query failed"), } } else { // Not a fault — it's the local-mode default. An empty bootstrap list // is exactly what keeps an unreachable peer off the public DHT, so // saying it at WARN would train operators to ignore a real warning. info!( "kad: no bootstrap peers configured; public-DHT discovery is off \ (local mode — mDNS still works). Set PUBLIC_ADDR to join the public DHT." ); } info!(namespace = %self.kad_namespace, "kad: discovery namespace"); // Auto-redial ticker. `redial_interval == 0` disables; we still need a // ticker (`tokio::select!` requires all branches to compile) so use a // very long interval that effectively never fires. let effective_interval = if self.redial_interval.is_zero() || self.bootstrap.is_empty() { Duration::from_secs(60 * 60 * 24 * 365) // 1y; effectively never } else { self.redial_interval }; let mut redial_tick = tokio::time::interval(effective_interval); // Skip the immediate first tick — we just did the initial dials above. redial_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); redial_tick.tick().await; // kad provide ticker — fires immediately so we publish on startup, // then every kad_provide_interval. Provider records on remote peers // expire (~24 h default), so this must be lower than that. let mut kad_provide_tick = tokio::time::interval(self.kad_provide_interval); kad_provide_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); // kad discovery ticker — same pattern, fires immediately. let mut kad_discovery_tick = tokio::time::interval(self.kad_discovery_interval); kad_discovery_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); // Keep known gateways connected + routable (re-dial dropped ones, // re-fetch caps on live ones). See [`GatewayPin`]. let mut gateway_keepalive_tick = tokio::time::interval(GATEWAY_KEEPALIVE_INTERVAL); gateway_keepalive_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); loop { tokio::select! { cmd = rx.recv() => match cmd { Some(cmd) => self.on_command(&mut swarm, cmd).await, None => { debug!("command channel closed; shutting down swarm task"); break; } }, _ = redial_tick.tick() => { redial_bootstrap(&mut swarm, &self.bootstrap); }, _ = kad_provide_tick.tick() => { // A provider record is a promise that this peer can be // dialed. In local mode it can't be (no public address, // only container-internal listeners), so publishing one // would send every peer that walks the DHT into a dial // that can never connect. Stay quiet. if self.kad_provide_enabled { match swarm.behaviour_mut().kad.start_providing(self.ns_key.clone()) { Ok(qid) => debug!(?qid, namespace = %self.kad_namespace, "kad: re-publishing provider record"), Err(e) => warn!(error = %e, namespace = %self.kad_namespace, "kad: start_providing failed"), } } }, _ = kad_discovery_tick.tick() => { self.on_kad_discovery_tick(&mut swarm, &mut kad_discovery_tick); }, _ = gateway_keepalive_tick.tick() => { self.on_gateway_keepalive_tick(&mut swarm); }, event = swarm.select_next_some() => { self.on_swarm_event(&mut swarm, event, &mut kad_discovery_tick).await; } } } } /// Handle a single HTTP-layer command. async fn on_command(&mut self, swarm: &mut Swarm, cmd: Command) { match cmd { Command::Peers { reply } => { let connected: Vec = swarm.connected_peers().map(|p| p.to_string()).collect(); let info = PeersInfo { local_peer_id: self.local_peer_id.to_string(), connected, }; let _ = reply.send(info); } Command::BitswapGet { cid, reply } => { // M12: issue a bitswap fetch for one block. The // behaviour fans WANT-HAVE / WANT-BLOCK to connected // peers (gateways + sibling consumers that already // cached the cid). `bitswap.get` returns a QueryId; // we register it with the inflight map and the // `BehaviourEvent::Bitswap` arm fires the reply when // either `GetQueryResponse` (success) or // `GetQueryError` (transport / no-providers / // timeout) arrives. let query_id = swarm.behaviour_mut().bitswap.get(&cid); tracing::debug!(%cid, ?query_id, "bitswap: issued .get()"); self.bitswap_inflight.register(query_id, reply); } Command::Provide { cid } => { // Announce on the public IPFS DHT, keyed by the CID's // multihash bytes — that's the kubo-compatible provider key, // so a vanilla IPFS node `get_providers`ing the same content // finds us. The StartProviding result is logged in // `default_log`. Works in `Mode::Client` (same call the // gateway makes). let key = RecordKey::new(&cid.hash().to_bytes()); match swarm.behaviour_mut().kad.start_providing(key) { Ok(qid) => debug!(?qid, %cid, "kad: providing seeded cid"), Err(e) => warn!(error = %e, %cid, "kad: start_providing(cid) failed"), } } Command::StopProviding { cid } => { let key = RecordKey::new(&cid.hash().to_bytes()); swarm.behaviour_mut().kad.stop_providing(&key); debug!(%cid, "kad: stopped providing seeded cid"); } } } /// Fired every `kad_discovery_interval` (steady-state) or /// `kad_fast_discovery_interval` (below floor). Issues a fresh /// `get_providers` query and adapts the next tick's cadence based on /// the current connection count. fn on_kad_discovery_tick( &mut self, swarm: &mut Swarm, kad_discovery_tick: &mut tokio::time::Interval, ) { let connected = swarm.connected_peers().count(); let qid = swarm.behaviour_mut().kad.get_providers(self.ns_key.clone()); self.last_kad_query_at = tokio::time::Instant::now(); // Adapt the next tick's cadence to whether we're below the // connection-floor. `reset_after` only affects the *next* tick — // current behaviour keeps the steady-state config-driven topup rate // when the swarm is full and accelerates discovery when it's not. let next = if connected < self.kad_target_peer_count { self.kad_fast_discovery_interval } else { self.kad_discovery_interval }; debug!( ?qid, namespace = %self.kad_namespace, connected, target = self.kad_target_peer_count, next_tick_secs = next.as_secs(), "kad: querying for providers" ); kad_discovery_tick.reset_after(next); } /// Top-level swarm-event dispatch. Each branch inspects `&event` and /// falls through to `default_log` at the end. /// Keep pinned gateways connected + routable (ported from meta-search): /// /// 1. **Connection drop** — re-dial a pinned gateway that isn't connected, /// directly (not through the mDNS / kad-provider dial-gates), re-seeding /// its addresses into kad first in case they were evicted. The redial /// triggers identify, which restores its base URL and redeem claims. /// 2. **Directory staleness** — identify goes quiet on an idle connection, /// so the 600 s gateway TTL can lapse on a live peer. Re-fetch caps /// over HTTP to re-stamp it. fn on_gateway_keepalive_tick(&mut self, swarm: &mut Swarm) { // Small, stable set — clone to sidestep the self/swarm borrow overlap. for (pid, pin) in self.pinned_gateways.clone() { if swarm.is_connected(&pid) { if self .peer_directory .needs_cap_refresh(&pid, self.gateway_caps.refresh_after) { if let Some(base_url) = pin.base_url { self.spawn_caps_fetch(pid, base_url); } } } else if pin.addrs.is_empty() { // Every address it announced was undialable — most likely a // gateway without PUBLIC_ADDR seen only over a relay. A dial // would fail every tick for the life of the process. trace!(peer_id = %pid, "gateway keep-alive: no dialable address for pinned gateway; skipping re-dial"); } else { for addr in &pin.addrs { swarm.behaviour_mut().kad.add_address(&pid, addr.clone()); } match swarm.dial(pid) { Ok(()) => debug!(peer_id = %pid, "gateway keep-alive: re-dialing dropped gateway"), Err(e) => debug!(peer_id = %pid, error = %e, "gateway keep-alive: re-dial failed"), } } } } /// Fetch a gateway's capabilities over HTTP off its `base_url` and record /// them in the directory. Fire-and-forget: a slow/dead gateway must not /// stall the swarm loop; a failure keeps whatever was held (it ages out on /// its own TTL) and is retried on the next identify / keep-alive tick. fn spawn_caps_fetch(&self, pid: PeerId, base_url: String) { let http = self.http.clone(); let dir = self.peer_directory.clone(); let timeout = self.gateway_caps.fetch_timeout; tokio::spawn(async move { match fetch_gateway_caps(&http, &base_url, timeout).await { Ok(caps) => { let redeems = caps.redeem_claims(); debug!(peer_id = %pid, base_url = %base_url, nzb_fetch = caps.nzb_fetch, redeem_claims = redeems.len(), "gateway caps: fetched"); dir.record_gateway_caps(pid, caps.nzb_fetch, redeems); } Err(e) => debug!(peer_id = %pid, base_url = %base_url, error = %e, "gateway caps: fetch failed"), } }); } async fn on_swarm_event( &mut self, swarm: &mut Swarm, event: SwarmEvent, kad_discovery_tick: &mut tokio::time::Interval, ) { // 0) Soft ceiling for inbound: when we're already at target, // close inbound connections before they negotiate. Pre-handshake // close — peer-id is not known yet, only the source address. // Bootstrap dials and our own outgoing dials are unaffected // (they go through the dial-gates in the mdns/kad branches // below). if let SwarmEvent::IncomingConnection { connection_id, .. } = &event { let cid = *connection_id; let connected = swarm.connected_peers().count(); if connected >= self.kad_target_peer_count { debug!(?cid, connected, target = self.kad_target_peer_count, "rejecting inbound connection; at target"); let _ = swarm.close_connection(cid); } } // 1) On every outgoing-dial connection, opportunistically populate // bootstrap[].peer_id so future redial ticks can skip // already-connected entries. if let SwarmEvent::ConnectionEstablished { peer_id, endpoint, .. } = &event { if let ConnectedPoint::Dialer { address, .. } = endpoint { // Always overwrite the cached peer-id on a fresh outgoing // dial, not just on first contact. Bootstrap targets that // lack a persistent peer-id (today: every meta-share peer // in the POC) rotate their identity on restart, and a // stale cached peer-id makes is_connected() return false // on the next tick — causing a redundant dial that // doubles the connection count. for entry in self.bootstrap.iter_mut() { if addr_targets_match(&entry.addr, address) && entry.peer_id != Some(*peer_id) { entry.peer_id = Some(*peer_id); debug!(peer_id = %peer_id, addr = %entry.addr, "auto-redial: bootstrap target peer-id learned/refreshed"); } } } } // 1b) On disconnect: if we drop below the connection floor, kick // the kad discovery tick to fire immediately so a replacement // peer is found quickly — but only if the last query was at // least `fast_discovery_interval` ago, so a flapping peer // can't trigger DHT queries faster than that floor. // `reset_after(0)` makes the *next* tick fire now; if we're // already mid-query the in-flight one continues. if let SwarmEvent::ConnectionClosed { peer_id, num_established, .. } = &event { // On the *last* connection to a peer closing, drop its gateway // state immediately — the fast complement to the read-side TTL // in PeerDirectory. A peer whose libp2p id rotates on restart // (the dev-stack norm) otherwise keeps advertising upstreams it // no longer serves, and the M7 fan-out burns a slot dialing the // dead id. `num_established` is the count remaining *after* this // close, so 0 means the peer is fully gone. if *num_established == 0 && self.peer_directory.forget_peer(peer_id) { debug!(peer_id = %peer_id, "peer fully disconnected — forgot gateway state"); } let connected = swarm.connected_peers().count(); if connected < self.kad_target_peer_count && self.last_kad_query_at.elapsed() >= self.kad_fast_discovery_interval { debug!(connected, target = self.kad_target_peer_count, "kad: below connection floor — kicking discovery"); kad_discovery_tick.reset_after(Duration::ZERO); } } // 2) When the identify protocol delivers a peer's listen // addresses, feed them into kad's routing table and parse // `baseUrl=` into the peer directory (the byte-fetch path // resolves a peer's HTTP API URL from here). if let SwarmEvent::Behaviour(BehaviourEvent::Identify( identify::Event::Received { peer_id, info, .. } )) = &event { let pid = *peer_id; // Only what a remote peer could actually dial: a container behind // docker's bridge reports 0.0.0.0 / 127.0.0.1 listeners too, and // the keep-alive below would retry a pinned bad address forever. for addr in info.listen_addrs.iter().filter(|a| is_dialable_addr(a)) { swarm.behaviour_mut().kad.add_address(&pid, addr.clone()); } let base_url = parse_base_url(&info.agent_version).map(str::to_string); let update = self.peer_directory.upsert_from_identify(pid, base_url); if let Some(url) = &update.base_url_changed { debug!(peer_id = %pid, base_url = %url, "identify: learned/refreshed peer base URL"); } // Pin every identified gateway for the keep-alive tick, refreshing // its addresses each time, so a dropped gateway gets re-dialed. if let Some(pin) = gateway_pin( &info.agent_version, &info.listen_addrs, self.peer_directory.base_url(&pid), ) { self.pinned_gateways.insert(pid, pin); } // Gateway discovery. A non-empty `gateways=` token is emitted by // meta-gateway and nobody else, so it's the "this peer is a // gateway" signal. Its *capabilities* aren't on the wire though — // libp2p 0.56 can't mutate agent_version after startup, so a // runtime plugin toggle would never reach us. We fetch them over // plain HTTP off the just-learned baseUrl instead (the reliable // source that replaced the flaky gossipsub heartbeat). // // Debounced by the directory so the identify burst on a // multi-connection peer doesn't fan out into redundant fetches. // The keep-alive tick re-stamps it when identify goes quiet. if advertises_gateways(&info.agent_version) && self .peer_directory .needs_cap_refresh(&pid, self.gateway_caps.refresh_after) { if let Some(base_url) = self.peer_directory.base_url(&pid) { self.spawn_caps_fetch(pid, base_url); } } } // 2b) mDNS: register the address in kad's routing table (so // subsequent lookups dedupe) and dial — unless at target. if let SwarmEvent::Behaviour(BehaviourEvent::Mdns( mdns::Event::Discovered(peers) )) = &event { for (pid, addr) in peers.iter() { if *pid == self.local_peer_id { continue; } swarm.behaviour_mut().kad.add_address(pid, addr.clone()); if swarm.is_connected(pid) { trace!(peer_id = %pid, %addr, "mdns: peer already connected"); continue; } // LAN-local (mDNS) peers bypass the public-peer connection // ceiling and are always dialed — this is the local mesh (the // gateway + sibling peers) and it must never be starved by a // busy public DHT that has pushed `connected` past `target`. // mDNS is LAN-scoped, so the set stays small (no unbounded-dial // risk); remote/public peers still respect the ceiling below. match swarm.dial(*pid) { Ok(()) => debug!(peer_id = %pid, %addr, "mdns: dialing newly-discovered peer"), Err(e) => debug!(peer_id = %pid, %addr, error = %e, "mdns: dial failed"), } } } // 2c.5) Bitswap (M12) — fire the reply oneshot for an outbound // `.get(cid)` when bitswap delivers the block or surfaces // a query error. The same Behaviour also serves inbound // WANT-HAVE/WANT-BLOCK from our local blockstore; // those are intercepted by the Behaviour internally and // never bubble up as `beetswap::Event` variants, so this // arm only handles outbound results. if let SwarmEvent::Behaviour(BehaviourEvent::Bitswap(ev)) = &event { let fired = self.bitswap_inflight.on_event(ev); if fired { trace!("bitswap: inflight query resolved"); } } // 3) kad providers — dial each new peer-id (subject to soft ceiling). if let SwarmEvent::Behaviour(BehaviourEvent::Kad( kad::Event::OutboundQueryProgressed { result: QueryResult::GetProviders(Ok(GetProvidersOk::FoundProviders { providers, .. })), .. } )) = &event { for pid in providers.iter() { if *pid == self.local_peer_id { continue; // ourselves } if swarm.is_connected(pid) { trace!(peer_id = %pid, "kad: provider already connected"); continue; } let connected = swarm.connected_peers().count(); if connected >= self.kad_target_peer_count { trace!(peer_id = %pid, connected, target = self.kad_target_peer_count, "kad: at target — skipping dial of provider"); continue; } match swarm.dial(*pid) { Ok(()) => debug!(peer_id = %pid, "kad: dialing newly-discovered provider"), Err(e) => debug!(peer_id = %pid, error = %e, "kad: dial of discovered provider failed"), } } } default_log(event); } } /// Generic swarm-event logger — catches everything worth surfacing at /// info/debug/trace level after `on_swarm_event`'s explicit branches. fn default_log(event: SwarmEvent) { match event { SwarmEvent::NewListenAddr { address, .. } => { info!(%address, "swarm listening"); } SwarmEvent::ConnectionEstablished { peer_id, .. } => { trace!(%peer_id, "connection established"); } SwarmEvent::ConnectionClosed { peer_id, cause, .. } => { trace!(%peer_id, ?cause, "connection closed"); } SwarmEvent::IncomingConnectionError { error, .. } => { warn!(%error, "incoming connection error"); } SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => { warn!(?peer_id, %error, "outgoing connection error"); } SwarmEvent::Behaviour(BehaviourEvent::Identify(identify::Event::Received { peer_id, info, .. })) => { trace!(%peer_id, listen_addrs = info.listen_addrs.len(), "identify received"); } SwarmEvent::Behaviour(BehaviourEvent::Identify(_)) => { // identify::Event::{Sent,Pushed,Error} — not interesting at info level } SwarmEvent::Behaviour(BehaviourEvent::Kad(kad::Event::OutboundQueryProgressed { result: QueryResult::GetProviders(Ok(GetProvidersOk::FoundProviders { providers, .. })), .. })) => { debug!(provider_count = providers.len(), "kad: GetProviders ok"); } SwarmEvent::Behaviour(BehaviourEvent::Kad(kad::Event::OutboundQueryProgressed { result: QueryResult::StartProviding(res), .. })) => match res { Ok(_) => debug!("kad: StartProviding ok"), Err(e) => warn!(error = ?e, "kad: StartProviding error"), }, SwarmEvent::Behaviour(BehaviourEvent::Kad(kad::Event::OutboundQueryProgressed { result: QueryResult::Bootstrap(res), .. })) => match res { Ok(_) => trace!("kad: Bootstrap step ok"), Err(e) => debug!(error = ?e, "kad: Bootstrap step error"), }, SwarmEvent::Behaviour(BehaviourEvent::Kad(_)) => { // RoutingUpdated, InboundRequest (won't happen in client mode), // ModeChanged, etc. — quiet at info. } SwarmEvent::Behaviour(BehaviourEvent::Mdns(mdns::Event::Discovered(peers))) => { trace!(peer_count = peers.len(), "mdns: discovered local peers"); } SwarmEvent::Behaviour(BehaviourEvent::Mdns(mdns::Event::Expired(peers))) => { trace!(peer_count = peers.len(), "mdns: expired local peers"); } _ => {} } } #[cfg(test)] mod tests { use super::*; fn ma(s: &str) -> Multiaddr { s.parse().unwrap() } #[test] fn only_a_gateway_is_pinned() { let addrs = [ma("/ip4/172.18.0.16/tcp/4002")]; assert_eq!(gateway_pin("meta-share/2.3.0 baseUrl=http://x:3000", &addrs, None), None); assert!(gateway_pin("meta-gateway/1.0.41 gateways=usenet,tmdb", &addrs, None).is_some()); } #[test] fn a_pin_keeps_only_dialable_addresses() { let addrs = [ ma("/ip4/127.0.0.1/tcp/4002"), ma("/ip4/0.0.0.0/tcp/4002"), ma("/ip4/172.18.0.16/tcp/4002"), ma("/ip4/85.17.246.67/tcp/4002"), ]; let pin = gateway_pin( "meta-gateway/1.0.41 gateways=usenet", &addrs, Some("https://metagateway-watch.nsl.sh".into()), ) .unwrap(); assert_eq!(pin.addrs, vec![addrs[2].clone(), addrs[3].clone()]); assert_eq!(pin.base_url.as_deref(), Some("https://metagateway-watch.nsl.sh")); } }