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