mod local_delivery;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use bacnet_encoding::npdu::{encode_npdu, Npdu, NpduAddress};
use bacnet_transport::port::{DataAttribute, TransportPort};
use bacnet_types::enums::NetworkMessageType;
use bacnet_types::error::Error;
use bacnet_types::MacAddr;
use bytes::{BufMut, Bytes, BytesMut};
use tokio::sync::{mpsc, Mutex};
use tokio::task::JoinHandle;
use tracing::{debug, warn};
use crate::layer::{AdmissionReceiver, QueueAdmissionCounters, ReceivedApdu};
use crate::layer::{AdmissionSender, ReceivedNetworkControl};
use crate::router_table::RouterTable;
use bacnet_transport::port::TransportProvenance;
mod control_messages;
pub mod control_policy;
mod dispatch;
mod forwarding;
mod local_control;
mod options;
mod reject;
pub use control_policy::{ControlAuthContext, ControlAuthorizer, ControlClass};
pub use control_policy::{ControlDecisionCounters, ControlGate, ControlPolicy};
pub use control_policy::{ControlServiceCounters, ControlTrust};
use dispatch::PortDispatch;
use local_control::{LocalControl, OwnAddresses};
pub use options::{LocalApduReceiver, NetworkControlReceiver, RouterOptions, StartedRouter};
#[derive(Debug)]
enum SendRequest {
Unicast {
npdu: Bytes,
mac: MacAddr,
data_attributes: Vec<DataAttribute>,
},
Broadcast {
npdu: Bytes,
data_attributes: Vec<DataAttribute>,
},
}
impl SendRequest {
fn broadcast(npdu: Bytes) -> Self {
Self::broadcast_with_attributes(npdu, &[])
}
fn unicast_with_attributes(
npdu: Bytes,
mac: MacAddr,
data_attributes: &[DataAttribute],
) -> Self {
Self::Unicast {
npdu,
mac,
data_attributes: data_attributes.to_vec(),
}
}
fn broadcast_with_attributes(npdu: Bytes, data_attributes: &[DataAttribute]) -> Self {
Self::Broadcast {
npdu,
data_attributes: data_attributes.to_vec(),
}
}
}
const DISCOVERY_COOLDOWN: Duration = Duration::from_secs(5);
const DISCOVERY_MAX_AGE: Duration = Duration::from_secs(30);
const DISCOVERY_MAX_PENDING: usize = 256;
#[derive(Debug, Default)]
pub(crate) struct DiscoveryTracker {
last_solicited: HashMap<u16, Instant>,
cancelled: bool,
}
impl DiscoveryTracker {
pub(crate) fn should_solicit_at(&mut self, dnet: u16, now: Instant) -> bool {
if self.cancelled || dnet == 0 || dnet == 0xFFFF {
return false;
}
if let Some(last) = self.last_solicited.get(&dnet) {
if now.duration_since(*last) < DISCOVERY_COOLDOWN {
return false;
}
} else if self.last_solicited.len() >= DISCOVERY_MAX_PENDING {
return false;
}
self.last_solicited.insert(dnet, now);
true
}
pub(crate) fn should_solicit(&mut self, dnet: u16) -> bool {
self.should_solicit_at(dnet, Instant::now())
}
pub(crate) fn expire_at(&mut self, now: Instant) -> Vec<u16> {
let mut out: Vec<u16> = self
.last_solicited
.iter()
.filter(|(_, last)| now.duration_since(**last) > DISCOVERY_MAX_AGE)
.map(|(net, _)| *net)
.collect();
out.sort_unstable();
for net in &out {
self.last_solicited.remove(net);
}
out
}
pub(crate) fn cancel(&mut self) {
self.cancelled = true;
self.last_solicited.clear();
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.last_solicited.len()
}
#[cfg(test)]
pub(crate) fn is_cancelled(&self) -> bool {
self.cancelled
}
}
#[derive(Debug, Clone)]
pub(super) struct IngressContext {
pub port_idx: usize,
pub port_network: u16,
pub source_mac: MacAddr,
pub link_layer_group: bool,
pub data_attributes: Vec<DataAttribute>,
pub provenance: TransportProvenance,
pub npdu: Npdu,
}
#[cfg(test)]
impl IngressContext {
pub(super) fn test_local(
port_idx: usize,
port_network: u16,
source_mac: &[u8],
npdu: Npdu,
) -> Self {
Self {
port_idx,
port_network,
source_mac: MacAddr::from_slice(source_mac),
link_layer_group: true,
data_attributes: Vec::new(),
provenance: TransportProvenance::unverified(),
npdu,
}
}
#[cfg(test)]
pub(super) fn test_local_with_provenance(
port_idx: usize,
port_network: u16,
source_mac: &[u8],
npdu: Npdu,
provenance: TransportProvenance,
) -> Self {
Self {
port_idx,
port_network,
source_mac: MacAddr::from_slice(source_mac),
link_layer_group: true,
data_attributes: Vec::new(),
provenance,
npdu,
}
}
}
pub struct RouterPort<T: TransportPort> {
pub transport: T,
pub network_number: u16,
}
pub struct BACnetRouter {
table: Arc<Mutex<RouterTable>>,
discovery: Arc<Mutex<DiscoveryTracker>>,
control: Arc<control_policy::ControlGate>,
dispatch_tasks: Vec<JoinHandle<()>>,
sender_tasks: Vec<JoinHandle<()>>,
aging_task: Option<JoinHandle<()>>,
address_length_drops: Arc<AtomicU64>,
global_broadcast_dadr_drops: Arc<AtomicU64>,
broadcast_pdu_type_drops: Arc<AtomicU64>,
network_control_ingress_sequence: Arc<AtomicU64>,
}
impl BACnetRouter {
pub async fn start<T, A, C>(
ports: Vec<RouterPort<T>>,
options: RouterOptions<A, C>,
) -> Result<StartedRouter<A, C>, Error>
where
T: TransportPort + 'static,
A: LocalApduReceiver,
C: NetworkControlReceiver,
{
let (control_tx, network_control) = if options.wants_network_control() {
let (tx, rx, counters) = AdmissionReceiver::channel(C::TRACKED);
(Some(tx), Some(C::from_queue(rx, counters)))
} else {
(None, None)
};
let gate = Arc::new(options.control_gate());
let (router, rx, counters) =
Self::start_dispatch(ports, A::TRACKED, gate, control_tx).await?;
Ok(StartedRouter {
router,
apdus: A::from_queue(rx, counters),
network_control,
})
}
pub fn control_snapshot(&self) -> control_policy::ControlDecisionCounters {
self.control.snapshot()
}
pub fn address_length_drops(&self) -> u64 {
self.address_length_drops.load(Ordering::Relaxed)
}
pub fn global_broadcast_dadr_drops(&self) -> u64 {
self.global_broadcast_dadr_drops.load(Ordering::Relaxed)
}
pub fn broadcast_pdu_type_drops(&self) -> u64 {
self.broadcast_pdu_type_drops.load(Ordering::Relaxed)
}
async fn start_dispatch<T: TransportPort + 'static>(
mut ports: Vec<RouterPort<T>>,
track_depth: bool,
control: Arc<control_policy::ControlGate>,
network_control: Option<AdmissionSender<ReceivedNetworkControl>>,
) -> Result<(Self, mpsc::Receiver<ReceivedApdu>, QueueAdmissionCounters), Error> {
let mut table = RouterTable::new();
{
let mut seen = std::collections::HashSet::new();
for port in &ports {
if !seen.insert(port.network_number) {
return Err(Error::Encoding(format!(
"Duplicate network number {} in router ports",
port.network_number
)));
}
}
}
for (idx, port) in ports.iter().enumerate() {
table.add_direct(port.network_number, idx);
}
let table = Arc::new(Mutex::new(table));
let discovery = Arc::new(Mutex::new(DiscoveryTracker::default()));
let (local_tx, local_rx, counters) = AdmissionReceiver::channel(track_depth);
let mut port_receivers = Vec::new();
let mut send_txs: Vec<mpsc::Sender<SendRequest>> = Vec::new();
let mut sender_tasks = Vec::new();
let mut port_networks = Vec::new();
let mut port_local_macs = Vec::new();
for port in &mut ports {
let rx = port.transport.start().await?;
port_receivers.push(rx);
port_networks.push(port.network_number);
port_local_macs.push(MacAddr::from_slice(port.transport.local_mac()));
}
for port in ports {
let (send_tx, mut send_rx) = mpsc::channel::<SendRequest>(256);
send_txs.push(send_tx);
let transport = port.transport;
let task = tokio::spawn(async move {
while let Some(req) = send_rx.recv().await {
match req {
SendRequest::Unicast {
npdu,
mac,
data_attributes,
} => {
if let Err(e) = transport
.send_unicast_with_data_attributes(&npdu, &mac, &data_attributes)
.await
{
warn!(error = %e, "Router send_unicast failed");
}
}
SendRequest::Broadcast {
npdu,
data_attributes,
} => {
if let Err(e) = transport
.send_broadcast_with_data_attributes(&npdu, &data_attributes)
.await
{
warn!(error = %e, "Router send_broadcast failed");
}
}
}
}
});
sender_tasks.push(task);
}
let send_txs = Arc::new(send_txs);
for (port_idx, tx) in send_txs.iter().enumerate() {
let other_networks: Vec<u16> = port_networks
.iter()
.enumerate()
.filter(|(idx, _)| *idx != port_idx)
.map(|(_, net)| *net)
.collect();
if other_networks.is_empty() {
continue;
}
let mut payload = BytesMut::with_capacity(other_networks.len() * 2);
for net in &other_networks {
payload.put_u16(*net);
}
let payload_len = payload.len();
let response = Npdu {
is_network_message: true,
message_type: Some(NetworkMessageType::I_AM_ROUTER_TO_NETWORK.to_raw()),
payload: payload.freeze(),
..Npdu::default()
};
let mut buf = BytesMut::with_capacity(8 + payload_len);
if let Err(e) = encode_npdu(&mut buf, &response) {
warn!("Failed to encode I-Am-Router NPDU: {e}");
continue;
}
if let Err(e) = tx.try_send(SendRequest::broadcast(buf.freeze())) {
warn!(%e, "Router dropped I-Am-Router announcement: output channel full");
}
}
let mut dispatch_tasks = Vec::new();
let address_length_drops = Arc::new(AtomicU64::new(0));
let global_broadcast_dadr_drops = Arc::new(AtomicU64::new(0));
let broadcast_pdu_type_drops = Arc::new(AtomicU64::new(0));
let network_control_ingress_sequence = Arc::new(AtomicU64::new(0));
let local_control = Arc::new(LocalControl::new(
OwnAddresses::new(
port_networks
.iter()
.zip(&port_local_macs)
.map(|(&network, mac)| NpduAddress {
network,
mac_address: mac.clone(),
})
.collect(),
),
network_control,
Arc::clone(&network_control_ingress_sequence),
));
for (port_idx, rx) in port_receivers.into_iter().enumerate() {
let port = PortDispatch {
table: Arc::clone(&table),
discovery: Arc::clone(&discovery),
control: Arc::clone(&control),
local_control: Arc::clone(&local_control),
address_length_drops: Arc::clone(&address_length_drops),
global_broadcast_dadr_drops: Arc::clone(&global_broadcast_dadr_drops),
broadcast_pdu_type_drops: Arc::clone(&broadcast_pdu_type_drops),
local_tx: local_tx.clone(),
send_txs: Arc::clone(&send_txs),
port_idx,
port_network: port_networks[port_idx],
local_mac: port_local_macs[port_idx].clone(),
};
dispatch_tasks.push(tokio::spawn(port.run(rx)));
}
let aging_table = Arc::clone(&table);
let aging_discovery = Arc::clone(&discovery);
let aging_task = tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(60));
let max_age = Duration::from_secs(300); loop {
interval.tick().await;
let now = Instant::now();
let mut tbl = aging_table.lock().await;
let purged = tbl.purge_stale_at(now, max_age);
tbl.clear_expired_busy();
let expired_pending = tbl.expire_pending_at(now);
drop(tbl);
let mut disc = aging_discovery.lock().await;
let expired_discovery = disc.expire_at(now);
drop(disc);
for net in purged {
debug!(network = net, "Purged stale route");
}
for net in expired_pending {
debug!(network = net, "Expired hardened pending challenger");
}
for net in expired_discovery {
debug!(network = net, "Expired discovery solicitation");
}
}
});
Ok((
Self {
table,
discovery,
control,
dispatch_tasks,
sender_tasks,
aging_task: Some(aging_task),
address_length_drops,
global_broadcast_dadr_drops,
broadcast_pdu_type_drops,
network_control_ingress_sequence,
},
local_rx,
counters,
))
}
pub fn table(&self) -> &Arc<Mutex<RouterTable>> {
&self.table
}
pub async fn stop(&mut self) {
for task in self.dispatch_tasks.drain(..) {
task.abort();
let _ = task.await;
}
for task in self.sender_tasks.drain(..) {
task.abort();
let _ = task.await;
}
if let Some(task) = self.aging_task.take() {
task.abort();
let _ = task.await;
}
self.discovery.lock().await.cancel();
}
}
#[cfg(test)]
mod admission_tests;
#[cfg(test)]
mod busy_scope_tests;
#[cfg(test)]
mod claim_tests;
#[cfg(test)]
mod envelope_control_tests;
#[cfg(test)]
mod envelope_discovery_tests;
#[cfg(test)]
mod envelope_harness;
#[cfg(test)]
mod ingress_harness;
#[cfg(test)]
mod init_routing_table_tests;
#[cfg(test)]
mod rb06_convergence_tests;
#[cfg(test)]
mod rb09_control_policy_tests;
#[cfg(test)]
mod tests;