//! mDNS-based peer discovery for Freifunk / community mesh deployments. //! //! Browse for `_quicproquo._udp.local.` services on the local network and //! surface them as [`DiscoveredPeer`] structs. Servers announce themselves //! automatically on startup; this module lets clients find them without manual //! configuration. //! //! # Usage //! //! ```no_run //! use quicproquo_client::client::mesh_discovery::MeshDiscovery; //! //! let disc = MeshDiscovery::start()?; //! // Give mDNS time to collect announcements before reading. //! std::thread::sleep(std::time::Duration::from_secs(2)); //! for peer in disc.peers() { //! println!("found: {} at {}", peer.domain, peer.server_addr); //! } //! # Ok::<(), quicproquo_client::client::mesh_discovery::MeshDiscoveryError>(()) //! ``` #[cfg(feature = "mesh")] use mdns_sd::{ServiceDaemon, ServiceEvent}; use std::net::SocketAddr; #[cfg(feature = "mesh")] use std::sync::{Arc, Mutex}; #[cfg(feature = "mesh")] use std::collections::HashMap; /// A qpq server discovered on the local network via mDNS. #[derive(Debug, Clone)] pub struct DiscoveredPeer { /// Federation domain of the remote server (e.g. `"node1.freifunk.net"`). pub domain: String, /// QUIC RPC address to connect to. pub server_addr: SocketAddr, } /// A running mDNS browse session. /// /// Starts immediately on construction; drop to stop browsing. pub struct MeshDiscovery { #[cfg(feature = "mesh")] _daemon: ServiceDaemon, #[cfg(feature = "mesh")] peers: Arc>>, } #[derive(thiserror::Error, Debug)] pub enum MeshDiscoveryError { #[error("mDNS daemon failed to start: {0}")] DaemonError(String), #[error("mDNS browse failed: {0}")] BrowseError(String), #[error("mesh feature not compiled (rebuild with --features mesh)")] FeatureDisabled, } impl MeshDiscovery { /// Start browsing for `_quicproquo._udp.local.` services. /// /// Returns immediately; peers are collected in the background. /// Returns [`MeshDiscoveryError::FeatureDisabled`] when built without the /// `mesh` feature. pub fn start() -> Result { #[cfg(feature = "mesh")] { Self::start_inner() } #[cfg(not(feature = "mesh"))] { Err(MeshDiscoveryError::FeatureDisabled) } } #[cfg(feature = "mesh")] fn start_inner() -> Result { let daemon = ServiceDaemon::new() .map_err(|e| MeshDiscoveryError::DaemonError(e.to_string()))?; let receiver = daemon .browse("_quicproquo._udp.local.") .map_err(|e| MeshDiscoveryError::BrowseError(e.to_string()))?; let peers: Arc>> = Arc::new(Mutex::new(HashMap::new())); let peers_bg = Arc::clone(&peers); // Process mDNS events in a background thread (ServiceDaemon is sync). std::thread::spawn(move || { for event in receiver { match event { ServiceEvent::ServiceResolved(info) => { // Extract the qpq server address from TXT records. let server_addr_str = info .get_property_val_str("server") .map(|s| s.to_string()); let domain = info .get_property_val_str("domain") .map(|s| s.to_string()) .unwrap_or_else(|| info.get_fullname().to_string()); if let Some(addr_str) = server_addr_str { if let Ok(addr) = addr_str.parse::() { let peer = DiscoveredPeer { domain: domain.clone(), server_addr: addr, }; if let Ok(mut map) = peers_bg.lock() { map.insert(domain, peer); } } } } ServiceEvent::ServiceRemoved(_ty, fullname) => { if let Ok(mut map) = peers_bg.lock() { map.retain(|_, p| { !fullname.contains(&p.domain) }); } } // Other events (SearchStarted, SearchStopped) are informational. _ => {} } } }); Ok(Self { _daemon: daemon, peers, }) } /// Return a snapshot of all peers discovered so far. pub fn peers(&self) -> Vec { #[cfg(feature = "mesh")] { self.peers .lock() .map(|m| m.values().cloned().collect()) .unwrap_or_default() } #[cfg(not(feature = "mesh"))] { vec![] } } }