1pub mod matrix;
12
13use std::collections::{BTreeMap, HashSet};
14use std::future::Future;
15use std::net::{Ipv4Addr, SocketAddrV4};
16
17use bacnet_transport::bbmd::{BdtEntry, FdtEntryWire};
18use bacnet_transport::bip::BvlcExchangeError;
19use bacnet_transport::bvll::BvlcResultCode;
20use futures_util::stream::{FuturesUnordered, StreamExt};
21
22pub use self::matrix::{Cell, Finding, Matrix, Probe};
23
24pub const MAX_SURVEY: usize = 16;
27
28pub const SURVEY_AT_ONCE: usize = 4;
33
34pub const FDT_EXPIRING_S: u16 = 30;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub struct BdtRow {
43 pub address: SocketAddrV4,
45 pub mask: Ipv4Addr,
47}
48
49impl BdtRow {
50 pub fn is_unicast(&self) -> bool {
54 self.mask == Ipv4Addr::BROADCAST
55 }
56
57 pub fn forward_to(&self) -> Ipv4Addr {
61 Ipv4Addr::from(self.address.ip().to_bits() | !self.mask.to_bits())
62 }
63
64 pub fn explain(&self) -> String {
66 if self.is_unicast() {
67 "peers unicast to it, and it rebroadcasts on its subnet (two-hop)".into()
68 } else {
69 format!(
70 "peers send a directed broadcast to {} (one-hop; routers must pass it)",
71 self.forward_to()
72 )
73 }
74 }
75}
76
77impl From<&BdtEntry> for BdtRow {
78 fn from(entry: &BdtEntry) -> Self {
79 Self {
80 address: SocketAddrV4::new(Ipv4Addr::from(entry.ip), entry.port),
81 mask: Ipv4Addr::from(entry.broadcast_mask),
82 }
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub struct FdtRow {
89 pub address: SocketAddrV4,
92 pub ttl: u16,
94 pub remaining: u16,
97}
98
99impl FdtRow {
100 pub fn expiring(&self) -> bool {
102 self.remaining < FDT_EXPIRING_S
103 }
104}
105
106impl From<&FdtEntryWire> for FdtRow {
107 fn from(entry: &FdtEntryWire) -> Self {
108 Self {
109 address: SocketAddrV4::new(Ipv4Addr::from(entry.ip), entry.port),
110 ttl: entry.ttl,
111 remaining: entry.seconds_remaining,
112 }
113 }
114}
115
116pub fn mac(address: SocketAddrV4) -> [u8; 6] {
118 let [a, b, c, d] = address.ip().octets();
119 let [high, low] = address.port().to_be_bytes();
120 [a, b, c, d, high, low]
121}
122
123pub fn address_of(mac: &[u8]) -> Option<SocketAddrV4> {
125 let [a, b, c, d, high, low] = <[u8; 6]>::try_from(mac).ok()?;
126 Some(SocketAddrV4::new(
127 Ipv4Addr::new(a, b, c, d),
128 u16::from_be_bytes([high, low]),
129 ))
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
134pub enum Outcome<R> {
135 Table(R),
137 Nak(BvlcResultCode),
140 Unreachable(String),
142 Skipped,
145}
146
147impl<R> Outcome<R> {
148 pub fn table(&self) -> Option<&R> {
150 match self {
151 Self::Table(rows) => Some(rows),
152 _ => None,
153 }
154 }
155
156 pub fn from_error(error: &BvlcExchangeError) -> Self {
161 match error {
162 BvlcExchangeError::Nak(code) => Self::Nak(*code),
163 other => Self::Unreachable(unreachable_reason(other)),
164 }
165 }
166}
167
168pub fn probe_error(error: &BvlcExchangeError) -> Probe {
170 Probe::from_error(error)
171}
172
173pub const NO_ANSWER: &str = "no answer";
175
176pub fn unreachable_reason(error: &BvlcExchangeError) -> String {
178 match error {
179 BvlcExchangeError::NoReply(_) => NO_ANSWER.into(),
180 other => other.to_string(),
181 }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct Surveyed<X> {
188 pub bbmd: SocketAddrV4,
190 pub probe: Probe,
192 pub extra: X,
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
200pub enum NotFollowed {
201 Unresolved,
204 Unspecified,
206 Broadcast,
208 DirectedBroadcast,
212 Multicast,
214 Loopback,
216 Own,
218}
219
220impl NotFollowed {
221 pub fn as_str(self) -> &'static str {
223 match self {
224 Self::Unresolved => "unresolved",
225 Self::Unspecified => "unspecified",
226 Self::Broadcast => "broadcast",
227 Self::DirectedBroadcast => "directed-broadcast",
228 Self::Multicast => "multicast",
229 Self::Loopback => "loopback",
230 Self::Own => "own-address",
231 }
232 }
233
234 pub fn check(row: &BdtRow, here: &Here) -> Option<Self> {
239 let directed = !row.is_unicast() && row.forward_to() == *row.address.ip();
240 Self::check_address(row.address, here).or(directed.then_some(Self::DirectedBroadcast))
241 }
242
243 pub fn check_address(address: SocketAddrV4, here: &Here) -> Option<Self> {
246 let own = here.own;
247 let ip = address.ip();
248 if ip.is_unspecified() && address.port() == 0 {
249 Some(Self::Unresolved)
250 } else if ip.is_unspecified() || address.port() == 0 {
251 Some(Self::Unspecified)
252 } else if ip.is_broadcast() {
253 Some(Self::Broadcast)
254 } else if ip.is_multicast() {
255 Some(Self::Multicast)
256 } else if *ip == here.broadcast && here.broadcast != *own.ip() {
257 Some(Self::DirectedBroadcast)
260 } else if address == own {
261 Some(Self::Own)
262 } else if ip.is_loopback() && !own.ip().is_loopback() {
263 Some(Self::Loopback)
264 } else {
265 None
266 }
267 }
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273pub struct Here {
274 pub own: SocketAddrV4,
276 pub broadcast: Ipv4Addr,
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq)]
282pub struct Unfollowed {
283 pub address: SocketAddrV4,
285 pub listed_by: SocketAddrV4,
287 pub why: NotFollowed,
289}
290
291#[derive(Debug, Clone, PartialEq, Eq)]
293pub struct Survey<X> {
294 pub read: Vec<Surveyed<X>>,
296 pub unread: Vec<SocketAddrV4>,
299 pub not_followed: Vec<Unfollowed>,
301}
302
303pub async fn survey<X, F, R>(seeds: &[SocketAddrV4], here: Here, read: F) -> Survey<X>
316where
317 F: Fn(SocketAddrV4) -> R,
318 R: Future<Output = (Probe, X)>,
319{
320 let read = &read;
321 let mut seen = HashSet::new();
322 let mut found: Vec<SocketAddrV4> = seeds.iter().copied().filter(|s| seen.insert(*s)).collect();
324 let mut done: Vec<Surveyed<X>> = Vec::new();
325 let mut not_followed = Vec::new();
326 let mut early = BTreeMap::new();
328 let mut started = 0;
329 let mut reads = FuturesUnordered::new();
330 loop {
331 while reads.len() < SURVEY_AT_ONCE && started < found.len().min(MAX_SURVEY) {
332 let (index, bbmd) = (started, found[started]);
333 reads.push(async move { (index, read(bbmd).await) });
334 started += 1;
335 }
336 let Some((index, outcome)) = reads.next().await else {
337 break;
338 };
339 early.insert(index, outcome);
340 while let Some((probe, extra)) = early.remove(&done.len()) {
341 let bbmd = found[done.len()];
342 if let Probe::Table(rows) = &probe {
343 for row in rows {
344 if !seen.insert(row.address) {
345 continue;
346 }
347 match NotFollowed::check(row, &here) {
348 Some(why) => not_followed.push(Unfollowed {
349 address: row.address,
350 listed_by: bbmd,
351 why,
352 }),
353 None => found.push(row.address),
354 }
355 }
356 }
357 done.push(Surveyed { bbmd, probe, extra });
358 }
359 }
360 Survey {
361 unread: found.split_off(done.len()),
362 read: done,
363 not_followed,
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use std::time::Duration;
371
372 fn at(last: u8) -> SocketAddrV4 {
373 SocketAddrV4::new(Ipv4Addr::new(10, 0, last, 1), 47808)
374 }
375
376 fn nowhere() -> Here {
378 Here {
379 own: at(250),
380 broadcast: Ipv4Addr::BROADCAST,
381 }
382 }
383
384 fn row(address: &str, mask: [u8; 4]) -> BdtRow {
385 BdtRow {
386 address: address.parse().unwrap(),
387 mask: Ipv4Addr::from(mask),
388 }
389 }
390
391 #[test]
392 fn a_mask_says_how_peers_forward() {
393 let unicast = row("10.0.1.1:47808", [255; 4]);
394 assert!(unicast.is_unicast());
395 assert_eq!(unicast.forward_to(), Ipv4Addr::new(10, 0, 1, 1));
396 assert!(unicast.explain().contains("two-hop"));
397 let directed = row("10.0.2.1:47808", [255, 255, 255, 0]);
398 assert!(!directed.is_unicast());
399 assert_eq!(directed.forward_to(), Ipv4Addr::new(10, 0, 2, 255));
400 assert!(directed.explain().contains("10.0.2.255"));
401 }
402
403 #[test]
404 fn an_entry_under_thirty_seconds_is_expiring() {
405 let entry = |remaining| FdtRow {
406 address: at(9),
407 ttl: 60,
408 remaining,
409 };
410 assert!(entry(29).expiring());
411 assert!(!entry(30).expiring());
412 }
413
414 #[test]
415 fn a_nak_and_a_timeout_sort_apart() {
416 use bacnet_transport::bvll::BvlcFunction;
417 let code = BvlcResultCode::READ_BROADCAST_DISTRIBUTION_TABLE_NAK;
418 assert_eq!(probe_error(&BvlcExchangeError::Nak(code)), Probe::Nak(code));
419 let silent = BvlcExchangeError::NoReply(Duration::from_secs(3));
420 assert_eq!(probe_error(&silent), Probe::Unreachable("no answer".into()));
421 let success = BvlcExchangeError::UnexpectedResponse {
423 expected: BvlcFunction::READ_BROADCAST_DISTRIBUTION_TABLE_ACK,
424 got: BvlcFunction::BVLC_RESULT,
425 };
426 assert!(matches!(probe_error(&success), Probe::Unreachable(_)));
427 let local = BvlcExchangeError::Transport(bacnet_types::error::Error::Encoding(
428 "B/IP management client is stopped".into(),
429 ));
430 assert_eq!(
431 probe_error(&local),
432 Probe::Unreachable("encoding error: B/IP management client is stopped".into())
433 );
434 let mac = mac("10.0.0.1:47809".parse().unwrap());
435 assert_eq!(address_of(&mac), Some("10.0.0.1:47809".parse().unwrap()));
436 assert_eq!(address_of(&mac[..5]), None);
437 }
438
439 #[tokio::test]
442 async fn every_seed_is_read_then_what_the_tables_list() {
443 let tables = |bbmd: SocketAddrV4| match bbmd.ip().octets()[2] {
444 1 => vec![at(1), at(2), at(3)],
445 2 => vec![at(2), at(1)],
446 _ => vec![],
447 };
448 let surveyed = survey(&[at(1), at(2), at(1)], nowhere(), |bbmd| async move {
449 let rows = tables(bbmd)
450 .into_iter()
451 .map(|address| BdtRow {
452 address,
453 mask: Ipv4Addr::BROADCAST,
454 })
455 .collect();
456 (Probe::Table(rows), bbmd.port())
457 })
458 .await;
459 let order: Vec<SocketAddrV4> = surveyed.read.iter().map(|s| s.bbmd).collect();
460 assert_eq!(order, [at(1), at(2), at(3)]);
461 assert!(surveyed.read.iter().all(|s| s.extra == 47808));
462 assert!(surveyed.unread.is_empty() && surveyed.not_followed.is_empty());
463 }
464
465 #[tokio::test]
469 async fn addresses_no_read_may_go_to_are_not_followed() {
470 let here = Here {
471 own: at(1),
472 broadcast: Ipv4Addr::new(10, 0, 9, 255),
473 };
474 const ONES: [u8; 4] = [255; 4];
475 const SUBNET: [u8; 4] = [255, 255, 255, 0];
476 let bad = [
477 ("0.0.0.0:0", ONES, NotFollowed::Unresolved),
478 ("0.0.0.0:47808", ONES, NotFollowed::Unspecified),
479 ("10.0.5.1:0", ONES, NotFollowed::Unspecified),
480 ("255.255.255.255:47808", ONES, NotFollowed::Broadcast),
481 ("224.0.0.1:47808", ONES, NotFollowed::Multicast),
482 ("239.255.255.250:1900", ONES, NotFollowed::Multicast),
483 ("127.0.0.1:53", ONES, NotFollowed::Loopback),
484 ("10.0.9.255:47808", ONES, NotFollowed::DirectedBroadcast),
486 ("10.0.7.255:47808", SUBNET, NotFollowed::DirectedBroadcast),
488 ];
489 let mut rows: Vec<BdtRow> = bad
490 .iter()
491 .map(|(address, mask, _)| BdtRow {
492 address: address.parse().unwrap(),
493 mask: Ipv4Addr::from(*mask),
494 })
495 .collect();
496 rows.push(BdtRow {
498 address: here.own,
499 mask: Ipv4Addr::BROADCAST,
500 });
501 rows.push(rows[0]);
502 let reads = std::sync::Mutex::new(Vec::new());
503 let surveyed = survey(&[at(2)], here, |bbmd| {
504 reads.lock().unwrap().push(bbmd);
505 let rows = rows.clone();
506 async move { (Probe::Table(rows), ()) }
507 })
508 .await;
509 assert_eq!(*reads.lock().unwrap(), [at(2)], "only the seed is read");
510 let mut expected: Vec<Unfollowed> = bad
511 .iter()
512 .map(|(address, _, why)| Unfollowed {
513 address: address.parse().unwrap(),
514 listed_by: at(2),
515 why: *why,
516 })
517 .collect();
518 expected.push(Unfollowed {
519 address: here.own,
520 listed_by: at(2),
521 why: NotFollowed::Own,
522 });
523 assert_eq!(surveyed.not_followed, expected);
524 let entry = |address: &str, mask: [u8; 4]| BdtRow {
527 address: address.parse().unwrap(),
528 mask: Ipv4Addr::from(mask),
529 };
530 assert_eq!(
531 NotFollowed::check(&entry("10.0.7.255:47808", ONES), &here),
532 None
533 );
534 assert_eq!(
535 NotFollowed::check(&entry("10.0.7.1:47808", SUBNET), &here),
536 None
537 );
538 let local = Here {
540 own: "127.0.0.1:47808".parse().unwrap(),
541 broadcast: Ipv4Addr::new(127, 255, 255, 255),
542 };
543 let peer = entry("127.0.0.2:47809", ONES);
544 assert_eq!(NotFollowed::check(&peer, &local), None);
545 assert_eq!(
546 NotFollowed::check_address(local.own, &local),
547 Some(NotFollowed::Own)
548 );
549 assert_eq!(NotFollowed::check_address(at(3), &here), None);
550 }
551
552 #[tokio::test]
555 async fn a_large_fan_out_reads_at_most_the_cap() {
556 let next = std::sync::atomic::AtomicU32::new(0);
558 let peers = |_: SocketAddrV4| -> Vec<BdtRow> {
559 (0..128)
560 .map(|_| {
561 let n = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
562 BdtRow {
563 address: SocketAddrV4::new(Ipv4Addr::from(0x0B00_0000 + n), 47808),
564 mask: Ipv4Addr::BROADCAST,
565 }
566 })
567 .collect()
568 };
569 let reads = std::sync::atomic::AtomicUsize::new(0);
570 let surveyed = survey(&[at(1)], nowhere(), |bbmd| {
571 reads.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
572 let rows = peers(bbmd);
573 async move { (Probe::Table(rows), ()) }
574 })
575 .await;
576 assert_eq!(reads.into_inner(), MAX_SURVEY);
577 assert_eq!(surveyed.read.len(), MAX_SURVEY);
578 assert!(surveyed.unread.len() > 1_000, "{}", surveyed.unread.len());
579 let probes: Vec<(SocketAddrV4, Probe)> = surveyed
581 .read
582 .iter()
583 .map(|s| (s.bbmd, s.probe.clone()))
584 .collect();
585 let matrix = Matrix::build(&probes);
586 assert!(matrix.bbmds.len() <= matrix::GRID_CAP);
587 assert!(matrix.omitted > 1_000, "{}", matrix.omitted);
588 }
589
590 #[tokio::test]
592 async fn no_seed_reads_nothing() {
593 let surveyed = survey(&[], nowhere(), |_| async { (Probe::Skipped, ()) }).await;
594 assert!(surveyed.read.is_empty() && surveyed.unread.is_empty());
595 }
596}