use super::*;
use bacnet_encoding::npdu::{decode_npdu, NpduDecodeError};
use std::collections::HashMap;
use std::sync::Mutex;
use std::task::Poll;
use tokio::sync::mpsc;
use tracing::{debug, warn};
const QUEUE_CAPACITY: usize = 256;
const APDU_SOURCE_CAPACITY: usize = 16;
type AdmissionSource = (Option<u16>, MacAddr);
fn apdu_source(apdu: &ReceivedApdu) -> AdmissionSource {
(apdu.ingress_network, apdu.source_mac.clone())
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct QueueAdmissionSnapshot {
pub current_depth: usize,
pub high_water: usize,
pub full_drops: u64,
pub fairness_drops: u64,
pub closed_drops: u64,
}
#[derive(Debug, Clone, Default)]
pub struct QueueAdmissionCounters(Arc<Mutex<QueueAdmissionState>>);
#[derive(Debug, Default)]
struct QueueAdmissionState {
snapshot: QueueAdmissionSnapshot,
queued_by_source: HashMap<AdmissionSource, usize>,
}
impl QueueAdmissionState {
fn dequeued(&mut self, source: Option<AdmissionSource>) {
self.snapshot.current_depth -= 1;
if let Some(ref source) = source {
let count = self
.queued_by_source
.get_mut(source)
.expect("queued APDU source must be accounted");
*count -= 1;
if *count == 0 {
self.queued_by_source.remove(source);
}
self.assert_source_depth();
}
}
fn assert_source_depth(&self) {
debug_assert!(self.queued_by_source.len() <= self.snapshot.current_depth);
debug_assert!(self.snapshot.current_depth <= QUEUE_CAPACITY);
debug_assert_eq!(
self.queued_by_source.values().sum::<usize>(),
self.snapshot.current_depth
);
debug_assert!(self
.queued_by_source
.values()
.all(|count| (1..=APDU_SOURCE_CAPACITY).contains(count)));
}
}
impl QueueAdmissionCounters {
pub fn snapshot(&self) -> QueueAdmissionSnapshot {
self.0
.lock()
.expect("queue admission counters poisoned")
.snapshot
}
}
#[derive(Debug)]
pub struct AdmissionReceiver<T> {
rx: mpsc::Receiver<T>,
counters: QueueAdmissionCounters,
source_key: Option<fn(&T) -> AdmissionSource>,
}
impl<T> AdmissionReceiver<T> {
pub(crate) fn channel(
track_depth: bool,
) -> (
AdmissionSender<T>,
mpsc::Receiver<T>,
QueueAdmissionCounters,
) {
let (tx, rx) = AdmissionSender::channel(track_depth);
let counters = tx.counters.clone();
(tx, rx, counters)
}
pub(crate) fn from_parts(rx: mpsc::Receiver<T>, counters: QueueAdmissionCounters) -> Self {
Self {
rx,
counters,
source_key: None,
}
}
pub fn counters(&self) -> QueueAdmissionCounters {
self.counters.clone()
}
pub async fn recv(&mut self) -> Option<T> {
std::future::poll_fn(|cx| {
let mut state = self
.counters
.0
.lock()
.expect("queue admission counters poisoned");
let result = self.rx.poll_recv(cx);
if let Poll::Ready(Some(item)) = &result {
state.dequeued(self.source_key.map(|source_key| source_key(item)));
}
result
})
.await
}
pub fn try_recv(&mut self) -> Result<T, mpsc::error::TryRecvError> {
let mut state = self
.counters
.0
.lock()
.expect("queue admission counters poisoned");
let result = self.rx.try_recv();
if let Ok(item) = &result {
state.dequeued(self.source_key.map(|source_key| source_key(item)));
}
result
}
pub fn close(&mut self) {
let _snapshot = self
.counters
.0
.lock()
.expect("queue admission counters poisoned");
self.rx.close();
}
}
impl AdmissionReceiver<ReceivedApdu> {
pub(crate) fn from_apdu_parts(
rx: mpsc::Receiver<ReceivedApdu>,
counters: QueueAdmissionCounters,
) -> Self {
Self {
rx,
counters,
source_key: Some(apdu_source),
}
}
}
impl<T> Drop for AdmissionReceiver<T> {
fn drop(&mut self) {
let mut state = self
.counters
.0
.lock()
.expect("queue admission counters poisoned");
self.rx.close();
state.snapshot.current_depth = 0;
state.queued_by_source = HashMap::new();
}
}
pub(crate) struct AdmissionSender<T> {
tx: mpsc::Sender<T>,
counters: QueueAdmissionCounters,
track_depth: bool,
}
impl<T> Clone for AdmissionSender<T> {
fn clone(&self) -> Self {
Self {
tx: self.tx.clone(),
counters: self.counters.clone(),
track_depth: self.track_depth,
}
}
}
impl<T> AdmissionSender<T> {
fn channel(track_depth: bool) -> (Self, mpsc::Receiver<T>) {
let (tx, rx) = mpsc::channel(QUEUE_CAPACITY);
(
Self {
tx,
counters: QueueAdmissionCounters::default(),
track_depth,
},
rx,
)
}
pub(crate) fn try_send(&self, item: T) -> Result<(), mpsc::error::TrySendError<T>> {
self.try_send_from(item, None)
}
fn try_send_from(
&self,
item: T,
source: Option<AdmissionSource>,
) -> Result<(), mpsc::error::TrySendError<T>> {
let mut state = self
.counters
.0
.lock()
.expect("queue admission counters poisoned");
if !self.tx.is_closed()
&& source.as_ref().is_some_and(|source| {
state.queued_by_source.get(source).copied().unwrap_or(0) >= APDU_SOURCE_CAPACITY
})
{
state.snapshot.fairness_drops = state.snapshot.fairness_drops.saturating_add(1);
return Err(mpsc::error::TrySendError::Full(item));
}
let result = self.tx.try_send(item);
match &result {
Ok(()) if self.track_depth => {
state.snapshot.current_depth += 1;
state.snapshot.high_water =
state.snapshot.high_water.max(state.snapshot.current_depth);
if let Some(source) = source {
*state.queued_by_source.entry(source).or_default() += 1;
state.assert_source_depth();
}
}
Ok(()) => {}
Err(mpsc::error::TrySendError::Full(_)) => {
state.snapshot.full_drops = state.snapshot.full_drops.saturating_add(1);
}
Err(mpsc::error::TrySendError::Closed(_)) => {
state.snapshot.closed_drops = state.snapshot.closed_drops.saturating_add(1);
}
}
result
}
}
impl AdmissionSender<ReceivedApdu> {
pub(crate) fn try_send_apdu(
&self,
apdu: ReceivedApdu,
) -> Result<(), mpsc::error::TrySendError<()>> {
let source = self.track_depth.then(|| apdu_source(&apdu));
self.try_send_from(apdu, source)
.map_err(|error| match error {
mpsc::error::TrySendError::Full(_) => mpsc::error::TrySendError::Full(()),
mpsc::error::TrySendError::Closed(_) => mpsc::error::TrySendError::Closed(()),
})
}
}
impl<T: TransportPort + 'static> NetworkLayer<T> {
pub fn enable_network_control_receiver(
&mut self,
) -> Result<mpsc::Receiver<ReceivedNetworkControl>, Error> {
self.enable_network_control(false).map(|(rx, _)| rx)
}
pub fn enable_network_control_receiver_with_admission(
&mut self,
) -> Result<AdmissionReceiver<ReceivedNetworkControl>, Error> {
let (rx, counters) = self.enable_network_control(true)?;
Ok(AdmissionReceiver::from_parts(rx, counters))
}
fn enable_network_control(
&mut self,
track_depth: bool,
) -> Result<
(
mpsc::Receiver<ReceivedNetworkControl>,
QueueAdmissionCounters,
),
Error,
> {
if self.dispatch_task.is_some() {
return Err(Error::Encoding(
"network-control receiver must be enabled before NetworkLayer::start".into(),
));
}
if self.network_control_tx.is_some() {
return Err(Error::Encoding(
"network-control receiver is already enabled".into(),
));
}
let (tx, rx) = AdmissionSender::channel(track_depth);
let counters = tx.counters.clone();
self.network_control_tx = Some(tx);
Ok((rx, counters))
}
pub async fn start(&mut self) -> Result<mpsc::Receiver<ReceivedApdu>, Error> {
self.start_dispatch(false).await.map(|(rx, _)| rx)
}
pub async fn start_with_admission(&mut self) -> Result<AdmissionReceiver<ReceivedApdu>, Error> {
let (rx, counters) = self.start_dispatch(true).await?;
Ok(AdmissionReceiver::from_apdu_parts(rx, counters))
}
async fn start_dispatch(
&mut self,
track_depth: bool,
) -> Result<(mpsc::Receiver<ReceivedApdu>, QueueAdmissionCounters), Error> {
let mut npdu_rx = self.transport.start().await?;
let mut network_control_tx = self.network_control_tx.take();
let network_control_ingress_sequence = Arc::clone(&self.network_control_ingress_sequence);
let address_length_drops = Arc::clone(&self.address_length_drops);
let global_broadcast_dadr_drops = Arc::clone(&self.global_broadcast_dadr_drops);
let broadcast_pdu_type_drops = Arc::clone(&self.broadcast_pdu_type_drops);
let (apdu_tx, apdu_rx) = AdmissionSender::channel(track_depth);
let counters = apdu_tx.counters.clone();
let dispatch_task = tokio::spawn(async move {
while let Some(received) = npdu_rx.recv().await {
if !link_source_fits(&received.source_mac, &address_length_drops) {
continue;
}
match decode_npdu(received.npdu.clone()) {
Ok(npdu) => {
if !destination_is_coherent(
npdu.destination.as_ref(),
&global_broadcast_dadr_drops,
) || !broadcast_carries_unconfirmed(&npdu, &broadcast_pdu_type_drops)
{
continue;
}
if npdu.is_network_message {
if let Some(tx) = network_control_tx.as_ref() {
let ingress_sequence = next_ingress_sequence(
network_control_ingress_sequence.as_ref(),
);
let control = ReceivedNetworkControl {
npdu,
source_mac: received.source_mac,
link_layer_group: received.link_layer_group,
data_attributes: received.data_attributes,
provenance: received.provenance,
ingress_sequence,
};
match tx.try_send(control) {
Ok(()) | Err(mpsc::error::TrySendError::Full(_)) => {}
Err(mpsc::error::TrySendError::Closed(_)) => {
network_control_tx = None;
debug!("Network-control receiver closed; resuming discard behavior");
}
}
} else {
debug!(
message_type = npdu.message_type,
"Ignoring network layer message (non-router mode)"
);
}
continue;
}
if let Some(ref dest) = npdu.destination {
if dest.network != 0xFFFF {
debug!(
dnet = dest.network,
"Discarding routed message (non-router)"
);
continue;
}
}
let source_network = npdu.source.clone();
let is_group =
is_group_delivery(received.link_layer_group, npdu.destination.as_ref());
let global_broadcast = is_global_broadcast(npdu.destination.as_ref());
let apdu = ReceivedApdu {
direct_response: received.direct_response.clone(),
apdu: npdu.payload,
source_mac: received.source_mac,
ingress_network: None,
source_network,
link_layer_group: received.link_layer_group,
is_group,
global_broadcast,
data_attributes: received.data_attributes,
provenance: received.provenance,
reply_tx: received.reply_tx,
};
match apdu_tx.try_send_apdu(apdu) {
Ok(()) | Err(mpsc::error::TrySendError::Full(_)) => {}
Err(mpsc::error::TrySendError::Closed(_)) => break,
}
}
Err(e @ NpduDecodeError::AddressTooLong { .. }) => {
count_drop(&address_length_drops);
warn!(error = %e, "Dropping NPDU with an over-long address");
}
Err(e) => {
warn!(error = %e, "Failed to decode NPDU");
}
}
}
});
self.dispatch_task = Some(dispatch_task);
Ok((apdu_rx, counters))
}
}
pub(crate) fn link_source_fits(source_mac: &[u8], drops: &AtomicU64) -> bool {
if source_mac.len() <= NpduAddress::MAX_MAC_LEN {
return true;
}
count_drop(drops);
warn!(
octets = source_mac.len(),
limit = NpduAddress::MAX_MAC_LEN,
"Dropping a frame whose link-layer source MAC is over the limit"
);
false
}
pub(crate) fn destination_is_coherent(
destination: Option<&NpduAddress>,
drops: &AtomicU64,
) -> bool {
let Some(destination) = destination else {
return true;
};
if destination.network != 0xFFFF || destination.mac_address.is_empty() {
return true;
}
count_drop(drops);
warn!(
dlen = destination.mac_address.len(),
"Dropping a global broadcast NPDU that also names a DADR"
);
false
}
pub(crate) fn broadcast_carries_unconfirmed(npdu: &Npdu, drops: &AtomicU64) -> bool {
let Some(destination) = npdu.destination.as_ref() else {
return true;
};
let broadcast = destination.network == 0xFFFF || destination.mac_address.is_empty();
let unconfirmed = npdu
.payload
.first()
.is_some_and(|&first| PduType::from_raw(first >> 4) == PduType::UNCONFIRMED_REQUEST);
if npdu.is_network_message || !broadcast || unconfirmed {
return true;
}
count_drop(drops);
warn!(
dnet = destination.network,
"Dropping a broadcast NPDU whose APDU isn't an Unconfirmed-Request"
);
false
}
#[cfg(test)]
#[path = "layer_fairness_tests.rs"]
mod fairness_tests;