mod dns_sd;
mod mirror;
use std::io;
use std::net::IpAddr;
use std::os::fd::{AsRawFd, RawFd};
use std::sync::Arc;
use std::time::Duration;
use arcbox_core::Runtime;
use arcbox_net::NetworkManager;
use tokio::io::Interest;
use tokio::io::unix::AsyncFd;
use tokio::sync::broadcast::error::RecvError;
use tokio_util::sync::CancellationToken;
use tracing::{debug, info, warn};
use dns_sd::{Connection, RecordHandle};
use mirror::{Event, Mirror, Registrar};
const RETRY_DENIED: Duration = Duration::from_secs(60);
pub fn spawn(runtime: &Arc<Runtime>, domain: &str, shutdown: &CancellationToken) {
if !is_mdns_domain(domain) {
debug!(domain, "not an mDNS domain; no Bonjour records");
return;
}
let network_manager = Arc::clone(runtime.network_manager());
let domain = domain.to_owned();
let shutdown = shutdown.clone();
drop(tokio::spawn(async move {
if let Err(error) = run(network_manager, &domain, shutdown).await {
warn!(%error, domain, "mDNS mirror stopped; names resolve through the resolver file only");
}
}));
}
fn is_mdns_domain(domain: &str) -> bool {
domain
.rsplit('.')
.next()
.is_some_and(|tld| tld.eq_ignore_ascii_case("local"))
}
struct Fd(RawFd);
impl AsRawFd for Fd {
fn as_raw_fd(&self) -> RawFd {
self.0
}
}
impl Registrar for Connection {
type Handle = RecordHandle;
fn register(&mut self, fqdn: &str, ip: IpAddr) -> io::Result<RecordHandle> {
self.register_record(fqdn, ip)
}
fn remove(&mut self, handle: RecordHandle) {
self.remove_record(handle);
}
}
async fn run(
network_manager: Arc<NetworkManager>,
domain: &str,
shutdown: CancellationToken,
) -> io::Result<()> {
let mut changes = network_manager.subscribe_dns_changes();
let connection = Connection::open()?;
let fd = connection.fd();
let mut mirror = Mirror::new(connection);
let socket = AsyncFd::with_interest(Fd(fd), Interest::READABLE)?;
mirror.resync(network_manager.local_dns_entries());
let mut announced = false;
loop {
let denied = mirror.is_denied();
let retry = async move {
if denied {
tokio::time::sleep(RETRY_DENIED).await;
} else {
std::future::pending::<()>().await;
}
};
tokio::select! {
() = shutdown.cancelled() => break,
change = changes.recv() => match change {
Ok(change) => mirror.apply(change),
Err(RecvError::Lagged(_)) => mirror.resync(network_manager.local_dns_entries()),
Err(RecvError::Closed) => break,
},
guard = socket.readable() => {
let mut guard = guard?;
guard.clear_ready();
let mut replies = Vec::new();
while mirror.registrar_mut().has_pending_reply() {
replies.extend(mirror.registrar_mut().process()?);
}
for reply in replies {
match mirror.handle_reply(reply.handle, reply.error) {
Some(Event::Confirmed { fqdn }) => {
if !announced {
announced = true;
info!("mDNSResponder serves this host's *.{domain} names");
}
debug!(fqdn, "mDNS record live");
}
Some(Event::Denied) => warn!(
retry_secs = RETRY_DENIED.as_secs(),
"mDNSResponder refused the *.{domain} records: this process has no \
Local Network access (System Settings › Privacy & Security › Local \
Network); names resolve through /etc/resolver/{domain} meanwhile"
),
Some(Event::Recovered) => {
info!("mDNSResponder accepts the *.{domain} records again");
}
Some(Event::Refused { fqdn, error }) => {
warn!(fqdn, error, "mDNSResponder refused a record");
}
None => {}
}
}
}
() = retry => mirror.retry_denied(),
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::is_mdns_domain;
#[test]
fn only_local_domains_are_announced() {
assert!(is_mdns_domain("arcbox.local"));
assert!(is_mdns_domain("worktree.dev.arcbox.local"));
assert!(is_mdns_domain("ARCBOX.LOCAL"));
assert!(is_mdns_domain("local"));
assert!(!is_mdns_domain("arcbox.test"));
assert!(!is_mdns_domain("notlocal"));
assert!(!is_mdns_domain("local.example"));
}
}