Skip to main content

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}