Skip to main content

simple_mdns/async_discovery/
service_discovery.rs

1use 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
22/// Service Discovery implementation using DNS-SD.
23/// This implementation advertise all the registered addresses, query for the same service on the same network and
24/// keeps a cache of known service instances
25///
26/// Notice that this crate does not provide any means of finding your own ip address. There are crates that provide this kind of feature.
27///
28/// ## Example
29/// ```no_run
30/// use simple_mdns::async_discovery::ServiceDiscovery;
31/// use simple_mdns::InstanceInformation;
32/// use std::str::FromStr;
33///
34/// let mut discovery = ServiceDiscovery::new(
35///     InstanceInformation::new("a".into()).with_socket_address("192.168.1.22:8090".parse().expect("Invalid Socket Address")),
36///     "_mysrv._tcp.local",
37///     60
38/// ).expect("Failed to create service discovery");
39///
40/// ```
41pub 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    /// Creates a new ServiceDiscovery by providing `instance_information`, `service_name`, `resource ttl`. The service will be created using IPV4 scope with UNSPECIFIED Interface
49    ///
50    /// `service_name` must be in the standard specified by the mdns RFC, example: **_my_service._tcp.local**
51    /// `resource_ttl` refers to the amount of time in seconds your service will be cached in the dns responder.
52    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    /// Creates a new ServiceDiscovery by providing `instance_information`, `service_name`, `resource ttl`, `on_disovery` and `network_scope`
67    ///
68    /// `service_name` must be in the standard specified by the mdns RFC, example: **_my_service._tcp.local**
69    /// `resource_ttl` refers to the amount of time in seconds your service will be cached in the dns responder.
70    /// `on_discovery` channel, if provided, will receive every instance information when
71    /// discovered
72    /// `network_scope` to be used
73    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    /// Remove service from discovery by announcing with a cache flush and
129    /// removing all the internal resource records
130    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    /// Announce the service by sending a packet with all the resource records in the answers
137    /// section. It is not necessary to call this method manually, it will be called automatically
138    /// when the instance is added to the discovery.
139    ///
140    /// if `cache_flush` is true, then the resources will have the cache flush flag set, this will
141    /// cause them to be removed from any cache that receives the packet.
142    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    /// Return the [`InstanceInformation`] of all known services
154    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        // FIXME: include only the resources with appropriate network scope
295        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        // RFC 6763 §4 — discover service instances via PTR query on the service type name
347        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}