use std::net::SocketAddr;
use std::sync::Arc;
use mdns_sd::{Error as MdnsError, ServiceDaemon, ServiceInfo};
use tracing::{info, warn};
use crate::state::AppState;
pub struct MdnsHandle {
_daemon: ServiceDaemon,
}
pub struct MdnsAdvertiser {
state: Arc<AppState>,
}
impl MdnsAdvertiser {
pub fn new(state: Arc<AppState>) -> Self {
Self { state }
}
pub async fn start(&self) -> Option<MdnsHandle> {
let config = self.state.config.read().await;
let bind = config.server.bind.clone();
drop(config);
let bind_addr = match bind.parse::<SocketAddr>() {
Ok(addr) => addr,
Err(e) => {
warn!(
"Cannot parse bind address '{}' for mDNS advertisement: {}",
bind, e
);
return None;
}
};
if bind_addr.ip().is_loopback() {
info!(
"Skipping mDNS advertisement for loopback-only bind address '{}'",
bind
);
return None;
}
let port = bind_addr.port();
let daemon = match ServiceDaemon::new() {
Ok(d) => d,
Err(e) => {
warn!(
"Failed to create mDNS daemon: {}. Service discovery disabled.",
e
);
return None;
}
};
let hostname = hostname::get()
.ok()
.and_then(|h| h.into_string().ok())
.unwrap_or_else(|| "aranet-service".to_string());
let instance_name = format!("Aranet Service on {}", hostname);
let service_type = "_aranet._tcp.local.";
let properties = [
("version", env!("CARGO_PKG_VERSION")),
("path", "/api"),
("dashboard", "/dashboard"),
];
match build_service_info(service_type, &instance_name, &hostname, port, &properties) {
Ok(service_info) => {
if let Err(e) = daemon.register(service_info) {
warn!("Failed to register mDNS service: {}", e);
} else {
info!(
"Advertising via mDNS as '{}' on port {}",
instance_name, port
);
}
}
Err(e) => {
warn!("Failed to create mDNS service info: {}", e);
}
}
let http_service_type = "_http._tcp.local.";
match build_service_info(
http_service_type,
&instance_name,
&hostname,
port,
&properties,
) {
Ok(service_info) => {
if let Err(e) = daemon.register(service_info) {
warn!("Failed to register HTTP mDNS service: {}", e);
}
}
Err(e) => {
warn!("Failed to create HTTP mDNS service info: {}", e);
}
}
Some(MdnsHandle { _daemon: daemon })
}
}
fn build_service_info(
service_type: &str,
instance_name: &str,
hostname: &str,
port: u16,
properties: &[(&str, &str)],
) -> Result<ServiceInfo, MdnsError> {
ServiceInfo::new(
service_type,
instance_name,
&format!("{hostname}."),
"",
port,
properties,
)
.map(ServiceInfo::enable_addr_auto)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_service_info_enables_address_auto_population() {
let service_info = build_service_info(
"_aranet._tcp.local.",
"Aranet Service on test-host",
"test-host",
8080,
&[("version", env!("CARGO_PKG_VERSION"))],
)
.unwrap();
assert!(service_info.is_addr_auto());
}
}