use std::sync::atomic::AtomicU64;
use bacnet_encoding::npdu::{encode_npdu, Npdu, NpduAddress, NpduDecodeError};
use bacnet_transport::port::{DataAttribute, ReceivedNpdu};
use bacnet_types::enums::{NetworkMessageType, RejectMessageReason};
use bacnet_types::MacAddr;
use bytes::{BufMut, BytesMut};
use tokio::sync::{mpsc, Mutex};
use tracing::{debug, warn};
use super::forwarding::forward_unicast;
use super::local_control::{LocalControl, OwnAddresses};
use super::{IngressContext, SendRequest};
use crate::layer::count_drop;
use crate::router_table::{ReachabilityStatus, RouterTable};
pub(super) struct Refused<'a> {
pub send_txs: &'a [mpsc::Sender<SendRequest>],
pub own: &'a OwnAddresses,
pub port_idx: usize,
pub sender_mac: &'a [u8],
pub origin: Option<&'a NpduAddress>,
pub data_attributes: &'a [DataAttribute],
}
impl<'a> Refused<'a> {
pub(super) fn frame(
send_txs: &'a [mpsc::Sender<SendRequest>],
own: &'a OwnAddresses,
port_idx: usize,
received: &'a ReceivedNpdu,
npdu: &'a Npdu,
) -> Self {
Self {
send_txs,
own,
port_idx,
sender_mac: &received.source_mac,
origin: npdu.source.as_ref(),
data_attributes: &received.data_attributes,
}
}
pub(super) fn control(
send_txs: &'a [mpsc::Sender<SendRequest>],
own: &'a OwnAddresses,
ctx: &'a IngressContext,
) -> Self {
Self {
send_txs,
own,
port_idx: ctx.port_idx,
sender_mac: &ctx.source_mac,
origin: ctx.npdu.source.as_ref(),
data_attributes: &ctx.data_attributes,
}
}
fn reject_path(&self) -> Option<(usize, Option<NpduAddress>, &'a [u8])> {
let Some(origin) = self.origin else {
return Some((self.port_idx, None, self.sender_mac));
};
if self.own.contains(origin) {
return None;
}
Some(match self.own.port_on(origin.network) {
Some(port) => (port, None, &origin.mac_address[..]),
None => (self.port_idx, Some(origin.clone()), self.sender_mac),
})
}
}
pub(super) fn route_refusal(
reachability: Option<ReachabilityStatus>,
) -> Option<RejectMessageReason> {
match reachability.unwrap_or(ReachabilityStatus::Reachable) {
ReachabilityStatus::Busy => Some(RejectMessageReason::ROUTER_BUSY),
ReachabilityStatus::Unreachable => Some(RejectMessageReason::NOT_DIRECTLY_CONNECTED),
ReachabilityStatus::Reachable => None,
}
}
pub(super) fn send_reject(
refused: &Refused<'_>,
rejected_network: u16,
reason: RejectMessageReason,
) {
let Some((port, destination, link_mac)) = refused.reject_path() else {
debug!(
network = rejected_network,
"Router refused an NPDU it originated itself; no reject sent"
);
return;
};
let mut payload = BytesMut::with_capacity(3);
payload.put_u8(reason.to_raw());
payload.put_u16(rejected_network);
let reject = Npdu {
is_network_message: true,
message_type: Some(NetworkMessageType::REJECT_MESSAGE_TO_NETWORK.to_raw()),
destination,
hop_count: 255,
payload: payload.freeze(),
..Npdu::default()
};
let mut buf = BytesMut::with_capacity(32);
if let Err(e) = encode_npdu(&mut buf, &reject) {
warn!("Failed to encode Reject-Message NPDU: {e}");
return;
}
if let Err(e) = refused.send_txs[port].try_send(SendRequest::unicast_with_attributes(
buf.freeze(),
MacAddr::from_slice(link_mac),
refused.data_attributes,
)) {
warn!(%e, "Router dropped reject message: output channel full");
}
}
pub(super) fn refuse_address_too_long(
send_txs: &[mpsc::Sender<SendRequest>],
own: &OwnAddresses,
port_idx: usize,
received: &ReceivedNpdu,
refused: &NpduDecodeError,
drops: &AtomicU64,
) {
warn!(error = %refused, port = port_idx, "Router refused an NPDU");
count_drop(drops);
let NpduDecodeError::AddressTooLong { dnet, source, .. } = refused else {
return;
};
if let Some(dnet) = dnet.filter(|&dnet| dnet != 0xFFFF) {
let refused = Refused {
send_txs,
own,
port_idx,
sender_mac: &received.source_mac,
origin: source.as_ref(),
data_attributes: &received.data_attributes,
};
send_reject(&refused, dnet, RejectMessageReason::ADDRESSING_ERROR);
}
}
pub(super) async fn route_received_reject(
table: &Mutex<RouterTable>,
send_txs: &[mpsc::Sender<SendRequest>],
local: &LocalControl,
ctx: &IngressContext,
) {
let dnet = match ctx.npdu.destination.as_ref() {
Some(dest) if !local.is_own_address(dest) => dest.network,
_ => {
local.deliver(ctx);
return;
}
};
if dnet == 0xFFFF || dnet == ctx.port_network {
return;
}
let Some(route) = table.lock().await.lookup(dnet).cloned() else {
return;
};
forward_unicast(
send_txs,
&route,
ctx.port_network,
&ctx.source_mac,
ctx.npdu.clone(),
ctx.port_idx,
&ctx.data_attributes,
);
}
#[cfg(test)]
#[path = "reject_tests.rs"]
mod tests;