pub mod matrix;
use std::collections::{BTreeMap, HashSet};
use std::future::Future;
use std::net::{Ipv4Addr, SocketAddrV4};
use bacnet_transport::bbmd::{BdtEntry, FdtEntryWire};
use bacnet_transport::bip::BvlcExchangeError;
use bacnet_transport::bvll::BvlcResultCode;
use futures_util::stream::{FuturesUnordered, StreamExt};
pub use self::matrix::{Cell, Finding, Matrix, Probe};
pub const MAX_SURVEY: usize = 16;
pub const SURVEY_AT_ONCE: usize = 4;
pub const FDT_EXPIRING_S: u16 = 30;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BdtRow {
pub address: SocketAddrV4,
pub mask: Ipv4Addr,
}
impl BdtRow {
pub fn is_unicast(&self) -> bool {
self.mask == Ipv4Addr::BROADCAST
}
pub fn forward_to(&self) -> Ipv4Addr {
Ipv4Addr::from(self.address.ip().to_bits() | !self.mask.to_bits())
}
pub fn explain(&self) -> String {
if self.is_unicast() {
"peers unicast to it, and it rebroadcasts on its subnet (two-hop)".into()
} else {
format!(
"peers send a directed broadcast to {} (one-hop; routers must pass it)",
self.forward_to()
)
}
}
}
impl From<&BdtEntry> for BdtRow {
fn from(entry: &BdtEntry) -> Self {
Self {
address: SocketAddrV4::new(Ipv4Addr::from(entry.ip), entry.port),
mask: Ipv4Addr::from(entry.broadcast_mask),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FdtRow {
pub address: SocketAddrV4,
pub ttl: u16,
pub remaining: u16,
}
impl FdtRow {
pub fn expiring(&self) -> bool {
self.remaining < FDT_EXPIRING_S
}
}
impl From<&FdtEntryWire> for FdtRow {
fn from(entry: &FdtEntryWire) -> Self {
Self {
address: SocketAddrV4::new(Ipv4Addr::from(entry.ip), entry.port),
ttl: entry.ttl,
remaining: entry.seconds_remaining,
}
}
}
pub fn mac(address: SocketAddrV4) -> [u8; 6] {
let [a, b, c, d] = address.ip().octets();
let [high, low] = address.port().to_be_bytes();
[a, b, c, d, high, low]
}
pub fn address_of(mac: &[u8]) -> Option<SocketAddrV4> {
let [a, b, c, d, high, low] = <[u8; 6]>::try_from(mac).ok()?;
Some(SocketAddrV4::new(
Ipv4Addr::new(a, b, c, d),
u16::from_be_bytes([high, low]),
))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outcome<R> {
Table(R),
Nak(BvlcResultCode),
Unreachable(String),
Skipped,
}
impl<R> Outcome<R> {
pub fn table(&self) -> Option<&R> {
match self {
Self::Table(rows) => Some(rows),
_ => None,
}
}
pub fn from_error(error: &BvlcExchangeError) -> Self {
match error {
BvlcExchangeError::Nak(code) => Self::Nak(*code),
other => Self::Unreachable(unreachable_reason(other)),
}
}
}
pub fn probe_error(error: &BvlcExchangeError) -> Probe {
Probe::from_error(error)
}
pub const NO_ANSWER: &str = "no answer";
pub fn unreachable_reason(error: &BvlcExchangeError) -> String {
match error {
BvlcExchangeError::NoReply(_) => NO_ANSWER.into(),
other => other.to_string(),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Surveyed<X> {
pub bbmd: SocketAddrV4,
pub probe: Probe,
pub extra: X,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NotFollowed {
Unresolved,
Unspecified,
Broadcast,
DirectedBroadcast,
Multicast,
Loopback,
Own,
}
impl NotFollowed {
pub fn as_str(self) -> &'static str {
match self {
Self::Unresolved => "unresolved",
Self::Unspecified => "unspecified",
Self::Broadcast => "broadcast",
Self::DirectedBroadcast => "directed-broadcast",
Self::Multicast => "multicast",
Self::Loopback => "loopback",
Self::Own => "own-address",
}
}
pub fn check(row: &BdtRow, here: &Here) -> Option<Self> {
let directed = !row.is_unicast() && row.forward_to() == *row.address.ip();
Self::check_address(row.address, here).or(directed.then_some(Self::DirectedBroadcast))
}
pub fn check_address(address: SocketAddrV4, here: &Here) -> Option<Self> {
let own = here.own;
let ip = address.ip();
if ip.is_unspecified() && address.port() == 0 {
Some(Self::Unresolved)
} else if ip.is_unspecified() || address.port() == 0 {
Some(Self::Unspecified)
} else if ip.is_broadcast() {
Some(Self::Broadcast)
} else if ip.is_multicast() {
Some(Self::Multicast)
} else if *ip == here.broadcast && here.broadcast != *own.ip() {
Some(Self::DirectedBroadcast)
} else if address == own {
Some(Self::Own)
} else if ip.is_loopback() && !own.ip().is_loopback() {
Some(Self::Loopback)
} else {
None
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Here {
pub own: SocketAddrV4,
pub broadcast: Ipv4Addr,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Unfollowed {
pub address: SocketAddrV4,
pub listed_by: SocketAddrV4,
pub why: NotFollowed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Survey<X> {
pub read: Vec<Surveyed<X>>,
pub unread: Vec<SocketAddrV4>,
pub not_followed: Vec<Unfollowed>,
}
pub async fn survey<X, F, R>(seeds: &[SocketAddrV4], here: Here, read: F) -> Survey<X>
where
F: Fn(SocketAddrV4) -> R,
R: Future<Output = (Probe, X)>,
{
let read = &read;
let mut seen = HashSet::new();
let mut found: Vec<SocketAddrV4> = seeds.iter().copied().filter(|s| seen.insert(*s)).collect();
let mut done: Vec<Surveyed<X>> = Vec::new();
let mut not_followed = Vec::new();
let mut early = BTreeMap::new();
let mut started = 0;
let mut reads = FuturesUnordered::new();
loop {
while reads.len() < SURVEY_AT_ONCE && started < found.len().min(MAX_SURVEY) {
let (index, bbmd) = (started, found[started]);
reads.push(async move { (index, read(bbmd).await) });
started += 1;
}
let Some((index, outcome)) = reads.next().await else {
break;
};
early.insert(index, outcome);
while let Some((probe, extra)) = early.remove(&done.len()) {
let bbmd = found[done.len()];
if let Probe::Table(rows) = &probe {
for row in rows {
if !seen.insert(row.address) {
continue;
}
match NotFollowed::check(row, &here) {
Some(why) => not_followed.push(Unfollowed {
address: row.address,
listed_by: bbmd,
why,
}),
None => found.push(row.address),
}
}
}
done.push(Surveyed { bbmd, probe, extra });
}
}
Survey {
unread: found.split_off(done.len()),
read: done,
not_followed,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn at(last: u8) -> SocketAddrV4 {
SocketAddrV4::new(Ipv4Addr::new(10, 0, last, 1), 47808)
}
fn nowhere() -> Here {
Here {
own: at(250),
broadcast: Ipv4Addr::BROADCAST,
}
}
fn row(address: &str, mask: [u8; 4]) -> BdtRow {
BdtRow {
address: address.parse().unwrap(),
mask: Ipv4Addr::from(mask),
}
}
#[test]
fn a_mask_says_how_peers_forward() {
let unicast = row("10.0.1.1:47808", [255; 4]);
assert!(unicast.is_unicast());
assert_eq!(unicast.forward_to(), Ipv4Addr::new(10, 0, 1, 1));
assert!(unicast.explain().contains("two-hop"));
let directed = row("10.0.2.1:47808", [255, 255, 255, 0]);
assert!(!directed.is_unicast());
assert_eq!(directed.forward_to(), Ipv4Addr::new(10, 0, 2, 255));
assert!(directed.explain().contains("10.0.2.255"));
}
#[test]
fn an_entry_under_thirty_seconds_is_expiring() {
let entry = |remaining| FdtRow {
address: at(9),
ttl: 60,
remaining,
};
assert!(entry(29).expiring());
assert!(!entry(30).expiring());
}
#[test]
fn a_nak_and_a_timeout_sort_apart() {
use bacnet_transport::bvll::BvlcFunction;
let code = BvlcResultCode::READ_BROADCAST_DISTRIBUTION_TABLE_NAK;
assert_eq!(probe_error(&BvlcExchangeError::Nak(code)), Probe::Nak(code));
let silent = BvlcExchangeError::NoReply(Duration::from_secs(3));
assert_eq!(probe_error(&silent), Probe::Unreachable("no answer".into()));
let success = BvlcExchangeError::UnexpectedResponse {
expected: BvlcFunction::READ_BROADCAST_DISTRIBUTION_TABLE_ACK,
got: BvlcFunction::BVLC_RESULT,
};
assert!(matches!(probe_error(&success), Probe::Unreachable(_)));
let local = BvlcExchangeError::Transport(bacnet_types::error::Error::Encoding(
"B/IP management client is stopped".into(),
));
assert_eq!(
probe_error(&local),
Probe::Unreachable("encoding error: B/IP management client is stopped".into())
);
let mac = mac("10.0.0.1:47809".parse().unwrap());
assert_eq!(address_of(&mac), Some("10.0.0.1:47809".parse().unwrap()));
assert_eq!(address_of(&mac[..5]), None);
}
#[tokio::test]
async fn every_seed_is_read_then_what_the_tables_list() {
let tables = |bbmd: SocketAddrV4| match bbmd.ip().octets()[2] {
1 => vec![at(1), at(2), at(3)],
2 => vec![at(2), at(1)],
_ => vec![],
};
let surveyed = survey(&[at(1), at(2), at(1)], nowhere(), |bbmd| async move {
let rows = tables(bbmd)
.into_iter()
.map(|address| BdtRow {
address,
mask: Ipv4Addr::BROADCAST,
})
.collect();
(Probe::Table(rows), bbmd.port())
})
.await;
let order: Vec<SocketAddrV4> = surveyed.read.iter().map(|s| s.bbmd).collect();
assert_eq!(order, [at(1), at(2), at(3)]);
assert!(surveyed.read.iter().all(|s| s.extra == 47808));
assert!(surveyed.unread.is_empty() && surveyed.not_followed.is_empty());
}
#[tokio::test]
async fn addresses_no_read_may_go_to_are_not_followed() {
let here = Here {
own: at(1),
broadcast: Ipv4Addr::new(10, 0, 9, 255),
};
const ONES: [u8; 4] = [255; 4];
const SUBNET: [u8; 4] = [255, 255, 255, 0];
let bad = [
("0.0.0.0:0", ONES, NotFollowed::Unresolved),
("0.0.0.0:47808", ONES, NotFollowed::Unspecified),
("10.0.5.1:0", ONES, NotFollowed::Unspecified),
("255.255.255.255:47808", ONES, NotFollowed::Broadcast),
("224.0.0.1:47808", ONES, NotFollowed::Multicast),
("239.255.255.250:1900", ONES, NotFollowed::Multicast),
("127.0.0.1:53", ONES, NotFollowed::Loopback),
("10.0.9.255:47808", ONES, NotFollowed::DirectedBroadcast),
("10.0.7.255:47808", SUBNET, NotFollowed::DirectedBroadcast),
];
let mut rows: Vec<BdtRow> = bad
.iter()
.map(|(address, mask, _)| BdtRow {
address: address.parse().unwrap(),
mask: Ipv4Addr::from(*mask),
})
.collect();
rows.push(BdtRow {
address: here.own,
mask: Ipv4Addr::BROADCAST,
});
rows.push(rows[0]);
let reads = std::sync::Mutex::new(Vec::new());
let surveyed = survey(&[at(2)], here, |bbmd| {
reads.lock().unwrap().push(bbmd);
let rows = rows.clone();
async move { (Probe::Table(rows), ()) }
})
.await;
assert_eq!(*reads.lock().unwrap(), [at(2)], "only the seed is read");
let mut expected: Vec<Unfollowed> = bad
.iter()
.map(|(address, _, why)| Unfollowed {
address: address.parse().unwrap(),
listed_by: at(2),
why: *why,
})
.collect();
expected.push(Unfollowed {
address: here.own,
listed_by: at(2),
why: NotFollowed::Own,
});
assert_eq!(surveyed.not_followed, expected);
let entry = |address: &str, mask: [u8; 4]| BdtRow {
address: address.parse().unwrap(),
mask: Ipv4Addr::from(mask),
};
assert_eq!(
NotFollowed::check(&entry("10.0.7.255:47808", ONES), &here),
None
);
assert_eq!(
NotFollowed::check(&entry("10.0.7.1:47808", SUBNET), &here),
None
);
let local = Here {
own: "127.0.0.1:47808".parse().unwrap(),
broadcast: Ipv4Addr::new(127, 255, 255, 255),
};
let peer = entry("127.0.0.2:47809", ONES);
assert_eq!(NotFollowed::check(&peer, &local), None);
assert_eq!(
NotFollowed::check_address(local.own, &local),
Some(NotFollowed::Own)
);
assert_eq!(NotFollowed::check_address(at(3), &here), None);
}
#[tokio::test]
async fn a_large_fan_out_reads_at_most_the_cap() {
let next = std::sync::atomic::AtomicU32::new(0);
let peers = |_: SocketAddrV4| -> Vec<BdtRow> {
(0..128)
.map(|_| {
let n = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
BdtRow {
address: SocketAddrV4::new(Ipv4Addr::from(0x0B00_0000 + n), 47808),
mask: Ipv4Addr::BROADCAST,
}
})
.collect()
};
let reads = std::sync::atomic::AtomicUsize::new(0);
let surveyed = survey(&[at(1)], nowhere(), |bbmd| {
reads.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let rows = peers(bbmd);
async move { (Probe::Table(rows), ()) }
})
.await;
assert_eq!(reads.into_inner(), MAX_SURVEY);
assert_eq!(surveyed.read.len(), MAX_SURVEY);
assert!(surveyed.unread.len() > 1_000, "{}", surveyed.unread.len());
let probes: Vec<(SocketAddrV4, Probe)> = surveyed
.read
.iter()
.map(|s| (s.bbmd, s.probe.clone()))
.collect();
let matrix = Matrix::build(&probes);
assert!(matrix.bbmds.len() <= matrix::GRID_CAP);
assert!(matrix.omitted > 1_000, "{}", matrix.omitted);
}
#[tokio::test]
async fn no_seed_reads_nothing() {
let surveyed = survey(&[], nowhere(), |_| async { (Probe::Skipped, ()) }).await;
assert!(surveyed.read.is_empty() && surveyed.unread.is_empty());
}
}