//! mDNS advertisement for the local peer server. use std::{collections::HashMap, net::SocketAddr, time::Duration}; use lanspread_mdns::{DaemonEvent, LANSPREAD_SERVICE_TYPE, MdnsAdvertiser, MdnsMonitor}; use lanspread_proto::PROTOCOL_VERSION; use tokio_util::sync::CancellationToken; use crate::{context::PeerCtx, network::select_advertise_ip}; pub(super) async fn start_mdns_advertiser( ctx: &PeerCtx, server_addr: SocketAddr, ) -> eyre::Result { let advertise_ip = select_advertise_ip()?; let advertise_addr = SocketAddr::new(advertise_ip, server_addr.port()); log::info!("Advertising peer via mDNS from {advertise_addr}"); { let mut guard = ctx.local_peer_addr.write().await; *guard = Some(advertise_addr); } let peer_id = ctx.peer_id.as_ref().clone(); let hostname = gethostname::gethostname().to_string_lossy().into_owned(); let advertised_name = advertised_service_name(&hostname, &peer_id); let monitor_name = advertised_name.clone(); let properties = advertisement_properties(ctx, &hostname, &peer_id).await; let mdns = tokio::task::spawn_blocking(move || { MdnsAdvertiser::new( LANSPREAD_SERVICE_TYPE, &advertised_name, advertise_addr, Some(properties), ) }) .await??; log::info!("Registered mDNS service with name: {monitor_name}"); Ok(mdns) } pub(super) async fn monitor_mdns_events(monitor: MdnsMonitor, shutdown: CancellationToken) { loop { let event = tokio::select! { () = shutdown.cancelled() => break, event = monitor.recv_async() => event, }; match event { Ok(DaemonEvent::Error(err)) => { log::error!("mDNS error: {err}"); tokio::select! { () = shutdown.cancelled() => break, () = tokio::time::sleep(Duration::from_secs(1)) => {} } } Ok(other_event) => { log::trace!("mDNS event: {other_event:?}"); } Err(err) => { log::debug!("mDNS monitor channel closed: {err}"); break; } } } } fn advertised_service_name(hostname: &str, peer_id: &str) -> String { let max_hostname_len = 63usize.saturating_sub(peer_id.len() + 1); let truncated_hostname = if hostname.len() > max_hostname_len { hostname.get(..max_hostname_len).unwrap_or(hostname) } else { hostname }; if truncated_hostname.is_empty() { peer_id.to_string() } else { format!("{truncated_hostname}-{peer_id}") } } async fn advertisement_properties( ctx: &PeerCtx, hostname: &str, peer_id: &str, ) -> HashMap { let (library_rev, library_digest) = { let library_guard = ctx.local_library.read().await; (library_guard.revision, library_guard.digest) }; let mut properties = HashMap::new(); properties.insert("peer_id".to_string(), peer_id.to_string()); properties.insert("proto_ver".to_string(), PROTOCOL_VERSION.to_string()); properties.insert("library_rev".to_string(), library_rev.to_string()); properties.insert("library_digest".to_string(), library_digest.to_string()); if !hostname.is_empty() { properties.insert("hostname".to_string(), hostname.to_string()); } properties }