pot_o_extensions/mdns_discovery.rs
1//! mDNS (Multicast DNS) service discovery for PoT-O validators.
2//!
3//! Enables validators on the same local network to automatically discover each other
4//! without requiring a Bootstrap Registry. Validators register themselves as mDNS services
5//! and can discover peers via mDNS queries.
6
7use flume::RecvTimeoutError;
8use mdns_sd::{ServiceDaemon, ServiceInfo};
9use std::net::IpAddr;
10use std::sync::{Arc, Mutex};
11use std::time::Duration;
12
13// ---------------------------------------------------------------------------
14// Types
15// ---------------------------------------------------------------------------
16
17/// Information about a discovered peer validator
18#[derive(Clone, Debug)]
19pub struct PeerDiscovery {
20 /// Unique identifier for the validator node
21 pub node_id: String,
22 /// Hostname (FQDN) of the validator
23 pub hostname: String,
24 /// IP address of the validator
25 pub ip: IpAddr,
26 /// Port on which the validator is listening
27 pub port: u16,
28}
29
30/// mDNS service discovery for validator peer discovery
31pub struct MdnsDiscovery {
32 /// Unique identifier for this validator
33 node_id: String,
34 /// Port on which this validator listens
35 port: u16,
36 /// mDNS service type for PoT-O validators
37 service_type: String,
38 /// mDNS daemon for service registration and discovery
39 daemon: Arc<Mutex<Option<ServiceDaemon>>>,
40}
41
42// ---------------------------------------------------------------------------
43// Implementation
44// ---------------------------------------------------------------------------
45
46impl MdnsDiscovery {
47 /// Service type string for PoT-O validators following mDNS conventions
48 const SERVICE_TYPE: &'static str = "_pot-o-validator._tcp.local.";
49
50 /// Create a new mDNS discovery instance and start the daemon.
51 ///
52 /// # Arguments
53 /// * `node_id` - Unique identifier for this validator
54 /// * `port` - Port on which this validator listens
55 ///
56 /// # Returns
57 /// Result with MdnsDiscovery instance or error if daemon fails to start
58 pub fn new(node_id: &str, port: u16) -> Result<Self, Box<dyn std::error::Error>> {
59 // Create and start the mDNS daemon
60 let daemon =
61 ServiceDaemon::new().map_err(|e| format!("Failed to create mDNS daemon: {}", e))?;
62
63 Ok(Self {
64 node_id: node_id.to_string(),
65 port,
66 service_type: Self::SERVICE_TYPE.to_string(),
67 daemon: Arc::new(Mutex::new(Some(daemon))),
68 })
69 }
70
71 /// Get the node ID of this validator
72 pub fn node_id(&self) -> &str {
73 &self.node_id
74 }
75
76 /// Get the port of this validator
77 pub fn port(&self) -> u16 {
78 self.port
79 }
80
81 /// Get the mDNS service type
82 pub fn service_type(&self) -> &str {
83 &self.service_type
84 }
85
86 /// Register this validator as an mDNS service.
87 ///
88 /// # Arguments
89 /// * `hostname` - Hostname/FQDN for this validator (e.g., "validator-1.local")
90 ///
91 /// # Returns
92 /// Result indicating success or error
93 pub fn register_service(&self, hostname: &str) -> Result<(), Box<dyn std::error::Error>> {
94 let daemon_lock = self
95 .daemon
96 .lock()
97 .map_err(|e| format!("Failed to lock daemon: {}", e))?;
98
99 let daemon = daemon_lock.as_ref().ok_or("mDNS daemon not initialized")?;
100
101 // Create service info with validator details
102 let service_info = ServiceInfo::new(
103 Self::SERVICE_TYPE,
104 &format!("{}._pot-o-validator._tcp.local.", self.node_id),
105 &self.node_id,
106 hostname,
107 self.port,
108 None,
109 )
110 .map_err(|e| format!("Failed to create ServiceInfo: {}", e))?;
111
112 // Register the service with the daemon
113 daemon
114 .register(service_info)
115 .map_err(|e| format!("Failed to register service: {}", e))?;
116
117 Ok(())
118 }
119
120 /// Discover other validators on the local network via mDNS.
121 ///
122 /// # Arguments
123 /// * `timeout_secs` - How long to wait for discovery responses
124 ///
125 /// # Returns
126 /// Result with vector of discovered PeerDiscovery instances
127 pub fn discover_peers(
128 &self,
129 timeout_secs: u64,
130 ) -> Result<Vec<PeerDiscovery>, Box<dyn std::error::Error>> {
131 let daemon_lock = self
132 .daemon
133 .lock()
134 .map_err(|e| format!("Failed to lock daemon: {}", e))?;
135
136 let daemon = daemon_lock.as_ref().ok_or("mDNS daemon not initialized")?;
137
138 // Browse for all PoT-O validator services
139 let receiver = daemon
140 .browse(Self::SERVICE_TYPE)
141 .map_err(|e| format!("Failed to browse services: {}", e))?;
142
143 let mut peers = Vec::new();
144 let deadline = std::time::Instant::now() + Duration::from_secs(timeout_secs);
145
146 // Collect discovery events until timeout
147 loop {
148 let remaining = deadline.saturating_duration_since(std::time::Instant::now());
149 if remaining.is_zero() {
150 break;
151 }
152
153 match receiver.recv_timeout(remaining) {
154 Ok(mdns_sd::ServiceEvent::ServiceResolved(info)) => {
155 // Extract peer information from resolved service
156 if let Some(peer) = self.extract_peer_from_service(&info) {
157 // Avoid adding ourselves to the peer list
158 if peer.node_id != self.node_id {
159 peers.push(peer);
160 }
161 }
162 }
163 Ok(mdns_sd::ServiceEvent::ServiceRemoved(_, _)) => {
164 // Service was removed, could filter from peers if we track them
165 }
166 Ok(mdns_sd::ServiceEvent::SearchStarted(_)) => {
167 // Search has started, ignore
168 }
169 Ok(mdns_sd::ServiceEvent::ServiceFound(_, _)) => {
170 // Service found but not yet resolved, ignore (we'll get ServiceResolved)
171 }
172 Ok(mdns_sd::ServiceEvent::SearchStopped(_)) => {
173 // Search stopped, could exit discovery here
174 break;
175 }
176 Err(RecvTimeoutError::Timeout) => {
177 break; // Timeout reached, exit discovery
178 }
179 Err(_) => {
180 // Other receive errors, exit discovery
181 break;
182 }
183 }
184 }
185
186 Ok(peers)
187 }
188
189 /// Unregister this validator from mDNS (cleanup).
190 ///
191 /// # Returns
192 /// Result indicating success or error
193 pub fn unregister_service(&self) -> Result<(), Box<dyn std::error::Error>> {
194 let daemon_lock = self
195 .daemon
196 .lock()
197 .map_err(|e| format!("Failed to lock daemon: {}", e))?;
198
199 let daemon = daemon_lock.as_ref().ok_or("mDNS daemon not initialized")?;
200
201 let service_name = format!("{}._pot-o-validator._tcp.local.", self.node_id);
202 daemon
203 .unregister(&service_name)
204 .map_err(|e| format!("Failed to unregister service: {}", e))?;
205
206 Ok(())
207 }
208
209 /// Extract peer discovery information from an mDNS ServiceInfo.
210 fn extract_peer_from_service(&self, info: &ServiceInfo) -> Option<PeerDiscovery> {
211 // Get the fullname (e.g., "validator-1._pot-o-validator._tcp.local.")
212 let fullname = info.get_fullname();
213
214 // Extract node_id by removing the service type suffix
215 let node_id = fullname
216 .strip_suffix("._pot-o-validator._tcp.local.")?
217 .to_string();
218
219 // Get hostname
220 let hostname = info.get_hostname().to_string();
221
222 // Get IP address (first available IPv4)
223 let ip = info
224 .get_addresses()
225 .iter()
226 .next()
227 .map(|ipv4| IpAddr::V4(*ipv4))?;
228
229 let port = info.get_port();
230
231 Some(PeerDiscovery {
232 node_id,
233 hostname,
234 ip,
235 port,
236 })
237 }
238}
239
240// Ensure MdnsDiscovery is Send + Sync for use in async contexts
241#[allow(dead_code)]
242fn _assert_send_sync()
243where
244 MdnsDiscovery: Send + Sync,
245{
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn test_service_type_constant() {
254 assert_eq!(MdnsDiscovery::SERVICE_TYPE, "_pot-o-validator._tcp.local.");
255 }
256
257 #[test]
258 fn test_peer_discovery_creation() {
259 let peer = PeerDiscovery {
260 node_id: "test-node".to_string(),
261 hostname: "test-node.local".to_string(),
262 ip: "127.0.0.1".parse().unwrap(),
263 port: 5555,
264 };
265
266 assert_eq!(peer.node_id, "test-node");
267 assert_eq!(peer.port, 5555);
268 }
269}