1use std::net::{Ipv4Addr, SocketAddrV4, UdpSocket};
25use std::sync::atomic::Ordering;
26use std::sync::{Arc, Mutex};
27use std::time::Duration;
28
29use super::config::MulticastConfig;
30use super::protocol::{encode, MAX_DATAGRAM};
31use super::provider::{
32 jitter_ms, Counters, MeshProvider, MeshProviderState, Observation, ProviderContext,
33 ProviderStatus,
34};
35use super::registry::MeshSource;
36use super::MeshError;
37
38const READ_TIMEOUT: Duration = Duration::from_millis(500);
40
41pub struct MulticastProvider {
44 config: MulticastConfig,
45 counters: Arc<Counters>,
46 state: Arc<Mutex<(MeshProviderState, Option<String>)>>,
47 bound: std::sync::atomic::AtomicU16,
50}
51
52impl MulticastProvider {
53 pub fn new(config: MulticastConfig) -> Self {
56 MulticastProvider {
57 config,
58 counters: Arc::new(Counters::default()),
59 state: Arc::new(Mutex::new((MeshProviderState::Stopped, None))),
60 bound: std::sync::atomic::AtomicU16::new(0),
61 }
62 }
63
64 pub fn port(&self) -> u16 {
79 self.bound.load(Ordering::SeqCst)
80 }
81
82 fn open(&self) -> Result<(UdpSocket, Ipv4Addr), MeshError> {
83 let group: Ipv4Addr = self.config.group.parse().map_err(|_| {
84 MeshError::Provider(format!("'{}' is not an IPv4 group", self.config.group))
85 })?;
86 if !group.is_multicast() {
87 return Err(MeshError::Provider(format!(
88 "{group} is not a multicast address"
89 )));
90 }
91 let socket = bind_shared(self.config.port).map_err(|e| {
92 MeshError::Provider(format!("cannot bind udp port {}: {e}", self.config.port))
93 })?;
94 socket
95 .join_multicast_v4(&group, &Ipv4Addr::UNSPECIFIED)
96 .map_err(|e| MeshError::Provider(format!("cannot join {group}: {e}")))?;
97 socket
98 .set_multicast_ttl_v4(self.config.ttl)
99 .map_err(|e| MeshError::Provider(format!("cannot set ttl {}: {e}", self.config.ttl)))?;
100 socket
101 .set_read_timeout(Some(READ_TIMEOUT))
102 .map_err(|e| MeshError::Provider(format!("cannot bound the socket read: {e}")))?;
103 Ok((socket, group))
104 }
105}
106
107#[cfg(unix)]
112fn bind_shared(port: u16) -> std::io::Result<UdpSocket> {
113 use std::os::fd::FromRawFd;
114 unsafe {
118 let fd = libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0);
119 if fd < 0 {
120 return Err(std::io::Error::last_os_error());
121 }
122 let fail = |fd| {
123 let error = std::io::Error::last_os_error();
124 libc::close(fd);
125 Err(error)
126 };
127 let one: libc::c_int = 1;
128 for option in [libc::SO_REUSEADDR, libc::SO_REUSEPORT] {
129 if libc::setsockopt(
130 fd,
131 libc::SOL_SOCKET,
132 option,
133 &one as *const libc::c_int as *const libc::c_void,
134 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
135 ) != 0
136 {
137 return fail(fd);
138 }
139 }
140 let mut address: libc::sockaddr_in = std::mem::zeroed();
141 address.sin_family = libc::AF_INET as libc::sa_family_t;
142 address.sin_port = port.to_be();
143 address.sin_addr = libc::in_addr {
144 s_addr: u32::from(Ipv4Addr::UNSPECIFIED).to_be(),
145 };
146 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
147 {
148 address.sin_len = std::mem::size_of::<libc::sockaddr_in>() as u8;
149 }
150 if libc::bind(
151 fd,
152 &address as *const libc::sockaddr_in as *const libc::sockaddr,
153 std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
154 ) != 0
155 {
156 return fail(fd);
157 }
158 Ok(UdpSocket::from_raw_fd(fd))
159 }
160}
161
162#[cfg(not(unix))]
164fn bind_shared(port: u16) -> std::io::Result<UdpSocket> {
165 UdpSocket::bind((Ipv4Addr::UNSPECIFIED, port))
166}
167
168impl MeshProvider for MulticastProvider {
169 fn id(&self) -> &'static str {
170 "udp_multicast"
171 }
172
173 fn start(&mut self, ctx: &ProviderContext) -> Result<(), MeshError> {
174 let (socket, group) = match self.open() {
175 Ok(opened) => opened,
176 Err(e) => {
177 *self.state.lock().expect("multicast state") =
178 (MeshProviderState::Failed, Some(e.to_string()));
179 return Err(e);
180 }
181 };
182 let port = socket.local_addr().map_or(self.config.port, |a| a.port());
184 self.bound.store(port, Ordering::SeqCst);
185 let destination = SocketAddrV4::new(group, port);
186 *self.state.lock().expect("multicast state") = (
187 MeshProviderState::Running,
188 Some(format!("group {destination}, ttl {}", self.config.ttl)),
189 );
190
191 {
193 let socket = socket.try_clone().map_err(|e| {
194 MeshError::Provider(format!("cannot clone the multicast socket: {e}"))
195 })?;
196 let tx = ctx.tx.clone();
197 let stop = Arc::clone(&ctx.stop);
198 let counters = Arc::clone(&self.counters);
199 let state = Arc::clone(&self.state);
200 let _ = std::thread::Builder::new()
201 .name("majordomus-mesh-multicast-listen".into())
202 .spawn(move || {
203 let mut buffer = [0u8; MAX_DATAGRAM + 1];
204 while !stop.load(Ordering::SeqCst) {
205 match socket.recv_from(&mut buffer) {
206 Ok((len, from)) => {
207 counters.received.fetch_add(1, Ordering::Relaxed);
208 let _ = tx.send(Observation {
209 source: MeshSource::UdpMulticast,
210 path: from.to_string(),
211 bytes: buffer[..len].to_vec(),
212 });
213 }
214 Err(e)
215 if e.kind() == std::io::ErrorKind::WouldBlock
216 || e.kind() == std::io::ErrorKind::TimedOut => {}
217 Err(e) => {
218 *state.lock().expect("multicast state") =
219 (MeshProviderState::Failed, Some(format!("recv: {e}")));
220 return;
221 }
222 }
223 }
224 *state.lock().expect("multicast state") = (MeshProviderState::Stopped, None);
225 });
226 }
227
228 {
231 let tx_socket = socket;
232 let stop = Arc::clone(&ctx.stop);
233 let beacon = Arc::clone(&ctx.beacon);
234 let counters = Arc::clone(&self.counters);
235 let interval = Duration::from_secs(self.config.interval_seconds);
236 let _ = std::thread::Builder::new()
237 .name("majordomus-mesh-multicast-announce".into())
238 .spawn(move || {
239 while !stop.load(Ordering::SeqCst) {
240 if let Ok(bytes) = encode(&beacon.next_envelope()) {
241 if tx_socket.send_to(&bytes, destination).is_ok() {
242 counters.sent.fetch_add(1, Ordering::Relaxed);
243 }
244 }
245 let total = interval + Duration::from_millis(jitter_ms(2000));
248 let mut slept = Duration::ZERO;
249 while slept < total && !stop.load(Ordering::SeqCst) {
250 let slice = READ_TIMEOUT.min(total - slept);
251 std::thread::sleep(slice);
252 slept += slice;
253 }
254 }
255 });
256 }
257 Ok(())
258 }
259
260 fn status(&self) -> ProviderStatus {
261 let (state, detail) = self.state.lock().expect("multicast state").clone();
262 ProviderStatus {
263 id: self.id().into(),
264 state,
265 detail,
266 sent: self.counters.sent.load(Ordering::Relaxed),
267 received: self.counters.received.load(Ordering::Relaxed),
268 }
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use crate::mesh::provider::MeshProvider;
276
277 fn heard_among(
280 rx: &std::sync::mpsc::Receiver<Observation>,
281 bytes: &[u8],
282 within: Duration,
283 ) -> Option<Observation> {
284 let deadline = std::time::Instant::now() + within;
285 loop {
286 let left = deadline.checked_duration_since(std::time::Instant::now())?;
287 let heard = rx.recv_timeout(left).ok()?;
288 if heard.bytes == bytes {
289 return Some(heard);
290 }
291 }
292 }
293
294 #[test]
295 fn the_datagram_a_test_sent_is_found_behind_the_providers_own_beacon() {
296 let (tx, rx) = std::sync::mpsc::channel();
297 let seen = |bytes: &[u8]| Observation {
298 source: MeshSource::UdpMulticast,
299 path: "127.0.0.1:1".into(),
300 bytes: bytes.to_vec(),
301 };
302 tx.send(seen(b"{\"v\":1,\"pk\":\"1fb5\"}")).unwrap();
303 tx.send(seen(b"a datagram for the listener")).unwrap();
304 let heard = heard_among(
305 &rx,
306 b"a datagram for the listener",
307 Duration::from_millis(200),
308 );
309 assert_eq!(
310 heard.map(|o| o.bytes),
311 Some(b"a datagram for the listener".to_vec())
312 );
313 assert!(heard_among(&rx, b"never sent", Duration::from_millis(50)).is_none());
315 }
316
317 #[test]
318 fn a_group_that_is_not_multicast_is_refused_with_the_reason() {
319 let mut provider = MulticastProvider::new(MulticastConfig {
320 group: "10.0.0.1".into(),
321 ..MulticastConfig::default()
322 });
323 let (tx, _rx) = crate::mesh::provider::ObservationSender::bounded(64);
324 let ctx = crate::mesh::provider::ProviderContext {
325 tx,
326 stop: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)),
327 beacon: std::sync::Arc::new(crate::mesh::provider::Beacon::new(
328 std::sync::Arc::new(crate::mesh::identity::NodeIdentity::ephemeral().unwrap()),
329 vec![],
330 vec![],
331 vec![],
332 "test",
333 )),
334 };
335 let error = provider.start(&ctx).unwrap_err().to_string();
336 assert!(error.contains("not a multicast address"), "{error}");
337 assert_eq!(provider.status().state, MeshProviderState::Failed);
338 }
339
340 #[test]
341 fn the_listener_hears_and_the_announcer_transmits() {
342 let mut provider = MulticastProvider::new(MulticastConfig {
357 port: 0,
358 interval_seconds: 1,
359 ..MulticastConfig::default()
360 });
361 assert_eq!(provider.port(), 0, "no port before the provider starts");
362 let (tx, rx) = crate::mesh::provider::ObservationSender::bounded(64);
363 let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
364 let ctx = crate::mesh::provider::ProviderContext {
365 tx,
366 stop: std::sync::Arc::clone(&stop),
367 beacon: std::sync::Arc::new(crate::mesh::provider::Beacon::new(
368 std::sync::Arc::new(crate::mesh::identity::NodeIdentity::ephemeral().unwrap()),
369 vec![],
370 vec![],
371 vec![],
372 "test",
373 )),
374 };
375
376 provider.start(&ctx).unwrap();
379 assert_eq!(provider.status().state, MeshProviderState::Running);
380 assert_eq!(provider.id(), "udp_multicast");
381 let port = provider.port();
382 assert_ne!(port, 0, "the bind chose a port");
383 let detail = provider.status().detail.unwrap_or_default();
384 assert!(
385 detail.contains(&format!(":{port}, ttl")),
386 "the status names the bound port: {detail}"
387 );
388
389 let sender = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
399 let _ = sender.send_to(b"a datagram for the listener", (Ipv4Addr::LOCALHOST, port));
400 if let Some(heard) =
401 heard_among(&rx, b"a datagram for the listener", Duration::from_secs(2))
402 {
403 assert_eq!(heard.source, MeshSource::UdpMulticast);
404 }
405
406 let deadline = std::time::Instant::now() + Duration::from_secs(2);
410 while provider.status().sent == 0 && std::time::Instant::now() < deadline {
411 std::thread::sleep(Duration::from_millis(50));
412 }
413 let _ = provider.status().sent;
416 stop.store(true, std::sync::atomic::Ordering::SeqCst);
417 }
418
419 #[test]
420 fn a_garbled_group_is_refused_before_any_socket_opens() {
421 let provider = MulticastProvider::new(MulticastConfig {
422 group: "not-an-ip".into(),
423 ..MulticastConfig::default()
424 });
425 assert!(provider.open().is_err());
426 }
427}