pub(crate) mod discovery;
pub(crate) mod matrix;
pub(crate) mod routers;
use std::net::{Ipv4Addr, SocketAddrV4};
use std::time::Duration;
use bacnet_transport::bbmd::{BdtEntry, FdtEntryWire};
use bacnet_transport::bip::{BvlcExchangeError, ForeignRegistrationSnapshot};
use self::matrix::Probe;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum NetworkCommand {
Tables {
bbmd: SocketAddrV4,
},
Survey {
seed: SocketAddrV4,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Foreign {
pub(crate) bbmd: SocketAddrV4,
pub(crate) ttl: u16,
pub(crate) renewal: Duration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct BdtRow {
pub(crate) address: SocketAddrV4,
pub(crate) mask: Ipv4Addr,
}
impl BdtRow {
pub(crate) fn is_unicast(&self) -> bool {
self.mask == Ipv4Addr::BROADCAST
}
pub(crate) fn forward_to(&self) -> Ipv4Addr {
Ipv4Addr::from(self.address.ip().to_bits() | !self.mask.to_bits())
}
pub(crate) 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(crate) struct FdtRow {
pub(crate) address: SocketAddrV4,
pub(crate) ttl: u16,
pub(crate) remaining: u16,
}
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(crate) fn own_entry(entries: &[FdtRow], local: SocketAddrV4) -> Option<usize> {
entries.iter().position(|entry| {
entry.address.port() == local.port()
&& (local.ip().is_unspecified() || entry.address.ip() == local.ip())
})
}
pub(crate) 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(crate) 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]),
))
}
fn unreachable_reason(error: &BvlcExchangeError) -> String {
match error {
BvlcExchangeError::NoReply(_) => "no answer".into(),
other => other.to_string(),
}
}
pub(crate) fn probe_error(error: &BvlcExchangeError) -> Probe {
match error {
BvlcExchangeError::Nak(code) => Probe::Nak(*code),
other => Probe::Unreachable(unreachable_reason(other)),
}
}
pub(crate) fn read_error(error: &BvlcExchangeError) -> String {
match error {
BvlcExchangeError::Nak(_) => format!("refused: {error}"),
BvlcExchangeError::UnexpectedResponse { .. } => format!("unexpected reply: {error}"),
other => format!("unreachable: {}", unreachable_reason(other)),
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum NetworkEvent {
Registration(ForeignRegistrationSnapshot),
Tables {
bbmd: SocketAddrV4,
local: SocketAddrV4,
bdt: Result<Vec<BdtRow>, String>,
fdt: Result<Vec<FdtRow>, String>,
},
Survey {
seed: SocketAddrV4,
probes: Vec<(SocketAddrV4, Probe)>,
},
Refused {
command: NetworkCommand,
reason: String,
},
}
#[cfg(test)]
mod tests {
use super::*;
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 our_entry_is_the_one_at_our_address() {
let entry = |address: &str| FdtRow {
address: address.parse().unwrap(),
ttl: 60,
remaining: 90,
};
let fdt = [entry("10.0.0.9:47808"), entry("10.0.0.5:50000")];
assert_eq!(own_entry(&fdt, "10.0.0.5:50000".parse().unwrap()), Some(1));
assert_eq!(own_entry(&fdt, "0.0.0.0:50000".parse().unwrap()), Some(1));
assert_eq!(own_entry(&fdt, "192.168.1.5:50000".parse().unwrap()), None);
}
#[test]
fn a_nak_and_a_timeout_sort_apart() {
use bacnet_transport::bvll::{BvlcFunction, BvlcResultCode};
let code = BvlcResultCode::READ_BROADCAST_DISTRIBUTION_TABLE_NAK;
let nak = BvlcExchangeError::Nak(code);
assert_eq!(probe_error(&nak), Probe::Nak(code));
assert_eq!(
read_error(&nak),
"refused: BVLC-Result NAK READ_BROADCAST_DISTRIBUTION_TABLE_NAK (0x0020)"
);
let silent = BvlcExchangeError::NoReply(Duration::from_secs(3));
assert_eq!(probe_error(&silent), Probe::Unreachable("no answer".into()));
assert_eq!(read_error(&silent), "unreachable: no answer");
let success = BvlcExchangeError::UnexpectedResponse {
expected: BvlcFunction::READ_BROADCAST_DISTRIBUTION_TABLE_ACK,
got: BvlcFunction::BVLC_RESULT,
};
assert!(matches!(probe_error(&success), Probe::Unreachable(_)));
assert!(read_error(&success).starts_with("unexpected reply: "));
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())
);
assert!(read_error(&local).starts_with("unreachable: "));
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);
}
}