#[cfg(feature = "stats")]
use alloc::sync::Arc;
use alloc::{
collections::{BTreeMap, VecDeque},
vec::Vec,
};
use core::{net::SocketAddr, time::Duration};
use mdns_proto::{
CollectedAnswer, EndpointConfig, Instant, QueryHandle, QuerySpec, ServiceHandle, ServiceSpec,
cache::CacheEntry,
endpoint::{Endpoint, EndpointEventEntry, ServiceRoute, WithdrawalSend},
error::{RegisterServiceError, StartQueryError},
event::{EndpointEvent, QueryUpdate, RouteEvent, ServiceUpdate},
query::Query,
service::Service,
slab::Slab,
transmit::Transmit,
};
use rand_core::Rng;
use smoltcp::wire::IpCidr;
use crate::{
constants::{MDNS_SOCKET_V4, MDNS_SOCKET_V6},
onlink,
udpio::{SendError, UdpIo},
};
#[cfg(feature = "stats")]
use hick_trace::stats::{Stats, StatsSnapshot};
#[cfg(test)]
mod tests;
const MAX_MDNS_MESSAGE: usize = 9000;
const MAX_SERVICE_UPDATES: usize = 16;
const MAX_RX_PER_PUMP: usize = 64;
const RECENT_SEND_BYTES: usize = 16 * 1024;
const RECENT_SEND_TTL: Duration = Duration::from_secs(5);
struct SelfSend<I> {
data: Vec<u8>,
at: I,
}
type AnswerPool = Slab<CollectedAnswer>;
type UpdatePool = Slab<QueryUpdate>;
type ProtoQuery<I> = Query<I, AnswerPool, UpdatePool>;
type ProtoService<I> = Service<I, Slab<Transmit>, Slab<ServiceUpdate>>;
type ProtoEndpoint<I, R> = Endpoint<
I,
R,
Slab<CacheEntry<I>>,
Slab<ServiceRoute>,
Slab<ProtoQuery<I>>,
Slab<EndpointEventEntry>,
AnswerPool,
UpdatePool,
>;
struct ServiceSlot<I: Instant> {
proto: ProtoService<I>,
updates: VecDeque<ServiceUpdate>,
errored: bool,
route_freed: bool,
caller_gone: bool,
}
impl<I: Instant> ServiceSlot<I> {
fn push_update(&mut self, update: ServiceUpdate) {
if matches!(
update,
ServiceUpdate::Conflict | ServiceUpdate::HostConflict
) {
let kind = core::mem::discriminant(&update);
if self
.updates
.iter()
.any(|u| core::mem::discriminant(u) == kind)
{
return;
}
}
if self.updates.len() >= MAX_SERVICE_UPDATES {
let victim = self
.updates
.iter()
.position(|u| matches!(u, ServiceUpdate::Conflict | ServiceUpdate::HostConflict));
match victim {
Some(pos) => {
self.updates.remove(pos);
}
None => {
self.updates.pop_front();
}
}
}
self.updates.push_back(update);
}
}
struct QuerySlot {
errored: bool,
}
#[derive(Debug, Clone, Copy)]
enum FamilySend {
Sent(usize),
Failed,
Unsupported,
Busy,
}
impl FamilySend {
fn is_sent(self) -> bool {
matches!(self, FamilySend::Sent(_))
}
fn withdrawal_send(self) -> WithdrawalSend {
match self {
FamilySend::Sent(_) => WithdrawalSend::Sent,
FamilySend::Busy => WithdrawalSend::Retry,
FamilySend::Unsupported | FamilySend::Failed => WithdrawalSend::WriteOff,
}
}
}
#[derive(Debug, Clone, Copy)]
struct Fanout {
v4: FamilySend,
v6: FamilySend,
}
impl Fanout {
fn any_sent(self) -> bool {
self.v4.is_sent() || self.v6.is_sent()
}
#[cfg_attr(not(feature = "stats"), allow(dead_code))]
fn sent_count(self) -> u32 {
u32::from(self.v4.is_sent()) + u32::from(self.v6.is_sent())
}
#[cfg_attr(not(feature = "stats"), allow(dead_code))]
fn bytes_on_wire(self) -> u64 {
let mut n = 0u64;
if let FamilySend::Sent(b) = self.v4 {
n += b as u64;
}
if let FamilySend::Sent(b) = self.v6 {
n += b as u64;
}
n
}
#[cfg_attr(not(feature = "stats"), allow(dead_code))]
fn failed_count(self) -> u32 {
u32::from(matches!(self.v4, FamilySend::Failed))
+ u32::from(matches!(self.v6, FamilySend::Failed))
}
fn any_busy(self) -> bool {
matches!(self.v4, FamilySend::Busy) || matches!(self.v6, FamilySend::Busy)
}
fn into_multicast_outcome(self, any_too_large: bool) -> MulticastOutcome {
if self.any_sent() {
MulticastOutcome::Delivered
} else if self.any_busy() {
MulticastOutcome::Retry
} else if any_too_large {
MulticastOutcome::Undeliverable
} else {
MulticastOutcome::Retry
}
}
}
#[derive(Debug, Clone, Copy)]
enum Origin {
Service(ServiceHandle),
Query(QueryHandle),
}
fn family_order<I: Instant>(failing_since: &[Option<I>; 2]) -> [(usize, SocketAddr); 2] {
let v4 = (0usize, MDNS_SOCKET_V4);
let v6 = (1usize, MDNS_SOCKET_V6);
let v6_first = match (failing_since[0], failing_since[1]) {
(Some(v4_since), Some(v6_since)) => v6_since < v4_since,
(None, Some(_)) => true,
_ => false,
};
if v6_first { [v6, v4] } else { [v4, v6] }
}
enum MulticastOutcome {
Delivered,
Retry,
Undeliverable,
}
fn record_into<I: Instant>(
recent: &mut VecDeque<SelfSend<I>>,
recent_bytes: &mut usize,
data: &[u8],
now: I,
) {
while let Some(front) = recent.front() {
if now
.checked_duration_since(front.at)
.is_some_and(|age| age > RECENT_SEND_TTL)
{
if let Some(old) = recent.pop_front() {
*recent_bytes -= old.data.len();
}
} else {
break;
}
}
while !recent.is_empty() && recent_bytes.saturating_add(data.len()) > RECENT_SEND_BYTES {
if let Some(old) = recent.pop_front() {
*recent_bytes -= old.data.len();
}
}
*recent_bytes = recent_bytes.saturating_add(data.len());
recent.push_back(SelfSend {
data: data.to_vec(),
at: now,
});
}
struct Multicaster<I> {
failing_since: [Option<I>; 2],
recent: VecDeque<SelfSend<I>>,
recent_bytes: usize,
}
impl<I: Instant> Multicaster<I> {
fn new() -> Self {
Self {
failing_since: [None; 2],
recent: VecDeque::new(),
recent_bytes: 0,
}
}
fn send_multicast<T: UdpIo>(
&mut self,
io: &mut T,
data: &[u8],
now: I,
) -> (MulticastOutcome, Fanout) {
let mut results = [FamilySend::Unsupported; 2];
let mut any_too_large = false;
for (idx, group) in family_order(&self.failing_since) {
let outcome = match io.try_send(data, group) {
Ok(()) => {
self.failing_since[idx] = None;
FamilySend::Sent(data.len())
}
Err(SendError::Busy) => {
self.failing_since[idx].get_or_insert(now);
FamilySend::Busy
}
Err(SendError::Unsupported) => FamilySend::Unsupported,
Err(SendError::TooLarge) => {
any_too_large = true;
FamilySend::Failed
}
};
results[idx] = outcome;
}
let fanout = Fanout {
v4: results[0],
v6: results[1],
};
if fanout.any_sent() {
self.record(data, now);
}
(fanout.into_multicast_outcome(any_too_large), fanout)
}
fn burst<T: UdpIo>(&mut self, io: &mut T, data: &[u8], owed: &mut [u8; 2], now: I) -> Fanout {
let mut results = [FamilySend::Unsupported; 2];
for (idx, group) in family_order(&self.failing_since) {
if owed[idx] == 0 {
continue;
}
let outcome = match io.try_send(data, group) {
Ok(()) => {
self.failing_since[idx] = None;
owed[idx] = owed[idx].saturating_sub(1);
FamilySend::Sent(data.len())
}
Err(SendError::Unsupported) => {
owed[idx] = 0;
FamilySend::Unsupported
}
Err(SendError::TooLarge) => {
owed[idx] = 0;
FamilySend::Failed
}
Err(SendError::Busy) => {
self.failing_since[idx].get_or_insert(now);
FamilySend::Busy
}
};
results[idx] = outcome;
}
Fanout {
v4: results[0],
v6: results[1],
}
}
fn is_self(&self, data: &[u8], now: I) -> bool {
self.recent.iter().any(|s| {
s.data.as_slice() == data
&& now
.checked_duration_since(s.at)
.is_some_and(|age| age <= RECENT_SEND_TTL)
})
}
fn record(&mut self, data: &[u8], now: I) {
record_into(&mut self.recent, &mut self.recent_bytes, data, now);
}
}
pub struct Engine<I: Instant, R> {
endpoint: ProtoEndpoint<I, R>,
services: BTreeMap<ServiceHandle, ServiceSlot<I>>,
queries: BTreeMap<QueryHandle, QuerySlot>,
subnets: Vec<IpCidr>,
completed_withdrawals: Vec<ServiceHandle>,
svc_handle_scratch: Vec<ServiceHandle>,
query_handle_scratch: Vec<QueryHandle>,
tx: Multicaster<I>,
#[cfg(feature = "stats")]
stats: Arc<Stats>,
}
impl<I, R> Engine<I, R>
where
I: Instant,
R: Rng,
{
pub fn new(config: EndpointConfig, rng: R) -> Self {
let endpoint = ProtoEndpoint::try_new(config, rng);
#[cfg(feature = "stats")]
let stats = endpoint.stats_handle();
Self {
endpoint,
services: BTreeMap::new(),
queries: BTreeMap::new(),
subnets: Vec::new(),
completed_withdrawals: Vec::new(),
svc_handle_scratch: Vec::new(),
query_handle_scratch: Vec::new(),
tx: Multicaster::new(),
#[cfg(feature = "stats")]
stats,
}
}
#[cfg(feature = "stats")]
#[cfg_attr(docsrs, doc(cfg(feature = "stats")))]
pub fn stats_handle(&self) -> Arc<Stats> {
self.stats.clone()
}
#[cfg(feature = "stats")]
#[cfg_attr(docsrs, doc(cfg(feature = "stats")))]
pub fn stats(&self) -> StatsSnapshot {
self.stats.snapshot()
}
pub fn set_local_subnets(&mut self, subnets: Vec<IpCidr>) {
self.subnets = subnets;
}
pub fn register_service(
&mut self,
spec: ServiceSpec,
now: I,
) -> Result<ServiceHandle, RegisterServiceError> {
let (handle, proto) = self
.endpoint
.try_register_service::<Slab<Transmit>, Slab<ServiceUpdate>>(spec, now)?;
self.services.insert(
handle,
ServiceSlot {
proto,
updates: VecDeque::new(),
errored: false,
route_freed: false,
caller_gone: false,
},
);
Ok(handle)
}
pub fn unregister_service(&mut self, handle: ServiceHandle, now: I) {
let route_freed = match self.services.get_mut(&handle) {
Some(slot) if slot.route_freed => true,
Some(slot) => {
slot.errored = true;
slot.caller_gone = true;
false
}
None => return,
};
if route_freed {
self.services.remove(&handle);
} else {
self.begin_service_withdrawal(handle, now);
}
}
pub fn start_query(&mut self, spec: QuerySpec, now: I) -> Result<QueryHandle, StartQueryError> {
let handle = self.endpoint.try_start_query(spec, now)?;
self.queries.insert(handle, QuerySlot { errored: false });
Ok(handle)
}
pub fn cancel_query(&mut self, handle: QueryHandle) {
self.queries.remove(&handle);
let _ = self.endpoint.cancel_query(handle);
}
pub fn collected_answers(
&self,
handle: QueryHandle,
) -> impl Iterator<Item = &CollectedAnswer> + '_ {
self.endpoint.collected_answers(handle)
}
pub fn query_accepted_count(&self, handle: QueryHandle) -> Option<u64> {
self.endpoint.query_accepted_count(handle)
}
pub fn pump<T: UdpIo>(&mut self, now: I, io: &mut T, scratch: &mut [u8]) -> Option<I> {
self.fire_timeouts(now);
let mut rx_processed = 0usize;
while rx_processed < MAX_RX_PER_PUMP {
let Some(meta) = io.try_recv(scratch) else {
break;
};
rx_processed += 1;
let len = meta.len;
if len == 0 {
#[cfg(feature = "stats")]
{
self.stats.packets_rx(1);
self.stats.packets_dropped(1);
}
#[cfg(feature = "defmt")]
defmt::debug!("rx drop: oversized/truncated datagram (len=0 marker)");
continue;
}
#[cfg(feature = "defmt")]
defmt::trace!("rx {} bytes", len);
if onlink::on_link(meta.hop_limit, meta.src.ip(), meta.local, &self.subnets) {
self.handle_one(now, meta.src, meta.local, &scratch[..len]);
} else {
#[cfg(feature = "stats")]
{
self.stats.packets_rx(1);
self.stats.bytes_rx(len as u64);
self.stats.packets_dropped(1);
}
#[cfg(feature = "defmt")]
defmt::debug!("rx drop: off-link datagram (RFC 6762 §11 trust boundary)");
}
}
let rx_capped = rx_processed == MAX_RX_PER_PUMP;
self.drain_service_updates(now);
while let Some((dst, len, origin)) = self.poll_one_transmit(now, scratch) {
if dst == MDNS_SOCKET_V4 || dst == MDNS_SOCKET_V6 {
#[cfg_attr(
not(any(feature = "stats", feature = "defmt")),
allow(unused_variables)
)]
let (outcome, fanout) = self.tx.send_multicast(io, &scratch[..len], now);
#[cfg(feature = "stats")]
{
let fc = fanout.failed_count();
if fc > 0 {
self.stats.send_errors(fc as u64);
}
}
match outcome {
MulticastOutcome::Delivered => {
#[cfg(feature = "stats")]
{
self.stats.packets_tx(fanout.sent_count() as u64);
self.stats.bytes_tx(fanout.bytes_on_wire());
}
#[cfg(feature = "defmt")]
defmt::trace!(
"tx multicast {} bytes delivered ({} families)",
len,
fanout.sent_count()
);
self.note_transmit_result(origin, now, true);
}
MulticastOutcome::Retry => self.note_transmit_result(origin, now, false),
MulticastOutcome::Undeliverable => {
#[cfg(feature = "defmt")]
defmt::warn!("tx multicast {} bytes undeliverable (too large)", len);
self.retire_origin(origin, now);
}
}
} else {
let result = io.try_send(&scratch[..len], dst);
let delivered = result.is_ok();
match result {
Ok(()) => {
#[cfg(feature = "stats")]
{
self.stats.packets_tx(1);
self.stats.bytes_tx(len as u64);
}
#[cfg(feature = "defmt")]
defmt::trace!("tx unicast {} bytes delivered", len);
}
Err(SendError::TooLarge) => {
#[cfg(feature = "stats")]
self.stats.send_errors(1);
}
Err(SendError::Busy) | Err(SendError::Unsupported) => {
}
}
self.note_transmit_result(origin, now, delivered);
}
}
self.drain_service_updates(now);
while let Some((dst, len, token)) = self.endpoint.poll_withdrawal_transmit(now, scratch) {
debug_assert_eq!(
dst, MDNS_SOCKET_V4,
"withdrawal dst must be the multicast marker"
);
let _ = dst;
let mut owed = [1u8; 2];
let fanout = self.tx.burst(io, &scratch[..len], &mut owed, now);
#[cfg(feature = "stats")]
{
let sent_count = fanout.sent_count();
if sent_count > 0 {
self.stats.packets_tx(u64::from(sent_count));
self.stats.bytes_tx(fanout.bytes_on_wire());
}
let failed_count = fanout.failed_count();
if failed_count > 0 {
self.stats.send_errors(u64::from(failed_count));
}
if fanout.any_sent() {
self.stats.goodbyes_tx(1);
}
}
#[cfg(feature = "defmt")]
if fanout.any_sent() {
defmt::trace!(
"tx withdrawal {} bytes ({} families)",
len,
fanout.sent_count()
);
}
self.endpoint.note_withdrawal_result(
token,
now,
fanout.v4.withdrawal_send(),
fanout.v6.withdrawal_send(),
);
}
self.completed_withdrawals.clear();
self
.endpoint
.drain_completed_withdrawals(now, &mut self.completed_withdrawals);
while let Some(handle) = self.completed_withdrawals.pop() {
match self.services.get_mut(&handle) {
Some(slot) if slot.updates.is_empty() || slot.caller_gone => {
self.services.remove(&handle);
}
Some(slot) => slot.route_freed = true,
None => {}
}
}
let deadline = self.poll_deadline();
if rx_capped {
Some(deadline.map_or(now, |d| d.min(now)))
} else {
deadline
}
}
fn handle_one(&mut self, now: I, src: SocketAddr, local: Option<core::net::IpAddr>, data: &[u8]) {
let local_ip = local.unwrap_or_else(|| src.ip());
let caller_is_self = self.tx.is_self(data, now);
let Self {
endpoint, services, ..
} = self;
let events = match endpoint.handle(now, src, local_ip, 0, data, caller_is_self) {
Ok(events) => events,
Err(_) => return,
};
for event in events {
match event {
Ok(RouteEvent::ToService(to_service)) => {
if let Some(slot) = services.get_mut(&to_service.handle())
&& !slot.errored
{
slot.proto.handle_event(to_service.into_event(), now);
}
}
Ok(_) => {}
Err(_) => break,
}
}
}
fn fire_timeouts(&mut self, now: I) {
let _ = self.endpoint.handle_timeout(now);
let Self {
endpoint, queries, ..
} = &mut *self;
for (&handle, slot) in queries.iter() {
if slot.errored {
continue;
}
let _ = endpoint.handle_query_timeout(handle, now);
}
for slot in self.services.values_mut() {
if !slot.errored {
let _ = slot.proto.handle_timeout(now);
}
}
}
fn drain_service_updates(&mut self, now: I) {
self.svc_handle_scratch.clear();
self
.svc_handle_scratch
.extend(self.services.keys().copied());
let mut i = 0;
while i < self.svc_handle_scratch.len() {
let handle = self.svc_handle_scratch[i];
i += 1;
while let Some(update) = self
.services
.get_mut(&handle)
.filter(|slot| !slot.errored)
.and_then(|slot| slot.proto.poll())
{
if let ServiceUpdate::Renamed(ref renamed) = update {
let new_name = renamed.new_name().clone();
let rename_result = self.endpoint.handle_service_renamed(handle, new_name);
let handoff = self
.services
.get_mut(&handle)
.and_then(|slot| slot.proto.take_rename_goodbye_handoff());
if let Some(handoff) = handoff {
self
.endpoint
.enqueue_rename_withdrawal(handoff, now, rename_result.is_err());
}
if rename_result.is_err() {
if let Some(slot) = self.services.get_mut(&handle) {
slot.push_update(ServiceUpdate::Conflict);
slot.errored = true;
}
self.begin_service_withdrawal(handle, now);
break;
}
}
let is_terminal = update.is_conflict() || update.is_host_conflict();
if let Some(slot) = self.services.get_mut(&handle) {
slot.push_update(update);
if is_terminal {
slot.errored = true;
}
}
if is_terminal {
self.begin_service_withdrawal(handle, now);
break;
}
}
}
}
fn begin_service_withdrawal(&mut self, handle: ServiceHandle, now: I) {
let (snap, handoff) = match self.services.get_mut(&handle) {
Some(slot) => {
let handoff = slot.proto.take_rename_goodbye_handoff();
(slot.proto.withdrawal_snapshot(), handoff)
}
None => return,
};
if let Some(handoff) = handoff {
self.endpoint.enqueue_rename_withdrawal(handoff, now, true);
}
self.endpoint.begin_withdrawal(handle, snap, now);
}
fn poll_one_transmit(
&mut self,
now: I,
scratch: &mut [u8],
) -> Option<(SocketAddr, usize, Origin)> {
let cap = scratch.len().min(MAX_MDNS_MESSAGE);
let scratch = &mut scratch[..cap];
self.svc_handle_scratch.clear();
self
.svc_handle_scratch
.extend(self.services.keys().copied());
let mut i = 0;
while i < self.svc_handle_scratch.len() {
let handle = self.svc_handle_scratch[i];
i += 1;
let escalated = {
let Some(slot) = self.services.get_mut(&handle) else {
continue;
};
if slot.errored {
continue;
}
match slot.proto.poll_transmit(now, scratch) {
Ok(Some(transmit)) => {
return Some((transmit.dst(), transmit.size(), Origin::Service(handle)));
}
Ok(None) => false,
Err(_) => {
slot.push_update(ServiceUpdate::Conflict);
slot.errored = true;
true
}
}
};
if escalated {
self.begin_service_withdrawal(handle, now);
}
}
self.query_handle_scratch.clear();
self
.query_handle_scratch
.extend(self.queries.keys().copied());
let mut i = 0;
while i < self.query_handle_scratch.len() {
let handle = self.query_handle_scratch[i];
i += 1;
if self.queries.get(&handle).is_some_and(|slot| slot.errored) {
continue;
}
match self.endpoint.poll_query_transmit(handle, now, scratch) {
Ok(Some(transmit)) => {
return Some((transmit.dst(), transmit.size(), Origin::Query(handle)));
}
Ok(None) => {}
Err(_) => {
self.retire_query(handle);
}
}
}
None
}
fn note_transmit_result(&mut self, origin: Origin, now: I, delivered: bool) {
match origin {
Origin::Service(handle) => {
if let Some(slot) = self.services.get_mut(&handle) {
slot.proto.note_transmit_result(now, delivered);
if delivered {
self.endpoint.note_service_advertised(
handle,
slot.proto.advertised_a_addrs(),
slot.proto.advertised_aaaa_addrs(),
slot.proto.advertises_instance(),
);
}
}
}
Origin::Query(handle) => {
self
.endpoint
.note_query_transmit_result(handle, now, delivered);
}
}
}
fn retire_origin(&mut self, origin: Origin, now: I) {
match origin {
Origin::Service(handle) => {
if let Some(slot) = self.services.get_mut(&handle) {
slot.push_update(ServiceUpdate::Conflict);
slot.errored = true;
}
self.begin_service_withdrawal(handle, now);
}
Origin::Query(handle) => self.retire_query(handle),
}
}
fn retire_query(&mut self, handle: QueryHandle) {
self.endpoint.retire_query(handle);
if let Some(slot) = self.queries.get_mut(&handle) {
slot.errored = true;
}
}
pub fn poll_deadline(&self) -> Option<I> {
let mut best = self.endpoint.poll_timeout();
for slot in self.services.values() {
if slot.errored {
continue;
}
if let Some(deadline) = slot.proto.poll_timeout() {
best = Some(best.map_or(deadline, |b| b.min(deadline)));
}
}
for (handle, slot) in &self.queries {
if slot.errored {
continue;
}
if let Some(deadline) = self.endpoint.poll_query_timeout(*handle) {
best = Some(best.map_or(deadline, |b| b.min(deadline)));
}
}
best
}
pub fn poll_service_update(&mut self, handle: ServiceHandle) -> Option<ServiceUpdate> {
let slot = self.services.get_mut(&handle)?;
let update = slot.updates.pop_front();
if update.is_some() && slot.route_freed && slot.updates.is_empty() {
self.services.remove(&handle);
}
update
}
pub fn poll_query_update(&mut self, handle: QueryHandle) -> Option<QueryUpdate> {
self.endpoint.poll_query(handle)
}
pub fn poll_endpoint_event(&mut self) -> Option<EndpointEvent> {
self.endpoint.poll()
}
}