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;