Skip to main content

hick_compio/
endpoint.rs

1//! Caller-side handle for an mDNS endpoint.
2
3use core::{
4  net::{IpAddr, Ipv4Addr, Ipv6Addr},
5  time::Duration,
6};
7use std::{
8  io::{self, ErrorKind},
9  rc::Rc,
10  time::Instant,
11};
12
13use futures::future::select_all;
14use hick_trace::*;
15use hick_udp::{
16  MulticastOptionsV4, MulticastOptionsV6, try_bind_v4, try_bind_v6, try_join_v4, try_join_v6,
17};
18use mdns_proto::{
19  Name, QuerySpec, ServiceSpec,
20  wire::{A, AAAA, ResourceType},
21};
22
23use crate::{
24  discovery::{Lookup, QueryParam, Resolver, ServiceEntry, Step, feed, fold, push_capped},
25  driver::{EndpointInner, run},
26  error::{RegisterError, ServerError, StartQueryError},
27  options::ServerOptions,
28  query::{Query, QueryEvent},
29  service::Service,
30  socket::Socket,
31};
32
33/// Handle to a running mDNS endpoint.
34///
35/// Cloneable; every clone shares the same underlying driver task. The driver
36/// task is spawned at [`Self::server`] time and exits when the last clone
37/// (and every derived [`Service`] / [`Query`] handle) is dropped — detected
38/// via `Rc::strong_count(&inner) == 1` inside the driver loop.
39#[derive(Clone)]
40pub struct Endpoint {
41  pub(crate) inner: Rc<EndpointInner>,
42}
43
44impl Endpoint {
45  /// Bind the multicast sockets configured in `opts` and spawn the driver
46  /// task on the current compio runtime.
47  pub async fn server(opts: ServerOptions) -> Result<Self, ServerError> {
48    if !opts.ipv4() && !opts.ipv6() {
49      return Err(ServerError::NoFamilyEnabled);
50    }
51
52    let interface_index = match opts.interface_index() {
53      Some(i) => i,
54      None => pick_default_interface_index(opts.ipv4(), opts.ipv6()).ok_or_else(|| {
55        ServerError::Io(io::Error::new(
56          ErrorKind::NotFound,
57          "no multicast-capable interface found",
58        ))
59      })?,
60    };
61
62    // Gracefully degrade — if the chosen interface lacks one of the
63    // requested families, bind only the family it can support rather than
64    // failing the entire endpoint.
65    let iface_has_v4 = match getifs::interface_by_index(interface_index) {
66      Ok(Some(i)) => matches!(i.ipv4_addrs(), Ok(ref a) if !a.is_empty()),
67      _ => false,
68    };
69    let iface_has_v6 = match getifs::interface_by_index(interface_index) {
70      Ok(Some(i)) => matches!(i.ipv6_addrs(), Ok(ref a) if !a.is_empty()),
71      _ => false,
72    };
73    let bind_v4 = opts.ipv4() && iface_has_v4;
74    let bind_v6 = opts.ipv6() && iface_has_v6;
75    if !bind_v4 && !bind_v6 {
76      return Err(ServerError::Io(io::Error::new(
77        ErrorKind::AddrNotAvailable,
78        "interface has no address in any requested family",
79      )));
80    }
81
82    let std_v4 = if bind_v4 {
83      match try_bind_v4(MulticastOptionsV4::new(interface_index)) {
84        Ok(s) => {
85          debug!(interface_index, "bound v4 mDNS socket");
86          // Fail construction on a join error: a bound-but-unjoined socket can
87          // send but never receives multicast, making the endpoint silently
88          // non-functional. Mirrors the reactor's fatal-join setup.
89          match try_join_v4(&s, interface_index) {
90            Ok(()) => debug!(interface_index, "joined v4 mDNS multicast group"),
91            Err(e) => {
92              warn!(error = %e, interface_index, "failed to join v4 mDNS multicast group");
93              return Err(ServerError::JoinV4(e));
94            }
95          }
96          s.set_nonblocking(true).map_err(ServerError::Io)?;
97          Some(s)
98        }
99        Err(e) => {
100          warn!(error = %e, interface_index, "failed to bind v4 mDNS socket");
101          return Err(ServerError::BindV4(e));
102        }
103      }
104    } else {
105      None
106    };
107
108    let std_v6 = if bind_v6 {
109      match try_bind_v6(MulticastOptionsV6::new(interface_index)) {
110        Ok(s) => {
111          debug!(interface_index, "bound v6 mDNS socket");
112          match try_join_v6(&s, interface_index) {
113            Ok(()) => debug!(interface_index, "joined v6 mDNS multicast group"),
114            Err(e) => {
115              warn!(error = %e, interface_index, "failed to join v6 mDNS multicast group");
116              return Err(ServerError::JoinV6(e));
117            }
118          }
119          s.set_nonblocking(true).map_err(ServerError::Io)?;
120          Some(s)
121        }
122        Err(e) => {
123          warn!(error = %e, interface_index, "failed to bind v6 mDNS socket");
124          return Err(ServerError::BindV6(e));
125        }
126      }
127    } else {
128      None
129    };
130
131    let sock_v4 = if let Some(s) = std_v4 {
132      Some(Rc::new(
133        Socket::from_std(s).await.map_err(ServerError::WrapSocket)?,
134      ))
135    } else {
136      None
137    };
138    let sock_v6 = if let Some(s) = std_v6 {
139      Some(Rc::new(
140        Socket::from_std(s).await.map_err(ServerError::WrapSocket)?,
141      ))
142    } else {
143      None
144    };
145
146    let inner = EndpointInner::new(
147      *opts.endpoint_config(),
148      opts.max_payload_size(),
149      opts.max_recv_packet_size(),
150    );
151
152    // Populate the §11 source-address fallback's local-subnet snapshot and
153    // pin the bound interface index under a brief borrow.
154    {
155      let mut st = inner.state.borrow_mut();
156      st.bound_interface = interface_index;
157      st.local_subnets = collect_local_subnets(interface_index);
158    }
159
160    let driver_fut = run(inner.clone(), sock_v4, sock_v6);
161    compio_runtime::spawn(driver_fut).detach();
162
163    Ok(Self { inner })
164  }
165
166  /// Return a point-in-time snapshot of the I/O + protocol counters for this
167  /// endpoint.
168  ///
169  /// Delegates to the shared [`stats::Stats`] instance owned by
170  /// the proto endpoint (and shared with the driver I/O paths), so the
171  /// snapshot covers wire-level rx/tx as well as protocol-level events.
172  #[cfg(feature = "stats")]
173  #[cfg_attr(docsrs, doc(cfg(feature = "stats")))]
174  pub fn stats(&self) -> stats::StatsSnapshot {
175    self.inner.state.borrow().stats.snapshot()
176  }
177
178  /// Register a new service.
179  ///
180  /// Returns a [`Service`] handle for receiving [`ServiceUpdate`] events;
181  /// dropping the handle implicitly unregisters the service. Drop encodes
182  /// the RFC 6762 §10.1 goodbye records and tears down the proto state
183  /// synchronously; the driver task then multicasts the queued TTL=0
184  /// datagrams a few times for loss resilience.
185  ///
186  /// [`ServiceUpdate`]: mdns_proto::ServiceUpdate
187  pub async fn register_service(&self, spec: ServiceSpec) -> Result<Service, RegisterError> {
188    let now = Instant::now();
189    // The handle-owned delivery mailbox: the driver ctx holds one clone (fills
190    // it), the returned `Service` handle holds the other (drains it). Created
191    // before the proto registration so both sides share the same buffer.
192    let mailbox = crate::service::new_service_mailbox();
193    let handle = {
194      let mut st = self.inner.state.borrow_mut();
195      st.register_service(spec, now, std::rc::Rc::clone(&mailbox))
196        .map_err(RegisterError::from)?
197    };
198    // Durable wake: a bare `notify()` can be lost across the driver's
199    // send-awaits, leaving newly-registered work unobserved (see
200    // `EndpointInner::dirty`). `mark_dirty` sets the level flag AND notifies.
201    self.inner.mark_dirty();
202    Ok(Service {
203      inner: self.inner.clone(),
204      handle,
205      mailbox,
206    })
207  }
208
209  /// Start a query against the endpoint.
210  ///
211  /// Returns a [`Query`] handle for receiving [`QueryEvent`]
212  /// events; dropping the handle implicitly cancels the query (the driver
213  /// removes the proto state machine on its next loop iteration).
214  pub async fn start_query(&self, spec: QuerySpec) -> Result<Query, StartQueryError> {
215    let now = Instant::now();
216    let handle = {
217      let mut st = self.inner.state.borrow_mut();
218      st.start_query(spec, now)
219        .map_err(|_| StartQueryError::StorageFull)?
220    };
221    // Durable wake (see `register_service` / `EndpointInner::dirty`): critical
222    // for a timeout-less `QuerySpec`, whose first transmit is the ONLY thing
223    // that would schedule a deadline — if this wake is lost and no deadline
224    // exists yet, the driver would otherwise park forever.
225    self.inner.mark_dirty();
226    Ok(Query {
227      inner: self.inner.clone(),
228      handle,
229      terminal_delivered: core::cell::Cell::new(false),
230    })
231  }
232
233  /// Browse for instances of a DNS-SD service type, resolving each into a
234  /// [`ServiceEntry`].  See [`Lookup`] and [`QueryParam`].
235  pub async fn browse(&self, param: QueryParam) -> Result<Lookup, StartQueryError> {
236    let resolve_timeout = param.resolve_timeout.unwrap_or(param.timeout);
237    // Size the PTR answer pool to the requested instance cap so a `max_entries`
238    // above the proto's default answer cap is actually reachable; without this
239    // surplus PTR answers would be evicted before the resolver could track
240    // them.
241    let ptr_spec = QuerySpec::new(param.service, ResourceType::Ptr)
242      .with_timeout(param.timeout)
243      .with_unicast_response(param.unicast_response)
244      .with_max_answers(param.max_entries);
245    let ptr_query = self.start_query(ptr_spec).await?;
246    Ok(Lookup {
247      endpoint: self.clone(),
248      queries: vec![(ptr_query, Step::Ptr)],
249      resolver: Resolver::new(param.max_entries),
250      resolve_timeout,
251      unicast: param.unicast_response,
252      finished: false,
253    })
254  }
255
256  /// Convenience for [`Self::browse`] with default parameters and the given
257  /// browse timeout.
258  pub async fn lookup(&self, service: Name, timeout: Duration) -> Result<Lookup, StartQueryError> {
259    self
260      .browse(QueryParam::new(service).with_timeout(timeout))
261      .await
262  }
263
264  /// Resolve a host name to its addresses via mDNS A / AAAA queries (RFC 6762),
265  /// without the DNS-SD browse/resolve chain.
266  ///
267  /// Issues both queries and collects every advertised address for the
268  /// `timeout` window (the answer window for multicast responses), returning
269  /// them IPv4 first then IPv6, deduplicated and capped per family.  The
270  /// result is empty if nothing answers.  Unlike [`Self::resolve_instance`]
271  /// this does not require — or interpret — DNS-SD records; it is the
272  /// multicast analogue of resolving a hostname.
273  pub async fn resolve_host(
274    &self,
275    host: Name,
276    timeout: Duration,
277  ) -> Result<Vec<IpAddr>, StartQueryError> {
278    // One deadline shared across both queries: the call stays bounded by
279    // `timeout` even if launching the second sub-query takes some of it.
280    // `checked_add` so `Duration::MAX` cannot panic the way `Instant + Duration`
281    // would.
282    let deadline = Instant::now().checked_add(timeout);
283    let remaining = || deadline.map_or(timeout, |d| d.saturating_duration_since(Instant::now()));
284    let host_key = fold(&host);
285    let qa = self
286      .start_query(QuerySpec::new(host.clone(), ResourceType::A).with_timeout(remaining()))
287      .await?;
288    let qaaaa = self
289      .start_query(QuerySpec::new(host, ResourceType::AAAA).with_timeout(remaining()))
290      .await?;
291    let mut ipv4: Vec<Ipv4Addr> = Vec::new();
292    let mut ipv6: Vec<Ipv6Addr> = Vec::new();
293    let mut queries: Vec<(Query, Step)> = vec![
294      (qa, Step::A(host_key.clone())),
295      (qaaaa, Step::AAAA(host_key)),
296    ];
297    while !queries.is_empty() {
298      let (result, idx) = {
299        let futs: Vec<_> = queries.iter().map(|(q, _)| Box::pin(q.next())).collect();
300        let (result, idx, _remaining) = select_all(futs).await;
301        (result, idx)
302      };
303      match result {
304        Some(QueryEvent::Answer(ans)) => match &queries[idx].1 {
305          Step::A(_) if ans.rtype() == ResourceType::A => {
306            if let Ok(r) = A::try_from_rdata(ans.rdata_slice()) {
307              push_capped(&mut ipv4, r.addr());
308            }
309          }
310          Step::AAAA(_) if ans.rtype() == ResourceType::AAAA => {
311            if let Ok(r) = AAAA::try_from_rdata(ans.rdata_slice()) {
312              push_capped(&mut ipv6, r.addr());
313            }
314          }
315          _ => {}
316        },
317        Some(QueryEvent::Terminal(_)) | None => {
318          queries.swap_remove(idx);
319        }
320      }
321    }
322    Ok(
323      ipv4
324        .into_iter()
325        .map(IpAddr::V4)
326        .chain(ipv6.into_iter().map(IpAddr::V6))
327        .collect(),
328    )
329  }
330
331  /// Resolve a *known* DNS-SD service instance directly into a
332  /// [`ServiceEntry`], skipping the PTR browse step (e.g.
333  /// `Name::try_from_str("Office._ipp._tcp.local.")`).
334  ///
335  /// Issues SRV + TXT for the instance and A / AAAA for the SRV target host,
336  /// and returns the first complete resolution — host + port, TXT, and at
337  /// least one address — or `None` if it does not complete within `timeout`.
338  /// Use [`Self::browse`] instead when the instance names are not known in
339  /// advance.
340  pub async fn resolve_instance(
341    &self,
342    instance: Name,
343    timeout: Duration,
344  ) -> Result<Option<ServiceEntry>, StartQueryError> {
345    // Shared deadline so an SRV arriving late cannot grant the A/AAAA
346    // follow-ups a second full `timeout` window.
347    let deadline = Instant::now().checked_add(timeout);
348    let remaining = || deadline.map_or(timeout, |d| d.saturating_duration_since(Instant::now()));
349    let mut resolver = Resolver::new(1);
350    let mut queries: Vec<(Query, Step)> = Vec::new();
351    for start in resolver.on_ptr(instance) {
352      let qtype = start.qtype();
353      let q = self
354        .start_query(QuerySpec::new(start.name, qtype).with_timeout(remaining()))
355        .await?;
356      queries.push((q, start.step));
357    }
358    loop {
359      if let Some(e) = resolver.take_ready() {
360        return Ok(Some(e));
361      }
362      if queries.is_empty() {
363        return Ok(None);
364      }
365      let (result, idx) = {
366        let futs: Vec<_> = queries.iter().map(|(q, _)| Box::pin(q.next())).collect();
367        let (result, idx, _remaining) = select_all(futs).await;
368        (result, idx)
369      };
370      match result {
371        Some(QueryEvent::Answer(_)) => {
372          let step = queries[idx].1.clone();
373          let event = result.expect("matched Some above");
374          for start in feed(&mut resolver, step, event) {
375            let qtype = start.qtype();
376            let q = self
377              .start_query(QuerySpec::new(start.name, qtype).with_timeout(remaining()))
378              .await?;
379            queries.push((q, start.step));
380          }
381        }
382        Some(QueryEvent::Terminal(_)) | None => {
383          queries.swap_remove(idx);
384        }
385      }
386    }
387  }
388}
389
390/// Wake the driver when an `Endpoint` clone is dropped so it can promptly
391/// observe `Rc::strong_count(&inner) == 1` and exit (per the exit-condition
392/// contract). Without this notify, the driver would only notice the dropped
393/// handle the next time it woke for a recv / timer event.
394impl Drop for Endpoint {
395  fn drop(&mut self) {
396    self.inner.notify.notify();
397  }
398}
399
400/// Snapshot the local IPv4 / IPv6 subnets owned by `iface_index`. Used by
401/// the §11 on-link source-address fallback when the kernel did not deliver
402/// an IPv4 TTL / IPv6 hop-limit cmsg.
403fn collect_local_subnets(iface_index: u32) -> Vec<(IpAddr, u8)> {
404  let mut out: Vec<(IpAddr, u8)> = Vec::new();
405  if iface_index == 0 {
406    return out;
407  }
408  if let Ok(Some(i)) = getifs::interface_by_index(iface_index) {
409    if let Ok(v4s) = i.ipv4_addrs() {
410      for n in v4s.iter() {
411        out.push((IpAddr::V4(n.addr()), n.prefix_len()));
412      }
413    }
414    if let Ok(v6s) = i.ipv6_addrs() {
415      for n in v6s.iter() {
416        out.push((IpAddr::V6(n.addr()), n.prefix_len()));
417      }
418    }
419  }
420  out
421}
422
423/// Pick a default interface index when the caller didn't pin one. Mirrors
424/// the algorithm in `hick-reactor::endpoint::pick_default_interface_index`:
425/// prefer a non-loopback, multicast-up interface that satisfies every
426/// requested family; fall back through looser predicates ending at the
427/// loopback interface.
428fn pick_default_interface_index(want_v4: bool, want_v6: bool) -> Option<u32> {
429  let ifs = getifs::interfaces().ok()?;
430  let has_v4 = |i: &getifs::Interface| matches!(i.ipv4_addrs(), Ok(ref v) if !v.is_empty());
431  let has_v6 = |i: &getifs::Interface| matches!(i.ipv6_addrs(), Ok(ref v) if !v.is_empty());
432  let multicast_up_non_loopback = |i: &getifs::Interface| -> bool {
433    let f = i.flags();
434    f.contains(getifs::Flags::UP)
435      && f.contains(getifs::Flags::MULTICAST)
436      && !f.contains(getifs::Flags::LOOPBACK)
437      && i.index() != 0
438  };
439  let loopback_up = |i: &getifs::Interface| -> bool {
440    i.flags().contains(getifs::Flags::LOOPBACK) && i.flags().contains(getifs::Flags::UP)
441  };
442  let strict =
443    |i: &&getifs::Interface| -> bool { (!want_v4 || has_v4(i)) && (!want_v6 || has_v6(i)) };
444  let loose = |i: &&getifs::Interface| -> bool { (want_v4 && has_v4(i)) || (want_v6 && has_v6(i)) };
445  ifs
446    .iter()
447    .find(|i| multicast_up_non_loopback(i) && strict(i))
448    .or_else(|| {
449      ifs
450        .iter()
451        .find(|i| multicast_up_non_loopback(i) && loose(i))
452    })
453    .or_else(|| ifs.iter().find(|i| loopback_up(i) && strict(i)))
454    .or_else(|| ifs.iter().find(|i| loopback_up(i) && loose(i)))
455    .map(|i| i.index())
456}
457
458#[cfg(test)]
459mod tests;