1use std::net::{Ipv4Addr, SocketAddrV4, UdpSocket};
26use std::sync::atomic::Ordering;
27use std::sync::{Arc, Mutex};
28use std::time::Duration;
29
30use super::config::{BroadcastConfig, BroadcastMode};
31use super::protocol::{encode, MAX_DATAGRAM};
32use super::provider::{
33 jitter_ms, Counters, MeshProvider, MeshProviderState, Observation, ProviderContext,
34 ProviderStatus,
35};
36use super::registry::MeshSource;
37use super::MeshError;
38
39const READ_TIMEOUT: Duration = Duration::from_millis(500);
40
41pub struct BroadcastProvider {
44 config: BroadcastConfig,
45 listen: bool,
47 counters: Arc<Counters>,
48 state: Arc<Mutex<(MeshProviderState, Option<String>)>>,
49}
50
51impl BroadcastProvider {
52 pub fn new(config: BroadcastConfig, listen: bool) -> Self {
56 BroadcastProvider {
57 config,
58 listen,
59 counters: Arc::new(Counters::default()),
60 state: Arc::new(Mutex::new((MeshProviderState::Stopped, None))),
61 }
62 }
63
64 fn destinations(&self) -> Result<Vec<SocketAddrV4>, MeshError> {
65 match self.config.mode {
66 BroadcastMode::Disabled => Ok(Vec::new()),
67 BroadcastMode::Auto => Ok(vec![SocketAddrV4::new(
68 Ipv4Addr::BROADCAST,
69 self.config.port,
70 )]),
71 BroadcastMode::Explicit => self
72 .config
73 .networks
74 .iter()
75 .map(|n| {
76 n.parse::<Ipv4Addr>()
77 .map(|ip| SocketAddrV4::new(ip, self.config.port))
78 .map_err(|_| {
79 MeshError::Provider(format!("'{n}' is not an IPv4 broadcast address"))
80 })
81 })
82 .collect(),
83 }
84 }
85}
86
87impl MeshProvider for BroadcastProvider {
88 fn id(&self) -> &'static str {
89 "udp_broadcast"
90 }
91
92 fn start(&mut self, ctx: &ProviderContext) -> Result<(), MeshError> {
93 let destinations = match self.destinations() {
94 Ok(d) => d,
95 Err(e) => {
96 *self.state.lock().expect("broadcast state") =
97 (MeshProviderState::Failed, Some(e.to_string()));
98 return Err(e);
99 }
100 };
101 if destinations.is_empty() {
102 let reason = "mode is disabled".to_string();
103 *self.state.lock().expect("broadcast state") =
104 (MeshProviderState::Stopped, Some(reason.clone()));
105 return Err(MeshError::Provider(reason));
106 }
107 let bind_port = if self.listen { self.config.port } else { 0 };
110 let socket = match UdpSocket::bind((Ipv4Addr::UNSPECIFIED, bind_port)) {
111 Ok(s) => s,
112 Err(e) => {
113 let reason = format!("cannot bind udp port {bind_port}: {e}");
114 *self.state.lock().expect("broadcast state") =
115 (MeshProviderState::Failed, Some(reason.clone()));
116 return Err(MeshError::Provider(reason));
117 }
118 };
119 if let Err(e) = socket.set_broadcast(true) {
120 let reason = format!("cannot enable broadcast: {e}");
121 *self.state.lock().expect("broadcast state") =
122 (MeshProviderState::Failed, Some(reason.clone()));
123 return Err(MeshError::Provider(reason));
124 }
125 let _ = socket.set_read_timeout(Some(READ_TIMEOUT));
126 *self.state.lock().expect("broadcast state") = (
127 MeshProviderState::Running,
128 Some(format!(
129 "{} destination(s), {}",
130 destinations.len(),
131 if self.listen {
132 "listening"
133 } else {
134 "transmit only"
135 }
136 )),
137 );
138
139 if self.listen {
140 let socket = socket.try_clone().map_err(|e| {
141 MeshError::Provider(format!("cannot clone the broadcast socket: {e}"))
142 })?;
143 let tx = ctx.tx.clone();
144 let stop = Arc::clone(&ctx.stop);
145 let counters = Arc::clone(&self.counters);
146 let state = Arc::clone(&self.state);
147 let _ = std::thread::Builder::new()
148 .name("majordomus-mesh-broadcast-listen".into())
149 .spawn(move || {
150 let mut buffer = [0u8; MAX_DATAGRAM + 1];
151 while !stop.load(Ordering::SeqCst) {
152 match socket.recv_from(&mut buffer) {
153 Ok((len, from)) => {
154 counters.received.fetch_add(1, Ordering::Relaxed);
155 let _ = tx.send(Observation {
156 source: MeshSource::UdpBroadcast,
157 path: from.to_string(),
158 bytes: buffer[..len].to_vec(),
159 });
160 }
161 Err(e)
162 if e.kind() == std::io::ErrorKind::WouldBlock
163 || e.kind() == std::io::ErrorKind::TimedOut => {}
164 Err(e) => {
165 *state.lock().expect("broadcast state") =
166 (MeshProviderState::Failed, Some(format!("recv: {e}")));
167 return;
168 }
169 }
170 }
171 *state.lock().expect("broadcast state") = (MeshProviderState::Stopped, None);
172 });
173 }
174
175 {
176 let stop = Arc::clone(&ctx.stop);
177 let beacon = Arc::clone(&ctx.beacon);
178 let counters = Arc::clone(&self.counters);
179 let interval = Duration::from_secs(self.config.interval_seconds);
180 let _ = std::thread::Builder::new()
181 .name("majordomus-mesh-broadcast-announce".into())
182 .spawn(move || {
183 while !stop.load(Ordering::SeqCst) {
184 if let Ok(bytes) = encode(&beacon.next_envelope()) {
185 for destination in &destinations {
186 if socket.send_to(&bytes, destination).is_ok() {
187 counters.sent.fetch_add(1, Ordering::Relaxed);
188 }
189 }
190 }
191 let total = interval + Duration::from_millis(jitter_ms(2000));
192 let mut slept = Duration::ZERO;
193 while slept < total && !stop.load(Ordering::SeqCst) {
194 let slice = READ_TIMEOUT.min(total - slept);
195 std::thread::sleep(slice);
196 slept += slice;
197 }
198 }
199 });
200 }
201 Ok(())
202 }
203
204 fn status(&self) -> ProviderStatus {
205 let (state, detail) = self.state.lock().expect("broadcast state").clone();
206 ProviderStatus {
207 id: self.id().into(),
208 state,
209 detail,
210 sent: self.counters.sent.load(Ordering::Relaxed),
211 received: self.counters.received.load(Ordering::Relaxed),
212 }
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn destinations_follow_the_declared_mode() {
222 let auto = BroadcastProvider::new(
223 BroadcastConfig {
224 mode: BroadcastMode::Auto,
225 ..BroadcastConfig::default()
226 },
227 false,
228 );
229 assert_eq!(auto.destinations().unwrap().len(), 1);
230 let explicit = BroadcastProvider::new(
231 BroadcastConfig {
232 mode: BroadcastMode::Explicit,
233 networks: vec!["192.168.1.255".into(), "10.0.0.255".into()],
234 ..BroadcastConfig::default()
235 },
236 false,
237 );
238 assert_eq!(explicit.destinations().unwrap().len(), 2);
239 let disabled = BroadcastProvider::new(BroadcastConfig::default(), false);
240 assert!(disabled.destinations().unwrap().is_empty());
241 }
242
243 const BIND_ATTEMPTS: usize = 8;
245
246 fn address_in_use() -> String {
249 let held = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).unwrap();
250 let refused = UdpSocket::bind(held.local_addr().unwrap()).unwrap_err();
251 assert_eq!(refused.kind(), std::io::ErrorKind::AddrInUse, "{refused}");
252 refused.to_string()
253 }
254
255 fn start_listening_on_a_free_port(
266 ctx: &crate::mesh::provider::ProviderContext,
267 ) -> (BroadcastProvider, u16) {
268 let in_use = address_in_use();
269 let mut lost = Vec::new();
270 for _ in 0..BIND_ATTEMPTS {
271 let port = {
272 let probe = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).unwrap();
273 probe.local_addr().unwrap().port()
274 };
275 let mut provider = BroadcastProvider::new(
276 BroadcastConfig {
277 mode: BroadcastMode::Auto,
278 port,
279 interval_seconds: 1,
280 ..BroadcastConfig::default()
281 },
282 true,
283 );
284 match provider.start(ctx) {
285 Ok(()) => {
286 assert_eq!(provider.status().state, MeshProviderState::Running);
289 return (provider, port);
290 }
291 Err(e)
292 if e.to_string()
293 .ends_with(&format!("cannot bind udp port {port}: {in_use}")) =>
294 {
295 lost.push(port);
296 }
297 Err(e) => panic!("the listening provider did not start: {e}"),
298 }
299 }
300 panic!("every port probed was taken before the provider could bind it: {lost:?}");
301 }
302
303 #[test]
304 fn the_sole_udp_provider_listens_and_counts_what_it_hears() {
305 let (tx, rx) = crate::mesh::provider::ObservationSender::bounded(64);
306 let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
307 let ctx = crate::mesh::provider::ProviderContext {
308 tx,
309 stop: std::sync::Arc::clone(&stop),
310 beacon: std::sync::Arc::new(crate::mesh::provider::Beacon::new(
311 std::sync::Arc::new(crate::mesh::identity::NodeIdentity::ephemeral().unwrap()),
312 vec![],
313 vec![],
314 vec![],
315 "test",
316 )),
317 };
318 let (_provider, port) = start_listening_on_a_free_port(&ctx);
319 let sender = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
324 let _ = sender.send_to(b"aimed at the port", (Ipv4Addr::LOCALHOST, port));
325 if let Ok(heard) = rx.recv_timeout(Duration::from_secs(2)) {
326 assert_eq!(heard.source, MeshSource::UdpBroadcast);
327 }
328 stop.store(true, std::sync::atomic::Ordering::SeqCst);
329 }
330
331 #[test]
332 fn a_malformed_network_is_an_error_naming_it() {
333 let provider = BroadcastProvider::new(
334 BroadcastConfig {
335 mode: BroadcastMode::Explicit,
336 networks: vec!["not-an-address".into()],
337 ..BroadcastConfig::default()
338 },
339 false,
340 );
341 let error = provider.destinations().unwrap_err().to_string();
342 assert!(error.contains("not-an-address"), "{error}");
343 }
344}