Skip to main content

mdns_proto/
records.rs

1//! Records published by a registered [`Service`].
2//!
3//! `ServiceRecords` is the durable, post-validation bundle: a name, A/AAAA
4//! addresses, the SRV target/port, and the TXT segments. Building the wire
5//! response is then a mechanical traversal.
6
7use crate::{
8  Name,
9  backend::{RdataBuf, Shared, rdata_from_vec},
10};
11use core::net::{Ipv4Addr, Ipv6Addr};
12use std::vec::Vec;
13
14/// Append `item` to a read-only `Shared<[T]>`, returning a freshly sealed slice.
15/// `ServiceRecords`' collections are built incrementally via the `add_*`
16/// builders then frozen, so the O(n) reseal per append is paid only at build
17/// time (n is a handful of addresses / subtypes) — in exchange the derived
18/// `ServiceRecords::clone` is O(1) on the withdrawal-snapshot and rename-handoff
19/// paths, which previously deep-copied all five collections.
20fn arc_push<T: Clone>(slice: &[T], item: T) -> Shared<[T]> {
21  slice
22    .iter()
23    .cloned()
24    .chain(core::iter::once(item))
25    .collect()
26}
27
28/// Records advertised by a single registered service.
29#[derive(Debug, Clone, Eq, PartialEq)]
30#[non_exhaustive]
31pub struct ServiceRecords {
32  /// DNS-SD service-type PTR owner (e.g. `_ipp._tcp.local.`). Required for
33  /// RFC 6763 discovery — browsers query this name to enumerate instances.
34  service_type: Name,
35  instance: Name,
36  host: Name,
37  port: u16,
38  priority: u16,
39  weight: u16,
40  a_addrs: Shared<[Ipv4Addr]>,
41  aaaa_addrs: Shared<[Ipv6Addr]>,
42  /// Parallel to `aaaa_addrs`: per-AAAA interface scope id (0 = "any").
43  ///
44  /// Used by [`Endpoint`](crate::endpoint::Endpoint) to disambiguate IPv6
45  /// self-loopback when the same link-local address (e.g. `fe80::1`) might
46  /// exist on multiple interfaces.  without scope, a peer using
47  /// the same link-local on a different interface would be wrongly
48  /// classified as self.  Set via [`Self::add_aaaa_scoped`].  Always the
49  /// same length as `aaaa_addrs`.
50  aaaa_scopes: Shared<[u32]>,
51  txt: Shared<[RdataBuf]>,
52  /// RFC 6763 §7.1 subtype browse names, e.g. `_printer._sub._ipp._tcp.local.`.
53  /// Each is the full `<sub>._sub.<service_type>` name (derived from
54  /// `service_type` at [`Self::add_subtype`] time, so it survives an instance
55  /// rename — the service type does not change). The service emits a shared PTR
56  /// `<browse_name> -> instance` for each, and answers browse queries for them.
57  subtypes: Shared<[Name]>,
58  ttl_secs: u32,
59}
60
61impl ServiceRecords {
62  /// Construct a new record bundle with the required fields. Optional fields
63  /// (records, txt, priority, weight) start empty/default.
64  ///
65  /// `service_type` is the DNS-SD PTR owner, e.g. `_ipp._tcp.local.`.
66  /// It must be the parent label sequence of `instance`.
67  pub fn new(service_type: Name, instance: Name, host: Name, port: u16, ttl_secs: u32) -> Self {
68    Self {
69      service_type,
70      instance,
71      host,
72      port,
73      priority: 0,
74      weight: 0,
75      a_addrs: Shared::from([]),
76      aaaa_addrs: Shared::from([]),
77      aaaa_scopes: Shared::from([]),
78      txt: Shared::from([]),
79      subtypes: Shared::from([]),
80      ttl_secs,
81    }
82  }
83
84  /// The DNS-SD service type (PTR owner), e.g. `_ipp._tcp.local.`.
85  #[inline(always)]
86  pub fn service_type(&self) -> &Name {
87    &self.service_type
88  }
89
90  /// The instance name (e.g. `MyPrinter._ipp._tcp.local.`).
91  #[inline(always)]
92  pub fn instance(&self) -> &Name {
93    &self.instance
94  }
95
96  /// The SRV target hostname.
97  #[inline(always)]
98  pub fn host(&self) -> &Name {
99    &self.host
100  }
101
102  /// The service port.
103  #[inline(always)]
104  pub const fn port(&self) -> u16 {
105    self.port
106  }
107
108  /// SRV priority field.
109  #[inline(always)]
110  pub const fn priority(&self) -> u16 {
111    self.priority
112  }
113
114  /// SRV weight field.
115  #[inline(always)]
116  pub const fn weight(&self) -> u16 {
117    self.weight
118  }
119
120  /// Record TTL in seconds.
121  #[inline(always)]
122  pub const fn ttl_secs(&self) -> u32 {
123    self.ttl_secs
124  }
125
126  /// Slice of IPv4 addresses.
127  #[inline(always)]
128  pub fn a_addrs_slice(&self) -> &[Ipv4Addr] {
129    &self.a_addrs
130  }
131
132  /// Slice of IPv6 addresses.
133  #[inline(always)]
134  pub fn aaaa_addrs_slice(&self) -> &[Ipv6Addr] {
135    &self.aaaa_addrs
136  }
137
138  /// Slice of per-AAAA interface scope ids (one entry per AAAA address;
139  /// parallel to [`Self::aaaa_addrs_slice`]).  A scope of `0` means
140  /// "unscoped / any interface" — appropriate for global addresses or
141  /// when the caller does not know which interface the link-local will
142  /// be published on.
143  ///
144  /// Used by [`Endpoint`](crate::endpoint::Endpoint) to disambiguate
145  /// IPv6 link-local self-loopback on multi-homed hosts.
146  #[inline(always)]
147  pub fn aaaa_scopes_slice(&self) -> &[u32] {
148    &self.aaaa_scopes
149  }
150
151  /// TXT segments as an iterator over byte slices.
152  pub fn txt_segments(&self) -> impl Iterator<Item = &[u8]> {
153    // `|b| b.as_ref()` (not `Bytes::as_ref`) so this compiles for both the
154    // `bytes::Bytes` and no-atomic `Arc<[u8]>` `RdataBuf` flavors.
155    self.txt.iter().map(|b| b.as_ref())
156  }
157
158  /// Append an IPv4 address.
159  pub fn add_a(&mut self, addr: Ipv4Addr) -> &mut Self {
160    self.a_addrs = arc_push(&self.a_addrs, addr);
161    self
162  }
163
164  /// Append an IPv6 address with an unspecified interface scope.
165  ///
166  /// Equivalent to [`Self::add_aaaa_scoped`] with `scope_id = 0`.  For
167  /// link-local addresses on multi-homed hosts prefer
168  /// [`Self::add_aaaa_scoped`] so self-loopback detection can
169  /// distinguish your own packets from peer packets that share the
170  /// same numeric link-local on another interface.
171  pub fn add_aaaa(&mut self, addr: Ipv6Addr) -> &mut Self {
172    self.add_aaaa_scoped(addr, 0)
173  }
174
175  /// Append an IPv6 address bound to a specific interface scope.
176  ///
177  /// `scope_id` is typically the receiving interface index (the same
178  /// value the host's `if_nametoindex(3)` returns).
179  /// `Endpoint::handle` matches self-loopback for link-local sources by
180  /// `(address, scope)` so a peer's identical link-local on a
181  /// different interface is NOT misclassified as self.
182  ///
183  /// A `scope_id` of `0` keeps the legacy behaviour (matches any
184  /// interface).  For global / unique-local IPv6 the scope is
185  /// effectively ignored.
186  pub fn add_aaaa_scoped(&mut self, addr: Ipv6Addr, scope_id: u32) -> &mut Self {
187    self.aaaa_addrs = arc_push(&self.aaaa_addrs, addr);
188    self.aaaa_scopes = arc_push(&self.aaaa_scopes, scope_id);
189    self
190  }
191
192  /// Append a TXT segment.
193  pub fn add_txt_segment(&mut self, segment: Vec<u8>) -> &mut Self {
194    self.txt = arc_push(&self.txt, rdata_from_vec(segment));
195    self
196  }
197
198  /// The RFC 6763 §7.1 subtype browse names registered for this service (each
199  /// the full `<subtype>._sub.<service_type>` form).
200  #[inline(always)]
201  pub fn subtype_names(&self) -> &[Name] {
202    &self.subtypes
203  }
204
205  /// Register a DNS-SD subtype (RFC 6763 §7.1). `subtype` is the subtype label
206  /// (e.g. `"_printer"`); the full browse name `<subtype>._sub.<service_type>`
207  /// is derived from the current service type and stored. The service then
208  /// advertises a shared PTR `<browse_name> -> instance` and answers browse
209  /// queries for it.
210  ///
211  /// Returns a [`NameError`](crate::name::NameError) if the derived
212  /// `<subtype>._sub.<service_type>` is not a valid DNS name (e.g. an over-long
213  /// label or total length).
214  pub fn add_subtype(&mut self, subtype: &str) -> Result<&mut Self, crate::name::NameError> {
215    let browse = std::format!(
216      "{}._sub.{}",
217      subtype.trim_end_matches('.'),
218      self.service_type.as_str()
219    );
220    let name = Name::try_from_str(&browse)?;
221    self.subtypes = arc_push(&self.subtypes, name);
222    Ok(self)
223  }
224
225  /// Set SRV priority.
226  pub fn set_priority(&mut self, v: u16) -> &mut Self {
227    self.priority = v;
228    self
229  }
230
231  /// Set SRV weight.
232  pub fn set_weight(&mut self, v: u16) -> &mut Self {
233    self.weight = v;
234    self
235  }
236
237  /// Set TTL in seconds.
238  pub fn set_ttl_secs(&mut self, v: u32) -> &mut Self {
239    self.ttl_secs = v;
240    self
241  }
242
243  /// Replace the instance name (used during conflict-driven rename).
244  pub fn set_instance(&mut self, name: Name) -> &mut Self {
245    self.instance = name;
246    self
247  }
248}
249
250#[cfg(test)]
251#[allow(clippy::unwrap_used)]
252mod tests;