diff options
Diffstat (limited to 'src/transport/p2p.rs')
| -rw-r--r-- | src/transport/p2p.rs | 125 |
1 files changed, 113 insertions, 12 deletions
diff --git a/src/transport/p2p.rs b/src/transport/p2p.rs index 675ff13..e6abb0a 100644 --- a/src/transport/p2p.rs +++ b/src/transport/p2p.rs @@ -29,9 +29,13 @@ //! (nodes of the swarm) to dial it back. Once AutoNAT says it can be dialled, it lets //! the relays it picked go. //! -//! What this transport doesn't do yet (`docs/future-work.md`): find peers on the LAN -//! (mDNS). It never uses public libp2p infrastructure: relays are those configured, or -//! members that say they relay. +//! 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 never uses public libp2p infrastructure: relays are those configured, or members +//! that say they relay. use std::collections::HashMap; use std::path::Path; @@ -105,6 +109,8 @@ pub(crate) struct Libp2pConfig { pub(crate) relays: Vec<Multiaddr>, /// Relay for other nodes. pub(crate) relay: bool, + /// Find peers on the LAN (mDNS). + pub(crate) mdns: bool, /// libp2p's in-memory transport instead of TCP and QUIC: for tests. pub(crate) memory: bool, } @@ -166,9 +172,29 @@ struct Book { serving: bool, /// Members that say they relay, and where (with the relay's peer ID). relays: HashMap<PeerId, Multiaddr>, + /// Peers found on the LAN (mDNS) not known yet to speak continuum. + lan: std::collections::HashSet<PeerId>, + /// Peers that speak continuum (identify says so). + continuum: std::collections::HashSet<PeerId>, + /// Peers found on the LAN that speak continuum, with their address: seeds not + /// handed out yet (`discovered_seeds`). + discovered: Vec<Multiaddr>, } impl Book { + /// `peer`, found on the LAN, speaks continuum: a seed, at the address we know first. + fn discover(&mut self, peer: PeerId) { + let Some(address) = self.addresses.get(&peer).and_then(|known| known.first()) else { + return; + }; + if let Ok(address) = address.clone().with_p2p(peer) + && !self.discovered.contains(&address) + && self.discovered.len() < MAX_PEERS + { + self.discovered.push(address); + } + } + /// Where others could dial us directly: the announced addresses, or else those we /// listen on (`published`), without the peer ID. fn direct(&self) -> Vec<Multiaddr> { @@ -262,9 +288,9 @@ impl Libp2pTransport { pub(crate) fn start(config: Libp2pConfig) -> Result<Self> { let peer_id = config.key.public().to_peer_id(); let mut swarm = if config.memory { - memory_swarm(config.key, config.relay)? + memory_swarm(config.key, config.relay, config.mdns)? } else { - network_swarm(config.key, config.relay)? + network_swarm(config.key, config.relay, config.mdns)? }; for address in config.listen { swarm @@ -353,10 +379,17 @@ struct Behaviour { relay: ::libp2p::swarm::behaviour::toggle::Toggle<::libp2p::relay::Behaviour>, dcutr: ::libp2p::dcutr::Behaviour, autonat: ::libp2p::autonat::Behaviour, + /// Only with `Libp2pConfig::mdns`. + mdns: ::libp2p::swarm::behaviour::toggle::Toggle<::libp2p::mdns::tokio::Behaviour>, } impl Behaviour { - fn new(key: &Keypair, relay_client: ::libp2p::relay::client::Behaviour, relay: bool) -> Self { + fn new( + key: &Keypair, + relay_client: ::libp2p::relay::client::Behaviour, + relay: bool, + mdns: bool, + ) -> std::io::Result<Self> { let peer_id = key.public().to_peer_id(); let identify = ::libp2p::identify::Config::new(IDENTIFY_VERSION.to_owned(), key.public()) .with_agent_version(format!("continuum/{}", env!("CARGO_PKG_VERSION"))); @@ -372,7 +405,14 @@ impl Behaviour { }, ) }); - Self { + let mdns = match mdns { + true => Some(::libp2p::mdns::tokio::Behaviour::new( + ::libp2p::mdns::Config::default(), + peer_id, + )?), + false => None, + }; + Ok(Self { stream: libp2p_stream::Behaviour::new(), identify: ::libp2p::identify::Behaviour::new(identify), relay_client, @@ -386,11 +426,12 @@ impl Behaviour { ..Default::default() }, ), - } + mdns: mdns.into(), + }) } } -fn network_swarm(key: Keypair, relay: bool) -> Result<Swarm<Behaviour>> { +fn network_swarm(key: Keypair, relay: bool, mdns: bool) -> Result<Swarm<Behaviour>> { Ok(::libp2p::SwarmBuilder::with_existing_identity(key) .with_tokio() .with_tcp( @@ -404,12 +445,15 @@ fn network_swarm(key: Keypair, relay: bool) -> Result<Swarm<Behaviour>> { ::libp2p::noise::Config::new, ::libp2p::yamux::Config::default, )? - .with_behaviour(|key, relay_client| Behaviour::new(key, relay_client, relay))? + .with_behaviour(|key, relay_client| { + Behaviour::new(key, relay_client, relay, mdns) + .map_err(|error| Box::new(error) as Box<dyn std::error::Error + Send + Sync>) + })? .with_swarm_config(|config| config.with_idle_connection_timeout(IDLE_TIMEOUT)) .build()) } -fn memory_swarm(key: Keypair, relay: bool) -> Result<Swarm<Behaviour>> { +fn memory_swarm(key: Keypair, relay: bool, mdns: bool) -> Result<Swarm<Behaviour>> { use ::libp2p::Transport as _; use ::libp2p::core::transport::{MemoryTransport, OrTransport}; use ::libp2p::core::upgrade::Version; @@ -425,7 +469,7 @@ fn memory_swarm(key: Keypair, relay: bool) -> Result<Swarm<Behaviour>> { .boxed(); Ok(Swarm::new( transport, - Behaviour::new(&key, relay_client, relay), + Behaviour::new(&key, relay_client, relay, mdns)?, peer_id, ::libp2p::swarm::Config::with_tokio_executor().with_idle_connection_timeout(IDLE_TIMEOUT), )) @@ -718,6 +762,39 @@ async fn run( for address in published(&info.listen_addrs).into_iter().rev() { book.remember(peer_id, address); } + if info.protocols.contains(&PROTOCOL) { + book.continuum.insert(peer_id); + if book.lan.remove(&peer_id) { + book.discover(peer_id); + } + } + } + SwarmEvent::Behaviour(BehaviourEvent::Mdns( + ::libp2p::mdns::Event::Discovered(found), + )) => { + for (peer, address) in found { + if peer == relaying.local { + continue; + } + debug!(peer = %peer, address = %address, "libp2p peer on the LAN"); + let continuum = { + let mut book = lock(&book); + book.remember(peer, address.clone()); + book.continuum.contains(&peer) + }; + if continuum { + lock(&book).discover(peer); + } else if lock(&book).lan.len() < MAX_PEERS { + // Connected, identify says whether it speaks continuum. + lock(&book).lan.insert(peer); + let _ = swarm.dial( + DialOpts::peer_id(peer) + .addresses(vec![address]) + .condition(PeerCondition::DisconnectedAndNotDialing) + .build(), + ); + } + } } _ => {} }, @@ -827,6 +904,14 @@ impl Transport for Libp2pTransport { Some(peer.to_string()) } + /// Nodes found on the LAN (mDNS) that speak continuum. + fn discovered_seeds(&self) -> Vec<String> { + 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<String> { let Ok(peer) = PeerId::from_str(target) else { @@ -980,6 +1065,7 @@ mod tests { announce: Vec::new(), relays, relay, + mdns: false, memory: true, }) .unwrap(); @@ -1117,6 +1203,7 @@ mod tests { announce: vec![announced.clone()], relays: Vec::new(), relay: false, + mdns: false, memory: true, }) .unwrap(); @@ -1229,6 +1316,20 @@ mod tests { ); } + /// A peer on the LAN that speaks continuum becomes a seed once, at its address. + #[test] + fn lan_peers_become_seeds_once() { + let mut book = Book::default(); + let lan = peer(); + let address: Multiaddr = "/ip4/192.168.1.5/tcp/9661".parse().unwrap(); + book.discover(lan); // No address known: nothing. + assert!(book.discovered.is_empty()); + book.remember(lan, address.clone()); + book.discover(lan); + book.discover(lan); + assert_eq!(book.discovered, [address.with_p2p(lan).unwrap()]); + } + #[tokio::test] async fn unknown_peers_are_refused() { let (b, _) = memory_node(); |
