use std::sync::atomic::AtomicU64;
use std::sync::Arc;
use bacnet_encoding::npdu::{decode_npdu, encode_npdu, Npdu, 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::warn;
use super::control_messages::handle_network_message;
use super::control_policy::ControlGate;
use super::forwarding::{forward_broadcast, forward_unicast};
use super::local_control::LocalControl;
use super::reject::{refuse_address_too_long, route_refusal, send_reject, Refused};
use super::{local_delivery, DiscoveryTracker, IngressContext, SendRequest};
use crate::layer::{
broadcast_carries_unconfirmed, destination_is_coherent, is_group_delivery, link_source_fits,
};
use crate::layer::{AdmissionSender, ReceivedApdu};
use crate::router_table::{ReachabilityStatus, RouteEntry, RouterTable};
pub(super) struct PortDispatch {
pub table: Arc<Mutex<RouterTable>>,
pub discovery: Arc<Mutex<DiscoveryTracker>>,
pub control: Arc<ControlGate>,
pub local_control: Arc<LocalControl>,
pub address_length_drops: Arc<AtomicU64>,
pub global_broadcast_dadr_drops: Arc<AtomicU64>,
pub broadcast_pdu_type_drops: Arc<AtomicU64>,
pub local_tx: AdmissionSender<ReceivedApdu>,
pub send_txs: Arc<Vec<mpsc::Sender<SendRequest>>>,
pub port_idx: usize,
pub port_network: u16,
pub local_mac: MacAddr,
}
impl PortDispatch {
pub(super) async fn run(self, mut rx: mpsc::Receiver<ReceivedNpdu>) {
while let Some(received) = rx.recv().await {
self.dispatch(received).await;
}
}
async fn dispatch(&self, received: ReceivedNpdu) {
if !link_source_fits(&received.source_mac, &self.address_length_drops) {
return;
}
let npdu = match decode_npdu(received.npdu.clone()) {
Ok(npdu) => npdu,
Err(e @ NpduDecodeError::AddressTooLong { .. }) => {
refuse_address_too_long(
&self.send_txs,
self.local_control.addresses(),
self.port_idx,
&received,
&e,
&self.address_length_drops,
);
return;
}
Err(e) => {
warn!(error = %e, port = self.port_idx, "Router decode failed");
return;
}
};
if !destination_is_coherent(npdu.destination.as_ref(), &self.global_broadcast_dadr_drops)
|| !broadcast_carries_unconfirmed(&npdu, &self.broadcast_pdu_type_drops)
{
return;
}
if npdu.is_network_message {
let ctx = IngressContext {
port_idx: self.port_idx,
port_network: self.port_network,
source_mac: received.source_mac.clone(),
link_layer_group: received.link_layer_group,
data_attributes: received.data_attributes.clone(),
provenance: received.provenance,
npdu,
};
dispatch_network_message(
&self.table,
&self.discovery,
&self.send_txs,
&ctx,
&self.control,
&self.local_control,
)
.await;
return;
}
let Some(dest_net) = npdu.destination.as_ref().map(|dest| dest.network) else {
let is_group = is_group_delivery(received.link_layer_group, None);
self.deliver(received, npdu, is_group);
return;
};
if dest_net == 0xFFFF {
forward_broadcast(
&self.send_txs,
self.port_idx,
self.port_network,
&received.source_mac,
&npdu,
&received.data_attributes,
);
self.deliver(received, npdu, true);
return;
}
let (route, reachability) = lookup_route(&self.table, dest_net).await;
let refused = || {
Refused::frame(
&self.send_txs,
self.local_control.addresses(),
self.port_idx,
&received,
&npdu,
)
};
let Some(route) = route else {
refuse_unknown(&self.discovery, &refused(), dest_net).await;
return;
};
if let Some(reason) = route_refusal(reachability) {
send_reject(&refused(), dest_net, reason);
return;
}
if route.port_index == self.port_idx && route.directly_connected {
let dest_mac = npdu
.destination
.as_ref()
.map(|d| &d.mac_address[..])
.unwrap_or(&[]);
if dest_mac == &self.local_mac[..] {
self.deliver(received, npdu, false);
return;
}
if dest_mac.is_empty() {
self.deliver(received.clone(), npdu.clone(), true);
}
}
forward_unicast(
&self.send_txs,
&route,
self.port_network,
&received.source_mac,
npdu,
self.port_idx,
&received.data_attributes,
);
}
fn deliver(&self, received: ReceivedNpdu, npdu: Npdu, is_group: bool) {
let apdu = local_delivery::application(received, npdu, self.port_network, is_group);
let _ = self.local_tx.try_send_apdu(apdu);
}
}
pub(super) async fn dispatch_network_message(
table: &Arc<Mutex<RouterTable>>,
discovery: &Arc<Mutex<DiscoveryTracker>>,
send_txs: &[mpsc::Sender<SendRequest>],
ctx: &IngressContext,
control: &ControlGate,
local: &LocalControl,
) {
let msg_type = match ctx.npdu.message_type {
Some(t) => t,
None => return,
};
if msg_type == NetworkMessageType::WHAT_IS_NETWORK_NUMBER.to_raw()
|| msg_type == NetworkMessageType::NETWORK_NUMBER_IS.to_raw()
|| msg_type == NetworkMessageType::REJECT_MESSAGE_TO_NETWORK.to_raw()
{
handle_network_message(table, send_txs, ctx, control, local).await;
return;
}
let directed_elsewhere = match ctx.npdu.destination.as_ref() {
Some(dest) => dest.network != 0xFFFF && dest.network != ctx.port_network,
None => false,
};
if !directed_elsewhere {
handle_network_message(table, send_txs, ctx, control, local).await;
return;
}
let dest_net = ctx
.npdu
.destination
.as_ref()
.map(|dest| dest.network)
.unwrap_or(0);
let (route, reachability) = lookup_route(table, dest_net).await;
let refused = || Refused::control(send_txs, local.addresses(), ctx);
let Some(route) = route else {
refuse_unknown(discovery, &refused(), dest_net).await;
return;
};
if let Some(reason) = route_refusal(reachability) {
send_reject(&refused(), dest_net, reason);
return;
}
forward_unicast(
send_txs,
&route,
ctx.port_network,
ctx.source_mac.as_slice(),
ctx.npdu.clone(),
ctx.port_idx,
&ctx.data_attributes,
);
}
async fn lookup_route(
table: &Mutex<RouterTable>,
dnet: u16,
) -> (Option<RouteEntry>, Option<ReachabilityStatus>) {
let mut tbl = table.lock().await;
let route = tbl.lookup(dnet).cloned();
let reachability = tbl.effective_reachability(dnet);
if route.is_some() {
tbl.touch(dnet);
}
(route, reachability)
}
async fn refuse_unknown(discovery: &Mutex<DiscoveryTracker>, refused: &Refused<'_>, dnet: u16) {
let solicit = discovery.lock().await.should_solicit(dnet);
if solicit {
solicit_who_is(
refused.send_txs,
refused.port_idx,
dnet,
refused.data_attributes,
);
}
send_reject(refused, dnet, RejectMessageReason::NOT_DIRECTLY_CONNECTED);
}
fn solicit_who_is(
send_txs: &[mpsc::Sender<SendRequest>],
ingress: usize,
dnet: u16,
data_attributes: &[DataAttribute],
) {
let mut payload = BytesMut::with_capacity(2);
payload.put_u16(dnet);
let npdu = Npdu {
is_network_message: true,
message_type: Some(NetworkMessageType::WHO_IS_ROUTER_TO_NETWORK.to_raw()),
payload: payload.freeze(),
..Npdu::default()
};
let mut buf = BytesMut::with_capacity(8);
if encode_npdu(&mut buf, &npdu).is_err() {
return;
}
let frozen = buf.freeze();
for (i, tx) in send_txs.iter().enumerate() {
if i != ingress {
let _ = tx.try_send(SendRequest::broadcast_with_attributes(
frozen.clone(),
data_attributes,
));
}
}
}