use std::cell::RefCell;
use std::collections::HashSet;
use std::ffi::{CStr, CString};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::num::NonZeroU32;
use std::os::raw::{c_char, c_void};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use log::trace;
use tokio::runtime::Handle;
use tokio::sync::mpsc::unbounded_channel;
use super::ffi::*;
use crate::browse::{
BrowseEvent, BrowseEventReceiver, BrowseEventSender, DiscoveredService, RemovedService,
ServiceBrowseError, TxtRecord, parse_txt_buffer,
};
const META_QUERY_TYPE: &str = "_services._dns-sd._udp";
const POLL_INTERVAL_MS: i32 = 200;
const RESOLVE_TIMEOUT_MS: i32 = 5000;
const GETADDR_TIMEOUT_MS: u64 = 2000;
pub(crate) struct BrowseGuard {
stop: Arc<AtomicBool>,
}
impl Drop for BrowseGuard {
fn drop(&mut self) {
self.stop.store(true, Ordering::SeqCst);
}
}
pub(crate) async fn browse_start(
service_type: &Option<String>,
domain: &Option<String>,
interface_index: Option<NonZeroU32>,
) -> Result<(BrowseEventReceiver, BrowseGuard), ServiceBrowseError> {
let (tx, rx) = unbounded_channel();
let stop = Arc::new(AtomicBool::new(false));
let handle = Handle::current();
let interface = interface_index.map(|i| i.get()).unwrap_or(0); let domain = domain.clone().unwrap_or_default();
let (regtype, is_meta) = match service_type {
Some(service_type) => (service_type.clone(), false),
None => (META_QUERY_TYPE.to_string(), true),
};
spawn_browse_thread(
regtype,
domain,
interface,
is_meta,
tx,
handle,
stop.clone(),
);
Ok((rx, BrowseGuard { stop }))
}
struct BrowseThreadContext {
tx: BrowseEventSender,
handle: Handle,
stop: Arc<AtomicBool>,
is_meta: bool,
interface: u32,
seen: RefCell<HashSet<String>>,
}
fn spawn_browse_thread(
regtype: String,
domain: String,
interface: u32,
is_meta: bool,
tx: BrowseEventSender,
handle: Handle,
stop: Arc<AtomicBool>,
) {
std::thread::spawn(move || {
if let Err(err) = run_browse(®type, &domain, interface, is_meta, &tx, &handle, &stop) {
let _ = tx.send(Err(err));
}
});
}
fn run_browse(
regtype: &str,
domain: &str,
interface: u32,
is_meta: bool,
tx: &BrowseEventSender,
handle: &Handle,
stop: &Arc<AtomicBool>,
) -> Result<(), ServiceBrowseError> {
let regtype_c = cstring(regtype)?;
let domain_c = if domain.is_empty() {
None
} else {
Some(cstring(domain)?)
};
let ctx = Box::new(BrowseThreadContext {
tx: tx.clone(),
handle: handle.clone(),
stop: stop.clone(),
is_meta,
interface,
seen: RefCell::new(HashSet::new()),
});
let ctx_ptr = &*ctx as *const BrowseThreadContext as *mut c_void;
let mut sd_ref = DNSServiceRef::default();
let err = unsafe {
DNSServiceBrowse(
&mut sd_ref,
0,
interface,
regtype_c.as_ptr(),
domain_c.as_ref().map_or(std::ptr::null(), |d| d.as_ptr()),
Some(browse_callback),
ctx_ptr,
)
};
if err != error::NO_ERROR {
return Err(ServiceBrowseError::BrowseFailed(format!(
"DNSServiceBrowse failed for {regtype}: {err}"
)));
}
let fd = unsafe { DNSServiceRefSockFD(sd_ref.0) };
if fd < 0 {
unsafe { DNSServiceRefDeallocate(sd_ref) };
return Err(ServiceBrowseError::BrowseFailed(
"DNSServiceRefSockFD returned an invalid descriptor".into(),
));
}
let result = poll_loop(fd, sd_ref.0, stop);
unsafe { DNSServiceRefDeallocate(sd_ref) };
drop(ctx);
result
}
fn poll_loop(
fd: i32,
sd: *mut _DNSServiceRef_t,
stop: &Arc<AtomicBool>,
) -> Result<(), ServiceBrowseError> {
while !stop.load(Ordering::SeqCst) {
let mut pfd = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
let rv = unsafe { libc::poll(&mut pfd, 1, POLL_INTERVAL_MS) };
if rv < 0 {
let errno = std::io::Error::last_os_error();
if errno.raw_os_error() == Some(libc::EINTR) {
continue;
}
return Err(ServiceBrowseError::BrowseFailed(format!(
"poll failed: {errno}"
)));
}
if rv == 0 {
continue; }
if pfd.revents & libc::POLLIN != 0 {
let perr = unsafe { DNSServiceProcessResult(sd) };
if perr != error::NO_ERROR {
return Err(ServiceBrowseError::BrowseFailed(format!(
"DNSServiceProcessResult failed: {perr}"
)));
}
} else if pfd.revents & (libc::POLLHUP | libc::POLLERR | libc::POLLNVAL) != 0 {
break;
}
}
Ok(())
}
unsafe extern "C" fn browse_callback(
_service_ref: DNSServiceRef,
flags: DNSServiceFlags,
interface_index: u32,
error_code: DNSServiceErrorType,
service_name: *const c_char,
regtype: *const c_char,
reply_domain: *const c_char,
context: *mut c_void,
) {
let ctx = unsafe { &*(context as *const BrowseThreadContext) };
if error_code != error::NO_ERROR {
let _ = ctx.tx.send(Err(ServiceBrowseError::BrowseFailed(format!(
"browse callback error: {error_code}"
))));
return;
}
let name = unsafe { cstr_to_string(service_name) };
let regtype = unsafe { cstr_to_string(regtype) };
let domain = unsafe { cstr_to_string(reply_domain) };
let is_add = flags & FLAGS_ADD != 0;
if ctx.is_meta {
if !is_add {
return; }
let proto = first_label(®type);
let service_type = format!("{name}.{proto}");
if ctx.seen.borrow_mut().insert(service_type.clone()) {
trace!("discovered service type {service_type:?} in domain {domain:?}");
spawn_browse_thread(
service_type,
domain,
ctx.interface,
false,
ctx.tx.clone(),
ctx.handle.clone(),
ctx.stop.clone(),
);
}
} else if is_add {
let tx = ctx.tx.clone();
ctx.handle
.spawn_blocking(move || resolve_service(name, regtype, domain, interface_index, tx));
} else {
let removed = RemovedService {
name,
service_type: trim_dot(®type),
domain: trim_dot(&domain),
interface_index: NonZeroU32::new(interface_index),
};
let _ = ctx.tx.send(Ok(BrowseEvent::Removed(removed)));
}
}
fn resolve_service(
name: String,
regtype: String,
domain: String,
interface: u32,
tx: BrowseEventSender,
) {
let (host, port, txt_records) = match do_resolve(&name, ®type, &domain, interface) {
Ok(resolved) => resolved,
Err(err) => {
let _ = tx.send(Err(ServiceBrowseError::ResolveFailed(name, err)));
return;
}
};
let addresses = get_addresses(&host, interface).unwrap_or_default();
let service = DiscoveredService {
name,
service_type: trim_dot(®type),
domain: trim_dot(&domain),
host_name: trim_dot(&host),
port,
addresses,
txt_records,
interface_index: NonZeroU32::new(interface),
};
let _ = tx.send(Ok(BrowseEvent::Found(service)));
}
#[derive(Default)]
struct ResolveResult {
host: Option<String>,
port: u16,
txt: Vec<TxtRecord>,
error: DNSServiceErrorType,
got: bool,
}
fn do_resolve(
name: &str,
regtype: &str,
domain: &str,
interface: u32,
) -> Result<(String, u16, Vec<TxtRecord>), String> {
let name_c = cstring(name).map_err(|e| e.to_string())?;
let regtype_c = cstring(regtype).map_err(|e| e.to_string())?;
let domain_c = cstring(domain).map_err(|e| e.to_string())?;
let mut result = ResolveResult::default();
let mut sd_ref = DNSServiceRef::default();
let err = unsafe {
DNSServiceResolve(
&mut sd_ref,
0,
interface,
name_c.as_ptr(),
regtype_c.as_ptr(),
domain_c.as_ptr(),
Some(resolve_callback),
&mut result as *mut ResolveResult as *mut c_void,
)
};
if err != error::NO_ERROR {
return Err(format!("DNSServiceResolve failed: {err}"));
}
let fd = unsafe { DNSServiceRefSockFD(sd_ref.0) };
let processed = process_once(fd, sd_ref.0, RESOLVE_TIMEOUT_MS);
unsafe { DNSServiceRefDeallocate(sd_ref) };
processed?;
if !result.got || result.error != error::NO_ERROR {
return Err(format!("resolve did not complete (error {})", result.error));
}
Ok((result.host.unwrap_or_default(), result.port, result.txt))
}
unsafe extern "C" fn resolve_callback(
_service_ref: DNSServiceRef,
_flags: DNSServiceFlags,
_interface_index: u32,
error_code: DNSServiceErrorType,
_fullname: *const c_char,
host_target: *const c_char,
port: u16,
txt_len: u16,
txt_record: *const u8,
context: *mut c_void,
) {
let result = unsafe { &mut *(context as *mut ResolveResult) };
result.got = true;
result.error = error_code;
if error_code != error::NO_ERROR {
return;
}
result.host = Some(unsafe { cstr_to_string(host_target) });
result.port = u16::from_be(port); if !txt_record.is_null() && txt_len > 0 {
let txt = unsafe { std::slice::from_raw_parts(txt_record, txt_len as usize) };
result.txt = parse_txt_buffer(txt);
}
}
#[derive(Default)]
struct AddrResult {
addrs: Vec<IpAddr>,
done: bool,
}
fn get_addresses(host: &str, interface: u32) -> Option<Vec<IpAddr>> {
let host_c = cstring(host).ok()?;
let mut result = AddrResult::default();
let mut sd_ref = DNSServiceRef::default();
let err = unsafe {
DNSServiceGetAddrInfo(
&mut sd_ref,
0,
interface,
0, host_c.as_ptr(),
Some(getaddr_callback),
&mut result as *mut AddrResult as *mut c_void,
)
};
if err != error::NO_ERROR {
return None;
}
let fd = unsafe { DNSServiceRefSockFD(sd_ref.0) };
let deadline = Instant::now() + Duration::from_millis(GETADDR_TIMEOUT_MS);
#[allow(clippy::while_immutable_condition)]
while !result.done {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
break;
}
let mut pfd = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
let rv = unsafe { libc::poll(&mut pfd, 1, remaining.as_millis() as i32) };
if rv <= 0 {
break;
}
if unsafe { DNSServiceProcessResult(sd_ref.0) } != error::NO_ERROR {
break;
}
}
unsafe { DNSServiceRefDeallocate(sd_ref) };
Some(result.addrs)
}
unsafe extern "C" fn getaddr_callback(
_service_ref: DNSServiceRef,
flags: DNSServiceFlags,
_interface_index: u32,
error_code: DNSServiceErrorType,
_hostname: *const c_char,
address: *const libc::sockaddr,
_ttl: u32,
context: *mut c_void,
) {
let result = unsafe { &mut *(context as *mut AddrResult) };
if error_code == error::NO_ERROR && !address.is_null() {
if let Some(ip) = unsafe { sockaddr_to_ip(address) } {
result.addrs.push(ip);
}
}
if flags & FLAGS_MORE_COMING == 0 {
result.done = true;
}
}
fn process_once(fd: i32, sd: *mut _DNSServiceRef_t, timeout_ms: i32) -> Result<(), String> {
let mut pfd = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
let rv = unsafe { libc::poll(&mut pfd, 1, timeout_ms) };
if rv < 0 {
return Err(format!("poll failed: {}", std::io::Error::last_os_error()));
}
if rv == 0 {
return Err("operation timed out".into());
}
if unsafe { DNSServiceProcessResult(sd) } != error::NO_ERROR {
return Err("DNSServiceProcessResult failed".into());
}
Ok(())
}
unsafe fn sockaddr_to_ip(addr: *const libc::sockaddr) -> Option<IpAddr> {
match unsafe { (*addr).sa_family } as i32 {
libc::AF_INET => {
let addr = unsafe { &*(addr as *const libc::sockaddr_in) };
Some(IpAddr::V4(Ipv4Addr::from(u32::from_be(
addr.sin_addr.s_addr,
))))
}
libc::AF_INET6 => {
let addr = unsafe { &*(addr as *const libc::sockaddr_in6) };
Some(IpAddr::V6(Ipv6Addr::from(addr.sin6_addr.s6_addr)))
}
_ => None,
}
}
fn cstring(s: &str) -> Result<CString, ServiceBrowseError> {
CString::new(s.as_bytes()).map_err(|e| {
ServiceBrowseError::ParameterContainsInteriorNulByte(s.to_string(), e.nul_position())
})
}
unsafe fn cstr_to_string(p: *const c_char) -> String {
if p.is_null() {
return String::new();
}
unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
}
fn first_label(s: &str) -> &str {
s.split('.').next().unwrap_or(s)
}
fn trim_dot(s: &str) -> String {
s.trim_end_matches('.').to_string()
}