use std::{
cell::RefCell,
collections::HashMap,
net::{IpAddr, Ipv4Addr},
rc::Rc,
};
use mdns_sd::{ServiceDaemon, ServiceEvent, ServiceInfo};
use tokio::task::{JoinHandle, spawn_local};
const SERVICE_TYPE: &str = "_mousehop._udp.local.";
const TXT_PRIMARY_KEY: &str = "primary";
fn primary_ipv4() -> Option<Ipv4Addr> {
let iface = netdev::get_default_interface().ok()?;
iface.ipv4.first().map(|net| net.addr())
}
fn local_hostname() -> String {
hostname::get()
.ok()
.and_then(|s| s.into_string().ok())
.unwrap_or_else(|| "mousehop".to_string())
}
fn strip_trailing_dot(s: &str) -> &str {
s.strip_suffix('.').unwrap_or(s)
}
fn instance_from_fullname<'a>(fullname: &'a str, service_type: &str) -> &'a str {
let suffix = format!(".{service_type}");
fullname.strip_suffix(&suffix).unwrap_or(fullname)
}
pub(crate) fn normalize_mdns_name(s: &str) -> String {
let s = strip_trailing_dot(s);
let s = s.strip_suffix(".local").unwrap_or(s);
s.to_ascii_lowercase()
}
pub(crate) type PrimaryCache = Rc<RefCell<HashMap<String, IpAddr>>>;
pub(crate) struct Discovery {
daemon: Option<ServiceDaemon>,
registered_fullname: Option<String>,
primary_cache: PrimaryCache,
browse_task: Option<JoinHandle<()>>,
port: u16,
}
impl Discovery {
pub(crate) fn new(port: u16, enabled: bool, primary_cache: PrimaryCache) -> Self {
if !enabled {
log::info!("mdns discovery disabled by config");
return Self::inert(port, primary_cache);
}
match ServiceDaemon::new() {
Ok(daemon) => {
let browse_task = start_browse(&daemon, primary_cache.clone());
let mut this = Self {
daemon: Some(daemon),
registered_fullname: None,
primary_cache,
browse_task,
port,
};
this.register();
this
}
Err(e) => {
log::warn!("mdns ServiceDaemon::new failed: {e}; discovery disabled");
Self::inert(port, primary_cache)
}
}
}
fn inert(port: u16, primary_cache: PrimaryCache) -> Self {
Self {
daemon: None,
registered_fullname: None,
primary_cache,
browse_task: None,
port,
}
}
fn register(&mut self) {
let Some(daemon) = self.daemon.as_ref() else {
return;
};
if let Some(old) = self.registered_fullname.take() {
let _ = daemon.unregister(&old);
}
let host = local_hostname();
let host_record = format!("{host}.local.");
let primary = match primary_ipv4() {
Some(ip) => ip,
None => {
log::warn!(
"mdns: no default-route interface; skipping registration (will retry on \
interface change)"
);
return;
}
};
let mut props = HashMap::new();
props.insert(TXT_PRIMARY_KEY.to_string(), primary.to_string());
let info = match ServiceInfo::new(
SERVICE_TYPE,
&host,
&host_record,
IpAddr::V4(primary),
self.port,
Some(props),
) {
Ok(i) => i,
Err(e) => {
log::warn!("mdns ServiceInfo::new failed: {e}; skipping registration");
return;
}
};
let fullname = info.get_fullname().to_string();
match daemon.register(info) {
Ok(()) => {
log::info!(
"mdns: registered {fullname} on {primary}:{port} (primary interface)",
port = self.port,
);
self.registered_fullname = Some(fullname);
}
Err(e) => log::warn!("mdns register failed: {e}"),
}
}
pub(crate) fn refresh(&mut self) {
if self.daemon.is_some() {
self.register();
}
}
pub(crate) fn set_port(&mut self, port: u16) {
if self.port == port {
return;
}
self.port = port;
self.refresh();
}
pub(crate) fn set_enabled(&mut self, enabled: bool) {
let currently = self.daemon.is_some();
if currently == enabled {
return;
}
if enabled {
*self = Self::new(self.port, true, self.primary_cache.clone());
} else {
self.shutdown();
}
}
fn shutdown(&mut self) {
if let Some(daemon) = self.daemon.take() {
if let Some(name) = self.registered_fullname.take() {
let _ = daemon.unregister(&name);
}
let _ = daemon.shutdown();
}
if let Some(task) = self.browse_task.take() {
task.abort();
}
}
}
impl Drop for Discovery {
fn drop(&mut self) {
self.shutdown();
}
}
fn start_browse(
daemon: &ServiceDaemon,
primary_cache: Rc<RefCell<HashMap<String, IpAddr>>>,
) -> Option<JoinHandle<()>> {
let receiver = match daemon.browse(SERVICE_TYPE) {
Ok(rx) => rx,
Err(e) => {
log::warn!("mdns browse failed: {e}");
return None;
}
};
Some(spawn_local(async move {
while let Ok(event) = receiver.recv_async().await {
match event {
ServiceEvent::ServiceResolved(resolved) => {
let Some(primary_str) = resolved.get_property_val_str(TXT_PRIMARY_KEY) else {
continue;
};
let Ok(ip) = primary_str.parse::<IpAddr>() else {
log::debug!(
"mdns: peer {} advertised malformed primary={primary_str:?}",
resolved.get_fullname()
);
continue;
};
let instance = instance_from_fullname(resolved.get_fullname(), SERVICE_TYPE);
let key = normalize_mdns_name(instance);
let target = strip_trailing_dot(resolved.get_hostname());
log::info!(
"mdns: peer instance={key} (target={target}) announces primary={ip} \
(port={port})",
port = resolved.get_port(),
);
primary_cache.borrow_mut().insert(key, ip);
}
ServiceEvent::ServiceRemoved(_, fullname) => {
log::debug!("mdns: service removed {fullname}");
}
_ => {}
}
}
}))
}