mdns_proto/event.rs
1//! Event types flowing between [`Endpoint`](crate::Endpoint),
2//! [`Service`](crate::Service), and [`Query`](crate::Query).
3
4use core::net::SocketAddr;
5
6use derive_more::{IsVariant, TryUnwrap, Unwrap};
7
8cfg_storage! {
9 use crate::Name;
10}
11use crate::{
12 QueryHandle, ServiceHandle,
13 wire::{QuestionRef, Ref},
14};
15
16/// A question routed to a registered service.
17#[derive(Debug, Copy, Clone)]
18pub struct ServiceQuestion<'a> {
19 question: QuestionRef<'a>,
20 src: SocketAddr,
21 query_id: u16,
22 truncated: bool,
23}
24impl<'a> ServiceQuestion<'a> {
25 #[allow(dead_code)]
26 #[inline(always)]
27 pub(crate) const fn new(question: QuestionRef<'a>, src: SocketAddr, query_id: u16) -> Self {
28 Self {
29 question,
30 src,
31 query_id,
32 truncated: false,
33 }
34 }
35 /// Marks whether the query packet carrying this question had the TC
36 /// (truncated) bit set — i.e. the querier is spreading its known-answer list
37 /// across multiple packets (RFC 6762 §7.2). The responder uses this to pick a
38 /// longer (400–500 ms) response delay so the follow-up known-answer packets
39 /// can arrive before it decides what to suppress.
40 #[allow(dead_code)]
41 #[inline(always)]
42 pub(crate) const fn with_truncated(mut self, truncated: bool) -> Self {
43 self.truncated = truncated;
44 self
45 }
46 /// Returns the DNS question.
47 #[inline(always)]
48 pub const fn question(&self) -> &QuestionRef<'a> {
49 &self.question
50 }
51 /// `true` if the query packet had the TC bit set (RFC 6762 §7.2 multipacket
52 /// known-answer suppression — more known-answer packets follow).
53 #[inline(always)]
54 pub const fn truncated(&self) -> bool {
55 self.truncated
56 }
57 /// Returns the source socket address of the peer that sent the query.
58 #[inline(always)]
59 pub const fn src(&self) -> SocketAddr {
60 self.src
61 }
62 /// Returns the DNS transaction ID of the query, echoed in a legacy
63 /// unicast response (RFC 6762 §6.7).
64 #[inline(always)]
65 pub const fn query_id(&self) -> u16 {
66 self.query_id
67 }
68}
69
70/// A response record observed during another peer's probe — indicates this
71/// service's name is in use, triggering conflict resolution.
72#[derive(Debug, Copy, Clone)]
73pub struct ProbeConflict<'a> {
74 src: SocketAddr,
75 record: Ref<'a>,
76}
77impl<'a> ProbeConflict<'a> {
78 #[allow(dead_code)]
79 #[inline(always)]
80 pub(crate) const fn new(src: SocketAddr, record: Ref<'a>) -> Self {
81 Self { src, record }
82 }
83 /// Returns the source address of the peer that sent the conflicting probe.
84 #[inline(always)]
85 pub const fn src(&self) -> SocketAddr {
86 self.src
87 }
88 /// Returns the conflicting DNS record observed from a peer.
89 #[inline(always)]
90 pub const fn record(&self) -> &Ref<'a> {
91 &self.record
92 }
93}
94
95/// A response record observed during another peer's probe — indicates this
96/// service's **host** name (A/AAAA owner) is already claimed by a peer.
97///
98/// Unlike [`ProbeConflict`], this does NOT trigger an automatic instance
99/// rename. The caller must resolve the host-name conflict out-of-band (e.g.
100/// by choosing a new host name and re-registering the service).
101#[derive(Debug, Copy, Clone)]
102pub struct HostConflict<'a> {
103 record: Ref<'a>,
104}
105impl<'a> HostConflict<'a> {
106 #[allow(dead_code)]
107 #[inline(always)]
108 pub(crate) const fn new(record: Ref<'a>) -> Self {
109 Self { record }
110 }
111 /// Returns the conflicting DNS record observed from a peer.
112 #[inline(always)]
113 pub const fn record(&self) -> &Ref<'a> {
114 &self.record
115 }
116}
117
118/// A known-answer hint observed in an incoming query, used to suppress
119/// our outgoing answers when the asker already has the same record.
120///
121/// carries `src` so the service can scope the hint to the
122/// querier that supplied it. Hints from sources that did NOT issue a
123/// Question in the current response cycle are dropped — without this,
124/// an attacker could inject KAS hints during a legitimate questioner's
125/// jitter window and suppress the response.
126#[derive(Debug, Copy, Clone)]
127pub struct KnownAnswer<'a> {
128 src: core::net::SocketAddr,
129 record: Ref<'a>,
130}
131impl<'a> KnownAnswer<'a> {
132 #[allow(dead_code)]
133 #[inline(always)]
134 pub(crate) const fn new(src: core::net::SocketAddr, record: Ref<'a>) -> Self {
135 Self { src, record }
136 }
137 /// Returns the known-answer DNS record from the incoming query.
138 #[inline(always)]
139 pub const fn record(&self) -> &Ref<'a> {
140 &self.record
141 }
142 /// Source address of the packet that carried this hint.
143 #[inline(always)]
144 pub const fn src(&self) -> core::net::SocketAddr {
145 self.src
146 }
147}
148
149/// Events delivered from `Endpoint::handle()` to a `Service`.
150#[derive(Debug, Copy, Clone, IsVariant, Unwrap, TryUnwrap)]
151#[unwrap(ref)]
152#[try_unwrap(ref)]
153#[non_exhaustive]
154pub enum ServiceEvent<'a> {
155 /// A question targeting this service arrived.
156 Question(ServiceQuestion<'a>),
157 /// While probing, another peer responded authoritatively for our **instance** name.
158 /// The service will auto-rename.
159 ProbeConflict(ProbeConflict<'a>),
160 /// While probing, another peer responded authoritatively for our **host** name
161 /// (A/AAAA owner). The service will NOT auto-rename — the caller must
162 /// intervene. See [`ServiceUpdate::HostConflict`].
163 HostConflict(HostConflict<'a>),
164 /// A known-answer hint from an incoming query — caller may use it for
165 /// KAS-style suppression of outgoing answers.
166 KnownAnswer(KnownAnswer<'a>),
167}
168
169/// Events delivered from `Endpoint::handle()` to a `Query`.
170#[derive(Debug, Copy, Clone, IsVariant, Unwrap, TryUnwrap)]
171#[unwrap(ref)]
172#[try_unwrap(ref)]
173#[non_exhaustive]
174pub enum QueryEvent<'a> {
175 /// A matching answer record arrived.
176 Answer(Ref<'a>),
177 /// Query received a truncated response (TC bit). Caller should hold
178 /// off cancelling this query — more KAS suppression follows.
179 Truncated,
180}
181
182/// A routing decision produced by `Endpoint::handle()`.
183#[derive(Debug, Copy, Clone)]
184pub struct ToService<'a> {
185 handle: ServiceHandle,
186 event: ServiceEvent<'a>,
187}
188impl<'a> ToService<'a> {
189 #[allow(dead_code)]
190 #[inline(always)]
191 pub(crate) const fn new(handle: ServiceHandle, event: ServiceEvent<'a>) -> Self {
192 Self { handle, event }
193 }
194 /// Returns the service handle this event is addressed to.
195 #[inline(always)]
196 pub const fn handle(&self) -> ServiceHandle {
197 self.handle
198 }
199 /// Returns a reference to the service event payload.
200 #[inline(always)]
201 pub const fn event(&self) -> &ServiceEvent<'a> {
202 &self.event
203 }
204 /// Consumes the routing decision and returns the inner event.
205 #[inline(always)]
206 pub const fn into_event(self) -> ServiceEvent<'a> {
207 self.event
208 }
209}
210
211/// A routing decision produced by `Endpoint::handle()`.
212#[derive(Debug, Copy, Clone)]
213pub struct ToQuery<'a> {
214 handle: QueryHandle,
215 event: QueryEvent<'a>,
216}
217impl<'a> ToQuery<'a> {
218 #[allow(dead_code)]
219 #[inline(always)]
220 pub(crate) const fn new(handle: QueryHandle, event: QueryEvent<'a>) -> Self {
221 Self { handle, event }
222 }
223 /// Returns the query handle this event is addressed to.
224 #[inline(always)]
225 pub const fn handle(&self) -> QueryHandle {
226 self.handle
227 }
228 /// Returns a reference to the query event payload.
229 #[inline(always)]
230 pub const fn event(&self) -> &QueryEvent<'a> {
231 &self.event
232 }
233 /// Consumes the routing decision and returns the inner event.
234 #[inline(always)]
235 pub const fn into_event(self) -> QueryEvent<'a> {
236 self.event
237 }
238}
239
240/// A routing decision yielded by `Endpoint::handle()`.
241#[derive(Debug, Copy, Clone, IsVariant, Unwrap, TryUnwrap)]
242#[unwrap(ref)]
243#[try_unwrap(ref)]
244#[non_exhaustive]
245pub enum RouteEvent<'a> {
246 /// Route the event to the matched service.
247 ToService(ToService<'a>),
248 /// Route the event to the matched query.
249 ToQuery(ToQuery<'a>),
250 /// The endpoint cache observed new or refreshed records as a side effect
251 /// of this datagram. Callers can use this as a hint to re-poll queries.
252 CacheUpdated,
253}
254
255cfg_storage! {
256 /// Detail payload for [`ServiceUpdate::Renamed`].
257 #[derive(Debug, Clone, Eq, PartialEq)]
258 pub struct ServiceRenamed {
259 new_name: Name,
260 }
261
262 const _: () = {
263 impl ServiceRenamed {
264 /// Construct a `ServiceRenamed` from a new name.
265 ///
266 /// Hidden from the documented surface: the proto state machine builds these
267 /// internally on a conflict rename, but downstream crates need a way to
268 /// synthesize them for tests (mirroring [`crate::CollectedAnswer::from_parts`]).
269 #[doc(hidden)]
270 #[inline(always)]
271 pub fn new(new_name: Name) -> Self {
272 Self { new_name }
273 }
274 /// Returns the new canonical name assigned after conflict resolution.
275 #[inline(always)]
276 pub fn new_name(&self) -> &Name {
277 &self.new_name
278 }
279 }
280 };
281
282 /// App-level events emitted by `Service::poll()`.
283 #[allow(clippy::large_enum_variant)]
284 #[derive(Debug, Clone, IsVariant, Unwrap, TryUnwrap)]
285 #[unwrap(ref)]
286 #[try_unwrap(ref)]
287 #[non_exhaustive]
288 pub enum ServiceUpdate {
289 /// Probing completed without conflict; the service is now advertised.
290 Established,
291 /// Probing detected a conflict; the service rebranded to a new name.
292 Renamed(ServiceRenamed),
293 /// A conflict cannot be resolved automatically (e.g. tiebreak space
294 /// exhausted). The caller must intervene.
295 Conflict,
296 /// A peer claimed our **host** name (A/AAAA owner) during probing.
297 ///
298 /// The service does NOT rename itself automatically. The caller must
299 /// resolve the conflict by choosing a new host name and re-registering,
300 /// or by deferring to the peer.
301 HostConflict,
302 }
303}
304
305/// App-level events emitted by `Query::poll()`.
306#[derive(Debug, Clone, Copy, IsVariant, Unwrap, TryUnwrap)]
307#[unwrap(ref)]
308#[try_unwrap(ref)]
309#[non_exhaustive]
310pub enum QueryUpdate {
311 /// Query's retry budget expired without finding a match.
312 Timeout,
313 /// Query is finished (caller cancelled or completed enough answers).
314 Done,
315}
316
317/// App-level events emitted by `Endpoint::poll()`.
318#[derive(Debug, Copy, Clone, IsVariant, Unwrap, TryUnwrap)]
319#[unwrap(ref)]
320#[try_unwrap(ref)]
321#[non_exhaustive]
322pub enum EndpointEvent {
323 /// A cached record's TTL expired.
324 CacheExpired,
325}