simple_mdns/async_discovery/
service_discovery.rs1use simple_dns::{rdata::RData, Name, Packet, Question, CLASS, TYPE};
2use tokio::{
3 net::UdpSocket,
4 select, spawn,
5 sync::{
6 mpsc::{channel, Receiver, Sender},
7 RwLock,
8 },
9 time::{sleep_until, Instant},
10};
11
12use std::{collections::HashSet, net::SocketAddr, sync::Arc, time::Duration};
13
14use crate::{
15 resource_record_manager::{
16 service_discovery_resource_manager, DomainResourceFilter, ResourceRecordManager,
17 },
18 socket_helper::nonblocking,
19 InstanceInformation, NetworkScope, SimpleMdnsError,
20};
21
22pub struct ServiceDiscovery {
42 resource_manager: Arc<RwLock<ResourceRecordManager<'static>>>,
43 service_name: Name<'static>,
44
45 advertise_tx: Sender<(bool, Option<tokio::sync::oneshot::Sender<()>>)>,
46}
47impl ServiceDiscovery {
48 pub fn new(
53 instance_information: InstanceInformation,
54 service_name: &str,
55 resource_ttl: u32,
56 ) -> Result<Self, SimpleMdnsError> {
57 Self::new_with_scope(
58 instance_information,
59 service_name,
60 resource_ttl,
61 None,
62 NetworkScope::V4,
63 )
64 }
65
66 pub fn new_with_scope(
74 instance_information: InstanceInformation,
75 service_name: &str,
76 resource_ttl: u32,
77 on_discovery: Option<tokio::sync::mpsc::Sender<InstanceInformation>>,
78 network_scope: NetworkScope,
79 ) -> Result<Self, SimpleMdnsError> {
80 let instance_full_name = format!(
81 "{}.{service_name}",
82 instance_information.escaped_instance_name()
83 );
84 let instance_full_name = Name::new(&instance_full_name)?.into_owned();
85 let service_name = Name::new(service_name)?.into_owned();
86
87 let resource_manager = service_discovery_resource_manager(
88 &service_name,
89 &instance_full_name,
90 resource_ttl,
91 instance_information,
92 )?;
93
94 let resource_manager = Arc::new(RwLock::new(resource_manager));
95 let service_discovery = ServiceDiscoveryExecutor {
96 instance_name: instance_full_name,
97 service_name: service_name.clone(),
98 resource_manager: resource_manager.clone(),
99 sender_socket: crate::socket_helper::sender_socket(network_scope.is_v4())
100 .and_then(nonblocking)?,
101 network_scope,
102 };
103
104 let (advertise_tx, advertise_rx) = channel(10);
105 spawn(async {
106 if let Err(err) = service_discovery
107 .execution_loop(advertise_rx, on_discovery)
108 .await
109 {
110 log::error!("Service discovery failed {err}");
111 }
112 });
113
114 let announce = advertise_tx.clone();
115 spawn(async move {
116 let _ = announce.send((false, None)).await;
117 tokio::time::sleep(Duration::from_secs(1)).await;
118 let _ = announce.send((false, None)).await;
119 });
120
121 Ok(Self {
122 resource_manager,
123 service_name,
124 advertise_tx,
125 })
126 }
127
128 pub async fn remove_service_from_discovery(&mut self) {
131 if (self.announce(true).await).is_err() {
132 log::error!("Failed to advertise cache flush");
133 };
134 }
135
136 pub async fn announce(&mut self, cache_flush: bool) -> Result<(), SimpleMdnsError> {
143 let (tx, rx) = tokio::sync::oneshot::channel();
144 self.advertise_tx
145 .send((cache_flush, Some(tx)))
146 .await
147 .map_err(|_| SimpleMdnsError::ServiceDiscoveryStopped)?;
148
149 rx.await
150 .map_err(|_| SimpleMdnsError::ServiceDiscoveryStopped)
151 }
152
153 pub async fn get_known_services(&self) -> HashSet<InstanceInformation> {
155 self.resource_manager
156 .read()
157 .await
158 .get_domain_resources(&self.service_name, DomainResourceFilter::cached())
159 .filter_map(|domain_resources| {
160 InstanceInformation::from_records(&self.service_name, domain_resources)
161 })
162 .collect()
163 }
164}
165
166struct ServiceDiscoveryExecutor {
167 instance_name: Name<'static>,
168 service_name: Name<'static>,
169 resource_manager: Arc<RwLock<ResourceRecordManager<'static>>>,
170 sender_socket: UdpSocket,
171 network_scope: NetworkScope,
172}
173
174impl ServiceDiscoveryExecutor {
175 async fn execution_loop(
176 self,
177 mut advertise: Receiver<(bool, Option<tokio::sync::oneshot::Sender<()>>)>,
178 mut on_discovery: Option<tokio::sync::mpsc::Sender<InstanceInformation>>,
179 ) -> Result<(), SimpleMdnsError> {
180 let recv_socket =
181 crate::socket_helper::join_multicast(self.network_scope).and_then(nonblocking)?;
182
183 let mut recv_buffer = [0u8; 9000];
184 let mut next_expiration = Instant::now() + Duration::from_secs(5);
185
186 self.query_service_instances().await?;
187
188 loop {
189 select! {
190 packet = recv_socket.recv_from(&mut recv_buffer) => {
191 let (count, addr) = packet?;
192 if let Err(err) = self.process_packet(&recv_buffer[..count], addr, &mut on_discovery).await {
193 log::error!("Failed to process received packet {err}");
194 }
195 }
196 _ = sleep_until(next_expiration) => {
197 if let Ok(new_expiration) = self.refresh_known_instances().await {
198 next_expiration = new_expiration;
199 }
200 }
201 cache_flush = advertise.recv() => {
202 match cache_flush {
203 Some((cache_flush, notify)) => {
204 match self.advertise_service(cache_flush).await {
205 Err(err) => log::error!("Failed to advertise service {err}"),
206 Ok(()) => {
207 if cache_flush {
208 self.resource_manager.write().await.remove_domain_resources(&self.instance_name);
209 }
210 if let Some(notify) = notify {
211 let _ = notify.send(());
212 }
213 }
214 }
215 }
216 None => {
217 break Ok(())
218 }
219 }
220 }
221 };
222 }
223 }
224
225 async fn refresh_known_instances(&self) -> std::io::Result<Instant> {
226 log::info!("Refreshing known services");
227 let now = Instant::now();
228 let next_expiration = self
229 .resource_manager
230 .read()
231 .await
232 .get_next_refresh()
233 .map(Instant::from_std);
234
235 log::trace!("next expiration: {:?}", next_expiration);
236 if let Some(expiration) = next_expiration {
237 if expiration <= now {
238 if let Err(err) = self.query_service_instances().await {
239 log::error!("There was an error querying service instances. {err}");
240 }
241 } else {
242 return Ok(expiration);
243 }
244 }
245
246 Ok(now + Duration::from_secs(5))
247 }
248
249 async fn process_packet(
250 &self,
251 buf: &[u8],
252 origin_addr: SocketAddr,
253 on_discovery: &mut Option<tokio::sync::mpsc::Sender<InstanceInformation>>,
254 ) -> Result<(), SimpleMdnsError> {
255 let packet = Packet::parse(buf)?;
256 if packet.has_flags(simple_dns::PacketFlag::RESPONSE) {
257 log::trace!("received response packet {}", packet.id());
258 add_response_to_resources(
259 packet,
260 &self.service_name,
261 &self.instance_name,
262 &mut *self.resource_manager.write().await,
263 on_discovery,
264 )
265 .await;
266 } else {
267 match crate::build_reply(packet, &*self.resource_manager.read().await) {
268 Some((reply_packet, unicast_response)) => {
269 let reply = reply_packet.build_bytes_vec_compressed()?;
270
271 let reply_addr = if unicast_response {
272 origin_addr
273 } else {
274 self.network_scope.socket_address()
275 };
276
277 self.sender_socket.send_to(&reply, &reply_addr).await?;
278 }
279 None => {
280 log::debug!("No reply to send");
281 }
282 }
283 }
284
285 Ok(())
286 }
287
288 async fn advertise_service(&self, cache_flush: bool) -> Result<(), SimpleMdnsError> {
289 log::info!("Advertising service");
290 let mut packet = Packet::new_reply(1);
291 let resource_manager = self.resource_manager.read().await;
292 let mut additional_records = HashSet::new();
293
294 for d_resources in resource_manager.get_domain_resources(
296 &self.service_name,
297 DomainResourceFilter::authoritative(true),
298 ) {
299 if cache_flush {
300 d_resources
301 .filter(|r| r.match_qclass(CLASS::IN.into()))
302 .for_each(|r| packet.answers.push(r.to_cache_flush_record()));
303 } else {
304 d_resources.cloned().for_each(|resource| {
305 if let RData::SRV(srv) = &resource.rdata {
306 let target = resource_manager
307 .get_domain_resources(
308 &srv.target,
309 DomainResourceFilter::authoritative(false),
310 )
311 .flatten()
312 .filter(|r| {
313 (r.match_qtype(TYPE::A.into()) || r.match_qtype(TYPE::AAAA.into()))
314 && r.match_qclass(CLASS::IN.into())
315 })
316 .cloned();
317
318 additional_records.extend(target);
319 }
320
321 packet.answers.push(resource);
322 });
323 };
324 }
325
326 for additional_record in additional_records {
327 packet.additional_records.push(additional_record)
328 }
329
330 if packet.answers.is_empty() {
331 log::info!("Failed to advertise service, no answers to send");
332 return Ok(());
333 }
334
335 let bytes = packet.build_bytes_vec_compressed()?;
336 self.sender_socket
337 .send_to(&bytes, &self.network_scope.socket_address())
338 .await?;
339
340 Ok(())
341 }
342
343 async fn query_service_instances(&self) -> Result<(), SimpleMdnsError> {
344 log::trace!("probing service instances");
345 let mut packet = Packet::new_query(0);
346 packet.questions.push(Question::new(
348 self.service_name.clone(),
349 TYPE::PTR.into(),
350 CLASS::IN.into(),
351 false,
352 ));
353
354 self.sender_socket
355 .send_to(
356 &packet.build_bytes_vec_compressed()?,
357 &self.network_scope.socket_address(),
358 )
359 .await?;
360
361 Ok(())
362 }
363}
364
365async fn add_response_to_resources(
366 packet: Packet<'_>,
367 service_name: &Name<'_>,
368 full_name: &Name<'_>,
369 owned_resources: &mut ResourceRecordManager<'static>,
370 on_discovery: &mut Option<tokio::sync::mpsc::Sender<InstanceInformation>>,
371) {
372 let resources = packet
373 .answers
374 .into_iter()
375 .chain(packet.additional_records)
376 .filter(|aw| aw.name.ne(full_name) && aw.name.is_subdomain_of(service_name))
377 .map(|r| r.into_owned());
378
379 if let Some(channel) = on_discovery {
380 let resources: Vec<_> = resources.collect();
381 if resources.is_empty() {
382 return;
383 }
384
385 let mut instance_name: Option<String> = Default::default();
386 let instance_information = InstanceInformation::from_records(
387 service_name,
388 resources.iter().inspect(|record| {
389 if instance_name.is_none() {
390 instance_name = record
391 .name
392 .without(service_name)
393 .map(|sub_domain| sub_domain.to_string());
394 }
395 }),
396 );
397
398 if let Some(instance_information) = instance_information {
399 if channel.send(instance_information).await.is_err() {
400 *on_discovery = None
401 }
402 }
403
404 for resource in resources {
405 owned_resources.add_cached_resource(resource);
406 }
407 } else {
408 for resource in resources {
409 owned_resources.add_cached_resource(resource);
410 }
411 }
412}