1use std::collections::HashSet;
7use std::net::{Ipv4Addr, SocketAddrV4};
8
9use async_trait::async_trait;
10use serde::{Deserialize, Serialize};
11use tokio::net::UdpSocket;
12
13use crate::mesh_discovery::{DiscoveredPeer, DiscoveryBackend, DiscoveryError, DiscoverySource};
14
15const MDNS_MULTICAST_ADDR: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 251);
17
18const MDNS_PORT: u16 = 5353;
20
21pub const WEFTOS_SERVICE_NAME: &str = "_weftos._tcp.local";
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
26pub struct MdnsAnnouncement {
27 pub service: String,
29 pub node_id: String,
31 pub address: String,
33 pub port: u16,
35 pub platform: String,
37 pub governance_genesis: String,
39}
40
41pub struct MdnsDiscovery {
46 local_announcement: MdnsAnnouncement,
48 pending: Vec<DiscoveredPeer>,
50 socket: Option<UdpSocket>,
52 active: bool,
54 known: HashSet<String>,
56}
57
58impl MdnsDiscovery {
59 pub fn new(
61 node_id: String,
62 listen_addr: String,
63 port: u16,
64 platform: String,
65 governance_genesis: String,
66 ) -> Self {
67 Self {
68 local_announcement: MdnsAnnouncement {
69 service: WEFTOS_SERVICE_NAME.to_string(),
70 node_id,
71 address: listen_addr,
72 port,
73 platform,
74 governance_genesis,
75 },
76 pending: Vec::new(),
77 socket: None,
78 active: false,
79 known: HashSet::new(),
80 }
81 }
82
83 async fn announce(&self) -> Result<(), DiscoveryError> {
85 if let Some(ref socket) = self.socket {
86 let payload = serde_json::to_vec(&self.local_announcement)
87 .map_err(|e| DiscoveryError::Backend(e.to_string()))?;
88 let dest = SocketAddrV4::new(MDNS_MULTICAST_ADDR, MDNS_PORT);
89 socket
90 .send_to(&payload, dest)
91 .await
92 .map_err(|e| DiscoveryError::Backend(e.to_string()))?;
93 }
94 Ok(())
95 }
96
97 async fn try_receive(&mut self) -> Vec<DiscoveredPeer> {
99 let mut peers = Vec::new();
100 let Some(ref socket) = self.socket else {
101 return peers;
102 };
103
104 let mut buf = [0u8; 4096];
105 if let Ok(Ok((len, _addr))) = tokio::time::timeout(
109 std::time::Duration::from_millis(10),
110 socket.recv_from(&mut buf),
111 )
112 .await
113 && let Ok(ann) = serde_json::from_slice::<MdnsAnnouncement>(&buf[..len])
114 && ann.node_id != self.local_announcement.node_id
115 && !self.known.contains(&ann.node_id)
116 {
117 self.known.insert(ann.node_id.clone());
118 peers.push(DiscoveredPeer {
119 node_id: ann.node_id,
120 address: format!("{}:{}", ann.address, ann.port),
121 platform: ann.platform,
122 source: DiscoverySource::Mdns,
123 });
124 }
125 peers
126 }
127
128 pub fn local_announcement(&self) -> &MdnsAnnouncement {
130 &self.local_announcement
131 }
132}
133
134#[async_trait]
135impl DiscoveryBackend for MdnsDiscovery {
136 fn name(&self) -> &str {
137 "mdns"
138 }
139
140 async fn start(&mut self) -> Result<(), DiscoveryError> {
141 let socket =
143 match UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, MDNS_PORT)).await {
144 Ok(s) => s,
145 Err(_) => {
146 UdpSocket::bind("0.0.0.0:0")
148 .await
149 .map_err(|e| DiscoveryError::Backend(format!("bind failed: {e}")))?
150 }
151 };
152
153 let _ = socket.join_multicast_v4(MDNS_MULTICAST_ADDR, Ipv4Addr::UNSPECIFIED);
155
156 self.socket = Some(socket);
157 self.active = true;
158
159 self.announce().await?;
161 Ok(())
162 }
163
164 async fn poll(&mut self) -> Vec<DiscoveredPeer> {
165 if !self.active {
166 return vec![];
167 }
168 let mut peers = self.try_receive().await;
169 peers.append(&mut self.pending);
170 peers
171 }
172
173 async fn stop(&mut self) -> Result<(), DiscoveryError> {
174 self.active = false;
175 self.socket = None;
176 self.pending.clear();
177 Ok(())
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn mdns_announcement_serde_roundtrip() {
187 let ann = MdnsAnnouncement {
188 service: WEFTOS_SERVICE_NAME.to_string(),
189 node_id: "node-abc".into(),
190 address: "192.168.1.42".into(),
191 port: 9489,
192 platform: "linux".into(),
193 governance_genesis: "deadbeef01234567".into(),
194 };
195 let json = serde_json::to_string(&ann).unwrap();
196 let back: MdnsAnnouncement = serde_json::from_str(&json).unwrap();
197 assert_eq!(ann, back);
198 }
199
200 #[tokio::test]
201 async fn mdns_discovery_creates_and_stops() {
202 let mut disc = MdnsDiscovery::new(
203 "test-node".into(),
204 "127.0.0.1".into(),
205 9489,
206 "linux".into(),
207 "0000000000000000".into(),
208 );
209 assert_eq!(disc.name(), "mdns");
210 assert!(!disc.active);
211
212 disc.start().await.unwrap();
213 assert!(disc.active);
214 assert!(disc.socket.is_some());
215
216 disc.stop().await.unwrap();
217 assert!(!disc.active);
218 assert!(disc.socket.is_none());
219 }
220
221 #[tokio::test]
222 async fn mdns_self_announcement_filtered() {
223 let mut disc = MdnsDiscovery::new(
225 "self-node".into(),
226 "127.0.0.1".into(),
227 9489,
228 "linux".into(),
229 "aabbccdd".into(),
230 );
231 disc.active = true;
232
233 assert_eq!(disc.local_announcement().node_id, "self-node");
237
238 let self_ann = disc.local_announcement.clone();
240 let payload = serde_json::to_vec(&self_ann).unwrap();
241
242 let parsed: MdnsAnnouncement = serde_json::from_slice(&payload).unwrap();
244 assert_eq!(parsed.node_id, "self-node");
245 assert_eq!(parsed.node_id, disc.local_announcement.node_id);
247 }
248
249 #[tokio::test]
250 async fn mdns_duplicate_filtered() {
251 let mut disc = MdnsDiscovery::new(
252 "local".into(),
253 "127.0.0.1".into(),
254 9489,
255 "linux".into(),
256 "aabb".into(),
257 );
258
259 disc.known.insert("remote-1".to_string());
261
262 assert!(!disc.known.insert("remote-1".to_string()));
264 assert!(disc.known.insert("remote-2".to_string()));
266 }
267
268 #[tokio::test]
269 async fn mdns_poll_when_inactive_returns_empty() {
270 let mut disc = MdnsDiscovery::new(
271 "n".into(),
272 "127.0.0.1".into(),
273 9489,
274 "linux".into(),
275 "aa".into(),
276 );
277 let peers = disc.poll().await;
279 assert!(peers.is_empty());
280 }
281
282 #[tokio::test]
283 async fn mdns_pending_drained_on_poll() {
284 let mut disc = MdnsDiscovery::new(
285 "n".into(),
286 "127.0.0.1".into(),
287 9489,
288 "linux".into(),
289 "aa".into(),
290 );
291 disc.active = true;
292 disc.pending.push(DiscoveredPeer {
293 node_id: "injected".into(),
294 address: "10.0.0.1:9489".into(),
295 platform: "darwin".into(),
296 source: DiscoverySource::Mdns,
297 });
298
299 let peers = disc.poll().await;
300 assert!(peers.iter().any(|p| p.node_id == "injected"));
303
304 let second = disc.poll().await;
306 assert!(second.is_empty());
307 }
308}