hightower_mdns/
lib.rs

1mod broadcast;
2mod constants;
3mod packet;
4mod query;
5mod socket;
6
7use std::io;
8use std::net::Ipv4Addr;
9use std::sync::Arc;
10use std::time::Duration;
11use socket2::Socket;
12use tokio::sync::mpsc;
13
14use socket::{create_send_socket, create_recv_socket};
15
16/// Response from an mDNS query or discovery
17#[derive(Debug, Clone, PartialEq)]
18pub struct MdnsResponse {
19    /// The hostname (e.g., "myhost.local")
20    pub hostname: String,
21    /// The IPv4 address
22    pub ip: Ipv4Addr,
23}
24
25/// Handle for interacting with a running mDNS service
26pub struct MdnsHandle {
27    send_socket: Arc<Socket>,
28    domain: String,
29    /// Channel for receiving discovered host announcements
30    pub discoveries: mpsc::Receiver<MdnsResponse>,
31    /// Channel for receiving query responses
32    pub responses: mpsc::Receiver<MdnsResponse>,
33}
34
35impl MdnsHandle {
36    /// Send a query for a specific hostname
37    ///
38    /// # Arguments
39    ///
40    /// * `hostname` - The hostname to query (without domain suffix)
41    pub async fn query(&self, hostname: &str) {
42        query::query(&self.send_socket, hostname, &self.domain).await;
43    }
44}
45
46/// mDNS service for advertising a hostname on the local network
47pub struct Mdns {
48    name: String,
49    domain: String,
50    broadcast_interval: Duration,
51    send_socket: Arc<Socket>,
52    recv_socket: Arc<Socket>,
53    local_ip: Ipv4Addr,
54}
55
56impl Mdns {
57    /// Create a new mDNS instance with the given name and IP address
58    ///
59    /// # Arguments
60    ///
61    /// * `name` - The mDNS name to advertise (without .local suffix)
62    /// * `ip` - The local IPv4 address to advertise
63    ///
64    /// # Example
65    ///
66    /// ```no_run
67    /// use hightower_mdns::Mdns;
68    /// use std::net::Ipv4Addr;
69    ///
70    /// let mdns = Mdns::new("myhost", Ipv4Addr::new(192, 168, 1, 100)).unwrap();
71    /// ```
72    pub fn new<S: Into<String>>(name: S, ip: Ipv4Addr) -> io::Result<Self> {
73        Self::with_interval(name, ip, Duration::from_secs(120))
74    }
75
76    /// Create a new mDNS instance with a custom broadcast interval
77    ///
78    /// # Arguments
79    ///
80    /// * `name` - The mDNS name to advertise (without domain suffix)
81    /// * `ip` - The local IPv4 address to advertise
82    /// * `interval` - Time between broadcasts (default is 120 seconds per RFC 6762)
83    pub fn with_interval<S: Into<String>>(name: S, ip: Ipv4Addr, interval: Duration) -> io::Result<Self> {
84        let send_socket = create_send_socket()?;
85        let recv_socket = create_recv_socket()?;
86
87        Ok(Self {
88            name: name.into(),
89            domain: "local".to_string(),
90            broadcast_interval: interval,
91            send_socket: Arc::new(send_socket),
92            recv_socket: Arc::new(recv_socket),
93            local_ip: ip,
94        })
95    }
96
97    /// Set a custom domain (default is "local")
98    ///
99    /// # Arguments
100    ///
101    /// * `domain` - The domain to use (without leading dot)
102    pub fn with_domain<S: Into<String>>(mut self, domain: S) -> Self {
103        self.domain = domain.into();
104        self
105    }
106
107    /// Get the name being advertised
108    pub fn name(&self) -> &str {
109        &self.name
110    }
111
112    /// Get the broadcast interval
113    pub fn broadcast_interval(&self) -> Duration {
114        self.broadcast_interval
115    }
116
117    /// Start the mDNS broadcast and listen loops
118    ///
119    /// This will continuously broadcast the mDNS name at the configured interval
120    /// and listen for queries from other peers.
121    /// Returns an MdnsHandle for querying and receiving discovery notifications.
122    pub fn run(self) -> MdnsHandle {
123        let (disc_tx, disc_rx) = mpsc::channel(100);
124        let (resp_tx, resp_rx) = mpsc::channel(100);
125
126        let send_socket = self.send_socket.clone();
127        let recv_socket = self.recv_socket.clone();
128        let name = Arc::new(self.name);
129        let domain = Arc::new(self.domain.clone());
130        let local_ip = self.local_ip;
131        let broadcast_interval = self.broadcast_interval;
132
133        // Spawn broadcast task
134        let send_socket_clone = send_socket.clone();
135        let name_clone = name.clone();
136        let domain_clone = domain.clone();
137        tokio::spawn(async move {
138            broadcast::broadcast_loop(&send_socket_clone, &name_clone, &domain_clone, local_ip, broadcast_interval).await;
139        });
140
141        // Spawn listen task
142        let send_socket_clone = send_socket.clone();
143        let name_clone = name.clone();
144        let domain_clone = domain.clone();
145        tokio::spawn(async move {
146            query::listen(&recv_socket, &send_socket_clone, &name_clone, &domain_clone, local_ip, disc_tx, resp_tx).await;
147        });
148
149        MdnsHandle {
150            send_socket,
151            domain: self.domain,
152            discoveries: disc_rx,
153            responses: resp_rx,
154        }
155    }
156
157    /// Get the local IP address being advertised
158    pub fn local_ip(&self) -> Ipv4Addr {
159        self.local_ip
160    }
161
162    /// Send a goodbye packet to notify others this host is leaving
163    ///
164    /// This sends a packet with TTL=0 to tell other hosts to remove this
165    /// hostname from their cache.
166    pub async fn goodbye(&self) {
167        broadcast::send_goodbye(&self.send_socket, &self.name, &self.domain, self.local_ip).await;
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_new_with_name() {
177        let ip = Ipv4Addr::new(192, 168, 1, 100);
178        let mdns = Mdns::new("myhost", ip).unwrap();
179        assert_eq!(mdns.name(), "myhost");
180        assert_eq!(mdns.local_ip(), ip);
181        assert_eq!(mdns.broadcast_interval(), Duration::from_secs(120));
182    }
183
184    #[test]
185    fn test_with_custom_interval() {
186        let ip = Ipv4Addr::new(10, 0, 0, 5);
187        let mdns = Mdns::with_interval("myhost", ip, Duration::from_secs(60)).unwrap();
188        assert_eq!(mdns.name(), "myhost");
189        assert_eq!(mdns.local_ip(), ip);
190        assert_eq!(mdns.broadcast_interval(), Duration::from_secs(60));
191    }
192}