//! mDNS peer discovery and discovery-time protocol negotiation. use std::time::Duration; use lanspread_mdns::{LANSPREAD_SERVICE_TYPE, MdnsBrowser, MdnsService, MdnsServicePoll}; use lanspread_proto::PROTOCOL_VERSION; use tokio::sync::mpsc::UnboundedSender; use crate::{ PeerEvent, context::Ctx, events, peer_db::PeerId, services::{handshake::perform_handshake_with_peer, legacy::request_games_from_peer}, }; struct MdnsPeerInfo { addr: std::net::SocketAddr, peer_id: Option, proto_ver: Option, library_rev: u64, library_digest: u64, } /// Runs the peer discovery service using mDNS. pub async fn run_peer_discovery( tx_notify_ui: UnboundedSender, ctx: Ctx, ) -> eyre::Result<()> { log::info!("Starting peer discovery task"); let service_type = LANSPREAD_SERVICE_TYPE.to_string(); let (service_tx, mut service_rx) = tokio::sync::mpsc::unbounded_channel(); let worker_shutdown = ctx.shutdown.clone(); let service_type_clone = service_type.clone(); let worker_handle = ctx .task_tracker .spawn_blocking(move || -> eyre::Result<()> { let browser = MdnsBrowser::new(&service_type_clone)?; while !worker_shutdown.is_cancelled() { match browser.next_service_timeout(None, Duration::from_millis(250))? { MdnsServicePoll::Service(service) => { if service_tx.send(service).is_err() { log::debug!("Peer discovery consumer dropped; stopping worker"); break; } } MdnsServicePoll::Timeout => {} MdnsServicePoll::Closed => { log::warn!("mDNS browser closed; stopping peer discovery worker"); break; } } } Ok(()) }); loop { tokio::select! { () = ctx.shutdown.cancelled() => break, service = service_rx.recv() => { let Some(service) = service else { break; }; let info = parse_mdns_peer(&service); if is_self_advertisement(&info, &ctx).await { log::trace!("Ignoring self advertisement at {}", info.addr); continue; } handle_discovered_peer(info, &ctx, &tx_notify_ui).await; } } } match worker_handle.await { Ok(Ok(())) if ctx.shutdown.is_cancelled() => Ok(()), Ok(Ok(())) => { eyre::bail!("mDNS discovery worker exited unexpectedly"); } Ok(Err(err)) if ctx.shutdown.is_cancelled() => { log::debug!("Peer discovery worker stopped during shutdown: {err}"); Ok(()) } Ok(Err(err)) => Err(err.wrap_err("peer discovery worker failed")), Err(err) if ctx.shutdown.is_cancelled() => { log::debug!("Peer discovery worker join ended during shutdown: {err}"); Ok(()) } Err(err) => Err(eyre::eyre!("peer discovery worker join error: {err}")), } } fn parse_mdns_peer(service: &MdnsService) -> MdnsPeerInfo { MdnsPeerInfo { addr: service.addr, peer_id: service.properties.get("peer_id").cloned(), proto_ver: service .properties .get("proto_ver") .and_then(|value| value.parse::().ok()), library_rev: service .properties .get("library_rev") .and_then(|value| value.parse::().ok()) .unwrap_or(0), library_digest: service .properties .get("library_digest") .and_then(|value| value.parse::().ok()) .unwrap_or(0), } } async fn is_self_advertisement(info: &MdnsPeerInfo, ctx: &Ctx) -> bool { let guard = ctx.local_peer_addr.read().await; guard.as_ref().is_some_and(|addr| *addr == info.addr) || info .peer_id .as_ref() .is_some_and(|peer_id| peer_id == ctx.peer_id.as_ref()) } async fn handle_discovered_peer( info: MdnsPeerInfo, ctx: &Ctx, tx_notify_ui: &UnboundedSender, ) { let peer_id = info .peer_id .clone() .unwrap_or_else(|| format!("legacy-{}", info.addr)); let upsert = { let mut db = ctx.peer_game_db.write().await; let upsert = db.upsert_peer(peer_id.clone(), info.addr); let features = db.peer_features(&peer_id); if info.library_rev > 0 || info.library_digest > 0 { db.update_peer_library(&peer_id, info.library_rev, info.library_digest, features); } upsert }; if upsert.is_new { log::info!("Discovered peer at: {}", info.addr); events::emit_peer_discovered(&ctx.peer_game_db, tx_notify_ui, info.addr).await; } if upsert.is_new || upsert.addr_changed { spawn_protocol_negotiation(&info, ctx, tx_notify_ui.clone(), peer_id); } } fn spawn_protocol_negotiation( info: &MdnsPeerInfo, ctx: &Ctx, tx_notify_ui: UnboundedSender, peer_id: PeerId, ) { let peer_addr = info.addr; let proto_ver = info.proto_ver; let peer_id_arc = ctx.peer_id.clone(); let local_library = ctx.local_library.clone(); let peer_game_db = ctx.peer_game_db.clone(); ctx.task_tracker.spawn(async move { let handshake_result = if proto_ver.is_none() || proto_ver == Some(PROTOCOL_VERSION) { perform_handshake_with_peer( peer_id_arc, local_library, peer_game_db.clone(), tx_notify_ui.clone(), peer_addr, Some(peer_id), ) .await } else { Err(eyre::eyre!("Skipping hello for legacy peer")) }; if handshake_result.is_err() && let Err(err) = request_games_from_peer(peer_addr, tx_notify_ui, peer_game_db).await { log::error!("Failed to request games from peer {peer_addr}: {err}"); } }); }