use std::cell::RefCell;
use std::ffi::{CStr, CString, c_char, c_int, c_void};
use std::io;
use std::net::IpAddr;
use std::os::fd::RawFd;
type DnsServiceRef = *mut c_void;
type DnsRecordRef = *mut c_void;
type DnsServiceRegisterRecordReply = unsafe extern "C" fn(
sd_ref: DnsServiceRef,
record_ref: DnsRecordRef,
flags: u32,
error: i32,
context: *mut c_void,
);
const FLAGS_UNIQUE: u32 = 0x20;
const INTERFACE_NAME: &CStr = c"lo0";
const CLASS_IN: u16 = 1;
const TYPE_A: u16 = 1;
const TYPE_AAAA: u16 = 28;
pub const ERR_POLICY_DENIED: i32 = -65570;
unsafe extern "C" {
fn DNSServiceCreateConnection(sd_ref: *mut DnsServiceRef) -> i32;
fn DNSServiceRegisterRecord(
sd_ref: DnsServiceRef,
record_ref: *mut DnsRecordRef,
flags: u32,
interface_index: u32,
fullname: *const c_char,
rrtype: u16,
rrclass: u16,
rdlen: u16,
rdata: *const c_void,
ttl: u32,
callback: DnsServiceRegisterRecordReply,
context: *mut c_void,
) -> i32;
fn DNSServiceRemoveRecord(sd_ref: DnsServiceRef, record_ref: DnsRecordRef, flags: u32) -> i32;
fn DNSServiceRefSockFD(sd_ref: DnsServiceRef) -> c_int;
fn DNSServiceProcessResult(sd_ref: DnsServiceRef) -> i32;
fn DNSServiceRefDeallocate(sd_ref: DnsServiceRef);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RecordHandle(usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Reply {
pub handle: RecordHandle,
pub error: i32,
}
pub struct Connection {
sd_ref: DnsServiceRef,
interface_index: u32,
replies: Box<RefCell<Vec<Reply>>>,
}
unsafe impl Send for Connection {}
impl Connection {
pub fn open() -> io::Result<Self> {
let interface_index = unsafe { libc::if_nametoindex(INTERFACE_NAME.as_ptr()) };
if interface_index == 0 {
return Err(io::Error::other(format!(
"no interface {}",
INTERFACE_NAME.to_string_lossy()
)));
}
let mut sd_ref: DnsServiceRef = std::ptr::null_mut();
let error = unsafe { DNSServiceCreateConnection(&raw mut sd_ref) };
if error != 0 {
return Err(dns_sd_error("DNSServiceCreateConnection", error));
}
Ok(Self {
sd_ref,
interface_index,
replies: Box::new(RefCell::new(Vec::new())),
})
}
pub fn fd(&self) -> RawFd {
unsafe { DNSServiceRefSockFD(self.sd_ref) }
}
pub fn register_record(&mut self, fqdn: &str, ip: IpAddr) -> io::Result<RecordHandle> {
let name = CString::new(format!("{fqdn}."))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "NUL in DNS name"))?;
let v4;
let v6;
let (rrtype, rdata): (u16, &[u8]) = match ip {
IpAddr::V4(ip) => {
v4 = ip.octets();
(TYPE_A, &v4)
}
IpAddr::V6(ip) => {
v6 = ip.octets();
(TYPE_AAAA, &v6)
}
};
let rdlen = u16::try_from(rdata.len()).expect("an address is 4 or 16 bytes");
let mut record_ref: DnsRecordRef = std::ptr::null_mut();
let context = std::ptr::from_ref::<RefCell<Vec<Reply>>>(&*self.replies)
.cast_mut()
.cast::<c_void>();
let error = unsafe {
DNSServiceRegisterRecord(
self.sd_ref,
&raw mut record_ref,
FLAGS_UNIQUE,
self.interface_index,
name.as_ptr(),
rrtype,
CLASS_IN,
rdlen,
rdata.as_ptr().cast(),
0,
reply,
context,
)
};
if error != 0 {
return Err(dns_sd_error("DNSServiceRegisterRecord", error));
}
Ok(RecordHandle(record_ref.addr()))
}
pub fn remove_record(&mut self, handle: RecordHandle) {
let error = unsafe {
DNSServiceRemoveRecord(self.sd_ref, std::ptr::without_provenance_mut(handle.0), 0)
};
if error != 0 {
tracing::debug!(error, "DNSServiceRemoveRecord failed");
}
}
pub fn has_pending_reply(&self) -> bool {
let mut pollfd = libc::pollfd {
fd: self.fd(),
events: libc::POLLIN,
revents: 0,
};
unsafe { libc::poll(&raw mut pollfd, 1, 0) == 1 }
}
pub fn process(&mut self) -> io::Result<Vec<Reply>> {
let error = unsafe { DNSServiceProcessResult(self.sd_ref) };
if error != 0 {
return Err(dns_sd_error("DNSServiceProcessResult", error));
}
Ok(self.replies.borrow_mut().drain(..).collect())
}
}
impl Drop for Connection {
fn drop(&mut self) {
unsafe { DNSServiceRefDeallocate(self.sd_ref) }
}
}
unsafe extern "C" fn reply(
_sd_ref: DnsServiceRef,
record_ref: DnsRecordRef,
_flags: u32,
error: i32,
context: *mut c_void,
) {
let replies = unsafe { &*context.cast::<RefCell<Vec<Reply>>>() };
replies.borrow_mut().push(Reply {
handle: RecordHandle(record_ref.addr()),
error,
});
}
fn dns_sd_error(call: &str, error: i32) -> io::Error {
io::Error::other(format!("{call} failed: kDNSServiceErr {error}"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
use std::time::{Duration, Instant};
#[test]
fn a_registration_round_trips_through_mdnsresponder() {
let mut connection = Connection::open().expect("mDNSResponder is running");
let fqdn = format!("arcbox-dns-sd-test-{}.arcbox.local", std::process::id());
let handle = connection
.register_record(&fqdn, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 9)))
.unwrap();
let deadline = Instant::now() + Duration::from_secs(5);
let mut replies = Vec::new();
while replies.is_empty() && Instant::now() < deadline {
if connection.has_pending_reply() {
replies.extend(connection.process().unwrap());
} else {
std::thread::sleep(Duration::from_millis(20));
}
}
assert_eq!(replies.len(), 1, "one reply for one registration");
assert_eq!(replies[0].handle, handle);
eprintln!("mDNSResponder answered kDNSServiceErr {}", replies[0].error);
assert!(
matches!(replies[0].error, 0 | ERR_POLICY_DENIED),
"unexpected kDNSServiceErr {}",
replies[0].error
);
connection.remove_record(handle);
}
}