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}