libmdns/
lib.rs

1#![deny(clippy::all)]
2#![forbid(unsafe_code)]
3#![warn(clippy::pedantic)]
4#![warn(rust_2018_idioms)]
5#![warn(rust_2021_compatibility)]
6#![warn(rust_2024_compatibility)]
7#![warn(future_incompatible)]
8
9use futures_util::{future, future::FutureExt};
10use log::warn;
11use std::cell::RefCell;
12use std::future::Future;
13use std::io;
14use std::marker::Unpin;
15use std::net::IpAddr;
16use std::sync::{Arc, RwLock};
17
18use std::thread;
19use tokio::{runtime::Handle, sync::mpsc};
20
21mod dns_parser;
22use crate::dns_parser::Name;
23
24mod address_family;
25mod fsm;
26mod services;
27
28use crate::address_family::{Inet, Inet6};
29use crate::fsm::{Command, FSM};
30use crate::services::{ServiceData, Services, ServicesInner};
31
32/// The default TTL for announced mDNS Services.
33pub const DEFAULT_TTL: u32 = 60;
34const MDNS_PORT: u16 = 5353;
35
36pub struct Responder {
37    services: Services,
38    commands: RefCell<CommandSender>,
39    shutdown: Arc<Shutdown>,
40}
41
42pub struct Service {
43    id: usize,
44    services: Services,
45    commands: CommandSender,
46    _shutdown: Arc<Shutdown>,
47}
48
49type ResponderTask = Box<dyn Future<Output = ()> + Send + Unpin>;
50
51impl Responder {
52    /// Spawn a `Responder` task on an new os thread.
53    ///
54    /// # Panics
55    ///
56    /// If the tokio runtime cannot be created this will panic.
57    #[must_use]
58    pub fn new() -> Responder {
59        Self::new_with_ip_list(Vec::new()).unwrap()
60    }
61
62    /// Spawn a `Responder` task on an new os thread.
63    /// DNS response records will have the reported IPs limited to those passed in here.
64    /// This can be particularly useful on machines with lots of networks created by tools such as
65    /// Docker.
66    ///
67    /// # Errors
68    ///
69    /// If the hostname cannot be converted to a valid unicode string, this will return an error.
70    ///
71    /// # Panics
72    ///
73    /// If the tokio runtime cannot be created this will panic.
74    pub fn new_with_ip_list(allowed_ips: Vec<IpAddr>) -> io::Result<Responder> {
75        let (tx, rx) = std::sync::mpsc::sync_channel(0);
76        thread::Builder::new()
77            .name("mdns-responder".to_owned())
78            .spawn(move || {
79                let rt = tokio::runtime::Builder::new_current_thread()
80                    .enable_all()
81                    .build()
82                    .unwrap();
83                rt.block_on(async {
84                    match Self::with_default_handle_and_ip_list(allowed_ips) {
85                        Ok((responder, task)) => {
86                            tx.send(Ok(responder)).expect("tx responder channel closed");
87                            task.await;
88                        }
89                        Err(e) => tx.send(Err(e)).expect("tx responder channel closed"),
90                    }
91                });
92            })?;
93        rx.recv().expect("rx responder channel closed")
94    }
95
96    /// Spawn a `Responder` with the provided tokio `Handle`.
97    ///
98    /// # Example
99    /// ```no_run
100    /// use libmdns::Responder;
101    ///
102    /// # use std::io;
103    /// # fn main() -> io::Result<()> {
104    /// let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
105    /// let handle = rt.handle().clone();
106    /// let responder = Responder::spawn(&handle)?;
107    /// # Ok(())
108    /// # }
109    /// ```
110    ///
111    /// # Errors
112    ///
113    /// If the hostname cannot be converted to a valid unicode string, this will return an error.
114    pub fn spawn(handle: &Handle) -> io::Result<Responder> {
115        Self::spawn_with_ip_list(handle, Vec::new())
116    }
117
118    /// Spawn a `Responder` task  with the provided tokio `Handle`.
119    /// DNS response records will have the reported IPs limited to those passed in here.
120    /// This can be particularly useful on machines with lots of networks created by tools such as docker.
121    ///
122    /// # Example
123    /// ```no_run
124    /// use libmdns::Responder;
125    ///
126    /// # use std::io;
127    /// # fn main() -> io::Result<()> {
128    /// let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
129    /// let handle = rt.handle().clone();
130    /// let vec: Vec<std::net::IpAddr> = vec![
131    ///     "192.168.1.10".parse::<std::net::Ipv4Addr>().unwrap().into(),
132    ///     std::net::Ipv6Addr::new(0, 0, 0, 0xfe80, 0x1ff, 0xfe23, 0x4567, 0x890a).into(),
133    /// ];
134    /// let responder = Responder::spawn_with_ip_list(&handle, vec)?;
135    /// # Ok(())
136    /// # }
137    /// ```
138    ///
139    /// # Errors
140    ///
141    /// If the hostname cannot be converted to a valid unicode string, this will return an error.
142    pub fn spawn_with_ip_list(handle: &Handle, allowed_ips: Vec<IpAddr>) -> io::Result<Responder> {
143        let (responder, task) = Self::with_default_handle_and_ip_list(allowed_ips)?;
144        handle.spawn(task);
145        Ok(responder)
146    }
147
148    /// Spawn a `Responder` task  with the provided tokio `Handle`.
149    /// DNS response records will have the reported IPs limited to those passed in here.
150    /// This can be particularly useful on machines with lots of networks created by tools such as
151    /// Docker.
152    /// And SRV field will have specified hostname instead of system hostname.
153    /// This can be particularly useful if the platform has the fixed hostname and the application
154    /// should make hostname unique for its purpose.
155    ///
156    /// # Example
157    /// ```no_run
158    /// use libmdns::Responder;
159    ///
160    /// # use std::io;
161    /// # fn main() -> io::Result<()> {
162    /// let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
163    /// let handle = rt.handle().clone();
164    /// let responder = Responder::spawn_with_ip_list_and_hostname(&handle, Vec::new(), "myUniqueName".to_owned())?;
165    /// # Ok(())
166    /// # }
167    /// ```
168    ///
169    /// # Errors
170    ///
171    /// If the hostname cannot be converted to a valid unicode string, this will return an error.
172    pub fn spawn_with_ip_list_and_hostname(
173        handle: &Handle,
174        allowed_ips: Vec<IpAddr>,
175        hostname: String,
176    ) -> io::Result<Responder> {
177        let (responder, task) =
178            Self::with_default_handle_and_ip_list_and_hostname(allowed_ips, hostname)?;
179        handle.spawn(task);
180        Ok(responder)
181    }
182
183    /// Spawn a `Responder` on the default tokio handle.
184    ///
185    /// # Errors
186    ///
187    /// If the hostname cannot be converted to a valid unicode string, this will return an error.
188    pub fn with_default_handle() -> io::Result<(Responder, ResponderTask)> {
189        Self::with_default_handle_and_ip_list(Vec::new())
190    }
191
192    /// Spawn a `Responder` on the default tokio handle.
193    /// DNS response records will have the reported IPs limited to those passed in here.
194    /// This can be particularly useful on machines with lots of networks created by tools such as
195    /// Docker.
196    ///
197    /// # Errors
198    ///
199    /// If the hostname cannot be converted to a valid unicode string, this will return an error.
200    pub fn with_default_handle_and_ip_list(
201        allowed_ips: Vec<IpAddr>,
202    ) -> io::Result<(Responder, ResponderTask)> {
203        let hostname = hostname::get()?.into_string().map_err(|_| {
204            io::Error::new(io::ErrorKind::InvalidData, "Hostname not valid unicode")
205        })?;
206        Self::default_handle(allowed_ips, hostname)
207    }
208
209    /// Spawn a `Responder` on the default tokio handle.
210    /// DNS response records will have the reported IPs limited to those passed in here.
211    /// This can be particularly useful on machines with lots of networks created by tools such as
212    /// Docker.
213    /// And SRV field will have specified hostname instead of system hostname.
214    /// This can be particularly useful if the platform has the fixed hostname and the application
215    /// should make hostname unique for its purpose.
216    ///
217    /// # Errors
218    ///
219    /// If the hostname cannot be converted to a valid unicode string, this will return an error.
220    pub fn with_default_handle_and_ip_list_and_hostname(
221        allowed_ips: Vec<IpAddr>,
222        hostname: String,
223    ) -> io::Result<(Responder, ResponderTask)> {
224        Self::default_handle(allowed_ips, hostname)
225    }
226
227    fn default_handle(
228        allowed_ips: Vec<IpAddr>,
229        mut hostname: String,
230    ) -> io::Result<(Responder, ResponderTask)> {
231        #[allow(clippy::case_sensitive_file_extension_comparisons)]
232        if !hostname.ends_with(".local") {
233            hostname.push_str(".local");
234        }
235
236        let services = Arc::new(RwLock::new(ServicesInner::new(hostname)));
237
238        let v4 = FSM::<Inet>::new(&services, allowed_ips.clone());
239        let v6 = FSM::<Inet6>::new(&services, allowed_ips);
240
241        let (task, commands): (ResponderTask, _) = match (v4, v6) {
242            (Ok((v4_task, v4_command)), Ok((v6_task, v6_command))) => {
243                let tasks = future::join(v4_task, v6_task).map(|((), ())| ());
244                (Box::new(tasks), vec![v4_command, v6_command])
245            }
246
247            (Ok((v4_task, v4_command)), Err(err)) => {
248                warn!("Failed to register IPv6 receiver: {err:?}");
249                (Box::new(v4_task), vec![v4_command])
250            }
251
252            (Err(err), _) => return Err(err),
253        };
254
255        let commands = CommandSender(commands);
256        let responder = Responder {
257            services,
258            commands: RefCell::new(commands.clone()),
259            shutdown: Arc::new(Shutdown(commands)),
260        };
261
262        Ok((responder, task))
263    }
264}
265
266impl Responder {
267    /// Register a service to be advertised by the Responder with the [`DEFAULT_TTL`]. The service is unregistered on
268    /// drop.
269    ///
270    /// # Example
271    ///
272    /// ```no_run
273    /// use libmdns::Responder;
274    ///
275    /// # use std::io;
276    /// # fn main() -> io::Result<()> {
277    /// let responder = Responder::new();
278    /// // bind service
279    /// let _http_svc = responder.register(
280    ///          "_http._tcp".into(),
281    ///          "my http server".into(),
282    ///          80,
283    ///          &["path=/"]
284    ///      );
285    /// # Ok(())
286    /// # }
287    /// ```
288    ///
289    /// # Panics
290    ///
291    /// If the TXT records are longer than 255 bytes, this will panic.
292    #[must_use]
293    pub fn register(&self, svc_type: &str, svc_name: &str, port: u16, txt: &[&str]) -> Service {
294        self.register_with_ttl(svc_type, svc_name, port, txt, DEFAULT_TTL)
295    }
296
297    /// Register a service to be advertised by the Responder. With a custom TTL in seconds. The service is unregistered on
298    /// drop.
299    ///
300    /// You may prefer to use this over [`Responder::register`] if you know your service will be short-lived and want clients to respond
301    /// to it dissapearing more quickly (lower TTL), or if you find your service is very infrequently down and want to reduce
302    /// network traffic (higher TTL).
303    ///
304    /// This becomes more important whilst waiting for <https://github.com/librespot-org/libmdns/issues/27> to be resolved.
305    ///
306    /// # example
307    ///
308    /// ```no_run
309    /// use libmdns::Responder;
310    ///
311    /// # use std::io;
312    /// # fn main() -> io::Result<()> {
313    /// let responder = Responder::new();
314    /// // bind service
315    /// let _http_svc = responder.register_with_ttl(
316    ///          "_http._tcp".into(),
317    ///          "my really unreliable and short-lived http server".into(),
318    ///          80,
319    ///          &["path=/"],
320    ///          10 // mDNS clients are requested to re-check every 10 seconds for this HTTP server
321    ///      );
322    /// # Ok(())
323    /// # }
324    /// ```
325    ///
326    /// # Panics
327    ///
328    /// If the TXT records are longer than 255 bytes, this will panic.
329    #[must_use]
330    pub fn register_with_ttl(
331        &self,
332        svc_type: &str,
333        svc_name: &str,
334        port: u16,
335        txt: &[&str],
336        ttl: u32,
337    ) -> Service {
338        let txt = if txt.is_empty() {
339            vec![0]
340        } else {
341            txt.iter()
342                .flat_map(|entry| {
343                    let entry = entry.as_bytes();
344                    assert!(
345                        (entry.len() <= 255),
346                        "{:?} is too long for a TXT record",
347                        entry
348                    );
349                    #[allow(clippy::cast_possible_truncation)]
350                    std::iter::once(entry.len() as u8).chain(entry.iter().copied())
351                })
352                .collect()
353        };
354
355        let svc = ServiceData {
356            typ: Name::from_str(format!("{svc_type}.local")),
357            name: Name::from_str(format!("{svc_name}.{svc_type}.local")),
358            port,
359            txt,
360        };
361
362        self.commands
363            .borrow_mut()
364            .send_unsolicited(svc.clone(), ttl, true);
365
366        let id = self.services.write().unwrap().register(svc);
367
368        Service {
369            id,
370            commands: self.commands.borrow().clone(),
371            services: self.services.clone(),
372            _shutdown: self.shutdown.clone(),
373        }
374    }
375}
376
377impl Default for Responder {
378    fn default() -> Self {
379        Responder::new()
380    }
381}
382
383impl Drop for Service {
384    fn drop(&mut self) {
385        let svc = self.services.write().unwrap().unregister(self.id);
386        self.commands.send_unsolicited(svc, 0, false);
387    }
388}
389
390struct Shutdown(CommandSender);
391
392impl Drop for Shutdown {
393    fn drop(&mut self) {
394        self.0.send_shutdown();
395        // TODO wait for tasks to shutdown
396    }
397}
398
399#[derive(Clone)]
400struct CommandSender(Vec<mpsc::UnboundedSender<Command>>);
401impl CommandSender {
402    #[allow(clippy::needless_pass_by_value)]
403    fn send(&mut self, cmd: Command) {
404        for tx in &mut self.0 {
405            tx.send(cmd.clone()).expect("responder died");
406        }
407    }
408
409    fn send_unsolicited(&mut self, svc: ServiceData, ttl: u32, include_ip: bool) {
410        self.send(Command::SendUnsolicited {
411            svc,
412            ttl,
413            include_ip,
414        });
415    }
416
417    fn send_shutdown(&mut self) {
418        self.send(Command::Shutdown);
419    }
420}