#![cfg(feature = "discovery")]
use std::collections::HashSet;
use std::net::SocketAddr;
use std::time::{Duration, Instant};
use hickory_resolver::TokioAsyncResolver;
use hickory_resolver::config::{ResolverConfig, ResolverOpts};
use hickory_resolver::proto::rr::{RData, RecordType};
use super::Backend;
const MAX_INSTANCES: usize = 256;
const RESOLVE_BUDGET: Duration = Duration::from_secs(10);
const PER_LOOKUP_TIMEOUT: Duration = Duration::from_secs(3);
pub struct DnsSdBackend {
zone: String,
resolver: TokioAsyncResolver,
}
impl DnsSdBackend {
pub fn new(zone: impl Into<String>) -> Result<Self, std::io::Error> {
let resolver = match hickory_resolver::system_conf::read_system_conf() {
Ok((cfg, opts)) => TokioAsyncResolver::tokio(cfg, opts),
Err(e) => {
tracing::warn!(error = %e, "system DNS config unavailable; using defaults");
TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default())
}
};
Ok(Self {
zone: zone.into(),
resolver,
})
}
fn service_fqdn(&self) -> String {
format!("_epics-ca._tcp.{}", self.zone)
}
}
#[async_trait::async_trait]
impl Backend for DnsSdBackend {
async fn discover(&self) -> Vec<SocketAddr> {
let svc = self.service_fqdn();
let deadline = Instant::now() + RESOLVE_BUDGET;
let lookup_timeout =
|| PER_LOOKUP_TIMEOUT.min(deadline.saturating_duration_since(Instant::now()));
let ptr = match tokio::time::timeout(
lookup_timeout(),
self.resolver.lookup(svc.as_str(), RecordType::PTR),
)
.await
{
Ok(Ok(r)) => r,
Ok(Err(e)) => {
tracing::warn!(zone = %self.zone, error = %e,
"DNS-SD: PTR lookup failed");
return Vec::new();
}
Err(_) => {
tracing::warn!(zone = %self.zone,
"DNS-SD: PTR lookup timed out");
return Vec::new();
}
};
let mut seen_instances: HashSet<String> = HashSet::new();
let mut instances: Vec<String> = Vec::new();
for rdata in ptr.iter() {
if let RData::PTR(target) = rdata {
let name = target.to_string();
if seen_instances.insert(name.clone()) {
instances.push(name);
if instances.len() >= MAX_INSTANCES {
tracing::warn!(zone = %self.zone, cap = MAX_INSTANCES,
"DNS-SD: PTR answer exceeds instance cap; truncating");
break;
}
}
}
}
if instances.is_empty() {
tracing::debug!(zone = %self.zone, "DNS-SD: no PTR instances found");
return Vec::new();
}
let mut out_set: HashSet<SocketAddr> = HashSet::new();
let mut out: Vec<SocketAddr> = Vec::new();
for instance in &instances {
if Instant::now() >= deadline {
tracing::warn!(zone = %self.zone, budget = ?RESOLVE_BUDGET,
"DNS-SD: resolve budget exhausted; returning partial result");
break;
}
let srv = match tokio::time::timeout(
lookup_timeout(),
self.resolver.srv_lookup(instance.as_str()),
)
.await
{
Ok(Ok(r)) => r,
Ok(Err(e)) => {
tracing::warn!(zone = %self.zone, instance = %instance, error = %e,
"DNS-SD: SRV lookup failed");
continue;
}
Err(_) => {
tracing::warn!(zone = %self.zone, instance = %instance,
"DNS-SD: SRV lookup timed out");
continue;
}
};
for record in srv.iter() {
let port = record.port();
let target = record.target().to_string();
if let Ok(Ok(v4)) = tokio::time::timeout(
lookup_timeout(),
self.resolver.ipv4_lookup(target.as_str()),
)
.await
{
for ip in v4.iter() {
let addr = SocketAddr::new(std::net::IpAddr::V4(**ip), port);
if out_set.insert(addr) {
out.push(addr);
}
}
}
if let Ok(Ok(v6)) = tokio::time::timeout(
lookup_timeout(),
self.resolver.ipv6_lookup(target.as_str()),
)
.await
{
for ip in v6.iter() {
let addr = SocketAddr::new(std::net::IpAddr::V6(**ip), port);
if out_set.insert(addr) {
out.push(addr);
}
}
}
}
}
if out.is_empty() {
tracing::debug!(zone = %self.zone,
instances = instances.len(),
"DNS-SD: instances found but no addresses resolved");
} else {
tracing::info!(zone = %self.zone, count = out.len(),
"DNS-SD discovered IOCs");
}
out
}
}