use std::collections::HashSet;
use std::sync::Arc;
use std::time::{Duration, Instant};
use bacnet_encoding::npdu::{encode_npdu, Npdu, NpduAddress};
use bacnet_types::enums::{NetworkMessageType, RejectMessageReason};
use bacnet_types::MacAddr;
use bytes::{BufMut, Bytes, BytesMut};
use tokio::sync::{mpsc, Mutex};
use tracing::{debug, warn};
use crate::router_table::RouterTable;
use super::control_policy::{ControlClass, ControlGate};
use super::local_control::LocalControl;
use super::reject::{route_received_reject, send_reject, Refused};
use super::{IngressContext, SendRequest};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct RoutingTableEntry {
network: u16,
port_id: u8,
}
fn parse_routing_table_entries(data: &[u8]) -> Option<Vec<RoutingTableEntry>> {
let count = usize::from(*data.first()?);
let mut offset = 1usize;
let mut entries = Vec::with_capacity(count);
for _ in 0..count {
if offset + 4 > data.len() {
return None;
}
let network = u16::from_be_bytes([data[offset], data[offset + 1]]);
let port_id = data[offset + 2];
let info_len = usize::from(data[offset + 3]);
if offset + 4 + info_len > data.len() {
return None;
}
entries.push(RoutingTableEntry { network, port_id });
offset += 4 + info_len;
}
if offset != data.len() {
return None;
}
Some(entries)
}
const MAX_ROUTER_CONTROL_REPLY_NPDU: usize = 501;
const INIT_ACK_NPDU_OVERHEAD: usize = 3;
const INIT_ACK_ENTRY_OCTETS: usize = 4;
const MAX_INIT_ACK_ENTRIES: usize =
(MAX_ROUTER_CONTROL_REPLY_NPDU - INIT_ACK_NPDU_OVERHEAD - 1) / INIT_ACK_ENTRY_OCTETS;
const _: () = assert!(
MAX_INIT_ACK_ENTRIES >= 1
&& MAX_INIT_ACK_ENTRIES <= 255
&& INIT_ACK_NPDU_OVERHEAD + 1 + MAX_INIT_ACK_ENTRIES * INIT_ACK_ENTRY_OCTETS
<= MAX_ROUTER_CONTROL_REPLY_NPDU
);
pub(super) async fn handle_network_message(
table: &Arc<Mutex<RouterTable>>,
send_txs: &[mpsc::Sender<SendRequest>],
ctx: &IngressContext,
control: &ControlGate,
local: &LocalControl,
) {
const MAX_LEARNED_ROUTES: usize = 256;
let port_idx = ctx.port_idx;
let port_network = ctx.port_network;
let source_mac = ctx.source_mac.as_slice();
let npdu: &Npdu = &ctx.npdu;
let ingress_attributes = ctx.data_attributes.as_slice();
let msg_type = match npdu.message_type {
Some(t) => t,
None => return,
};
if msg_type == NetworkMessageType::WHO_IS_ROUTER_TO_NETWORK.to_raw() {
let requested_network = match npdu.payload.len() {
0 => None,
2 => {
let net = u16::from_be_bytes([npdu.payload[0], npdu.payload[1]]);
if net == 0 {
return;
}
Some(net)
}
_ => return,
};
let table = table.lock().await;
let networks: Vec<u16> = if let Some(net) = requested_network {
match table.lookup(net) {
Some(entry) if entry.port_index != port_idx => vec![net],
_ => {
drop(table);
let source = npdu.source.clone().unwrap_or(NpduAddress {
network: port_network,
mac_address: MacAddr::from_slice(source_mac),
});
let forward = Npdu {
is_network_message: true,
expecting_reply: npdu.expecting_reply,
priority: npdu.priority,
source: Some(source),
message_type: Some(NetworkMessageType::WHO_IS_ROUTER_TO_NETWORK.to_raw()),
payload: npdu.payload.clone(),
..Npdu::default()
};
let mut fwd_buf = BytesMut::with_capacity(8);
if let Ok(()) = encode_npdu(&mut fwd_buf, &forward) {
let frozen = fwd_buf.freeze();
for (i, tx) in send_txs.iter().enumerate() {
if i != port_idx {
let _ = tx.try_send(SendRequest::broadcast_with_attributes(
frozen.clone(),
ingress_attributes,
));
}
}
}
return;
}
}
} else {
table.networks_not_on_port(port_idx)
};
if networks.is_empty() {
return;
}
let mut payload = BytesMut::with_capacity(networks.len() * 2);
for net in &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(4 + payload_len);
if let Err(e) = encode_npdu(&mut buf, &response) {
warn!("Failed to encode I-Am-Router response NPDU: {e}");
return;
}
if let Err(e) = send_txs[port_idx].try_send(SendRequest::broadcast_with_attributes(
buf.freeze(),
ingress_attributes,
)) {
warn!(%e, "Router dropped I-Am-Router response: output channel full");
}
} else if msg_type == NetworkMessageType::I_AM_ROUTER_TO_NETWORK.to_raw() {
let data = &npdu.payload;
if data.is_empty() || !data.len().is_multiple_of(2) {
return;
}
{
let targets: Vec<u16> = data
.as_chunks::<2>()
.0
.iter()
.map(|c| u16::from_be_bytes(*c))
.collect();
if !control.authorize(ctx, ControlClass::IAm, &targets, false) {
return;
}
}
let mut table = table.lock().await;
let mut replacement_networks = HashSet::new();
let mut offset = 0;
while offset + 2 <= data.len() {
let net = u16::from_be_bytes([data[offset], data[offset + 1]]);
offset += 2;
if table.len() >= MAX_LEARNED_ROUTES && table.lookup(net).is_none() {
table.record_learning_cap();
warn!("Router table learned routes cap ({MAX_LEARNED_ROUTES}) reached, ignoring further networks");
break;
}
if table
.lookup(net)
.is_some_and(|entry| !entry.directly_connected && entry.port_index != port_idx)
&& !replacement_networks.insert(net)
{
continue;
}
if table.apply_learning_claim(
net,
port_idx,
MacAddr::from_slice(source_mac),
Instant::now(),
) {
table.record_learned();
debug!(
network = net,
port = port_idx,
"Learned route from I-Am-Router-To-Network"
);
}
}
drop(table);
let rebroadcast = Npdu {
is_network_message: true,
message_type: Some(NetworkMessageType::I_AM_ROUTER_TO_NETWORK.to_raw()),
payload: npdu.payload.clone(),
..Npdu::default()
};
let mut buf = BytesMut::with_capacity(4 + npdu.payload.len());
if let Ok(()) = encode_npdu(&mut buf, &rebroadcast) {
let frozen = buf.freeze();
for (i, tx) in send_txs.iter().enumerate() {
if i != port_idx {
let _ = tx.try_send(SendRequest::broadcast_with_attributes(
frozen.clone(),
ingress_attributes,
));
}
}
}
} else if msg_type == NetworkMessageType::REJECT_MESSAGE_TO_NETWORK.to_raw() {
if npdu.payload.len() != 3 {
return;
}
let reason = npdu.payload[0];
let rejected_net = u16::from_be_bytes([npdu.payload[1], npdu.payload[2]]);
if !control.authorize(ctx, ControlClass::Reject, &[rejected_net], false) {
return;
}
warn!(
network = rejected_net,
reason = reason,
"Received Reject-Message-To-Network"
);
{
let mut tbl = table.lock().await;
tbl.apply_reject(rejected_net, port_idx, reason, Instant::now());
}
route_received_reject(table, send_txs, local, ctx).await;
} else if msg_type == NetworkMessageType::ROUTER_BUSY_TO_NETWORK.to_raw() {
let data = &npdu.payload;
if !data.len().is_multiple_of(2) {
return;
}
let mut listed = Vec::with_capacity(data.len() / 2);
let mut offset = 0;
while offset + 2 <= data.len() {
listed.push(u16::from_be_bytes([data[offset], data[offset + 1]]));
offset += 2;
}
if !control.authorize(ctx, ControlClass::Busy, &listed, listed.is_empty()) {
return;
}
let peer = MacAddr::from_slice(source_mac);
let deadline = Instant::now() + Duration::from_secs(30);
{
let mut tbl = table.lock().await;
if listed.is_empty() {
for net in tbl.routes_served_via_peer(port_idx, &peer) {
tbl.mark_busy(net, deadline);
debug!(
network = net,
"Router busy — marked network as congested (30s timer)"
);
}
} else {
for net in listed {
if tbl.is_served_via_peer(net, port_idx, &peer) {
tbl.mark_busy(net, deadline);
debug!(
network = net,
"Router busy — marked network as congested (30s timer)"
);
}
}
}
}
let rebroadcast = Npdu {
is_network_message: true,
message_type: Some(NetworkMessageType::ROUTER_BUSY_TO_NETWORK.to_raw()),
payload: npdu.payload.clone(),
..Npdu::default()
};
let mut buf = BytesMut::with_capacity(4 + npdu.payload.len());
if let Ok(()) = encode_npdu(&mut buf, &rebroadcast) {
let frozen = buf.freeze();
for (i, tx) in send_txs.iter().enumerate() {
if i != port_idx {
let _ = tx.try_send(SendRequest::broadcast_with_attributes(
frozen.clone(),
ingress_attributes,
));
}
}
}
} else if msg_type == NetworkMessageType::ROUTER_AVAILABLE_TO_NETWORK.to_raw() {
let data = &npdu.payload;
if !data.len().is_multiple_of(2) {
return;
}
let mut listed = Vec::with_capacity(data.len() / 2);
let mut offset = 0;
while offset + 2 <= data.len() {
listed.push(u16::from_be_bytes([data[offset], data[offset + 1]]));
offset += 2;
}
if !control.authorize(ctx, ControlClass::Available, &listed, listed.is_empty()) {
return;
}
let peer = MacAddr::from_slice(source_mac);
{
let mut tbl = table.lock().await;
if listed.is_empty() {
for net in tbl.routes_served_via_peer(port_idx, &peer) {
tbl.mark_available(net);
debug!(network = net, "Router available — cleared congestion");
}
} else {
for net in listed {
if tbl.is_served_via_peer(net, port_idx, &peer) {
tbl.mark_available(net);
debug!(network = net, "Router available — cleared congestion");
}
}
}
}
let rebroadcast = Npdu {
is_network_message: true,
message_type: Some(NetworkMessageType::ROUTER_AVAILABLE_TO_NETWORK.to_raw()),
payload: npdu.payload.clone(),
..Npdu::default()
};
let mut buf = BytesMut::with_capacity(4 + npdu.payload.len());
if let Ok(()) = encode_npdu(&mut buf, &rebroadcast) {
let frozen = buf.freeze();
for (i, tx) in send_txs.iter().enumerate() {
if i != port_idx {
let _ = tx.try_send(SendRequest::broadcast_with_attributes(
frozen.clone(),
ingress_attributes,
));
}
}
}
} else if msg_type == NetworkMessageType::INITIALIZE_ROUTING_TABLE.to_raw() {
let data = &npdu.payload;
let Some(entries) = parse_routing_table_entries(data) else {
return;
};
if data[0] == 0 {
let snapshot: Vec<(u16, u8)> = {
let tbl = table.lock().await;
let mut snapshot = Vec::with_capacity(tbl.len());
for net in tbl.sorted_networks() {
let port_id = tbl
.lookup(net)
.and_then(|entry| RouterTable::wire_port_id(entry.port_index));
match port_id {
Some(port_id) => snapshot.push((net, port_id)),
None => warn!(
network = net,
"Init-Routing-Table query: route has no wire Port ID, skipped"
),
}
}
snapshot
};
let chunk_len = MAX_INIT_ACK_ENTRIES.clamp(1, 255);
let mut chunks: Vec<&[(u16, u8)]> = snapshot.chunks(chunk_len).collect();
if chunks.is_empty() {
chunks.push(&[]);
}
for chunk in chunks {
let mut payload = BytesMut::with_capacity(1 + chunk.len() * INIT_ACK_ENTRY_OCTETS);
payload.put_u8(chunk.len() as u8);
for (net, port_id) in chunk {
payload.put_u16(*net);
payload.put_u8(*port_id);
payload.put_u8(0); }
let response = Npdu {
is_network_message: true,
message_type: Some(NetworkMessageType::INITIALIZE_ROUTING_TABLE_ACK.to_raw()),
payload: payload.freeze(),
..Npdu::default()
};
let mut buf = BytesMut::with_capacity(
INIT_ACK_NPDU_OVERHEAD + 1 + chunk.len() * INIT_ACK_ENTRY_OCTETS,
);
if let Err(e) = encode_npdu(&mut buf, &response) {
warn!("Failed to encode Init-Routing-Table-ACK NPDU: {e}");
return;
}
debug_assert!(buf.len() <= MAX_ROUTER_CONTROL_REPLY_NPDU);
if let Err(e) = send_txs[port_idx].try_send(SendRequest::unicast_with_attributes(
buf.freeze(),
MacAddr::from_slice(source_mac),
ingress_attributes,
)) {
warn!(%e, "Router dropped Init-Routing-Table-ACK: output channel full");
return;
}
}
return;
}
{
let targets: Vec<u16> = entries.iter().map(|e| e.network).collect();
if !control.authorize(ctx, ControlClass::InitMgmt, &targets, false) {
return;
}
}
let port_count = send_txs.len();
{
let mut tbl = table.lock().await;
for entry in &entries {
if entry.network == 0 || entry.network == 0xFFFF {
continue;
}
if entry.port_id == 0 {
if tbl.apply_management_removal(entry.network) {
debug!(
network = entry.network,
"Init-Routing-Table purged learned route"
);
}
continue;
}
let Some(mapped) = RouterTable::port_index_for_wire_id(entry.port_id, port_count)
else {
warn!(
network = entry.network,
port_id = entry.port_id,
"Init-Routing-Table: unknown Port ID, entry skipped"
);
continue;
};
if tbl.len() >= MAX_LEARNED_ROUTES && tbl.lookup(entry.network).is_none() {
tbl.record_learning_cap();
warn!("Init-Routing-Table: route cap reached, ignoring further entries");
break;
}
if tbl.apply_management_update(
entry.network,
mapped,
MacAddr::from_slice(source_mac),
) {
debug!(
network = entry.network,
port = mapped,
"Applied route from Init-Routing-Table management update"
);
}
}
}
let response = Npdu {
is_network_message: true,
message_type: Some(NetworkMessageType::INITIALIZE_ROUTING_TABLE_ACK.to_raw()),
payload: Bytes::new(),
..Npdu::default()
};
let mut buf = BytesMut::with_capacity(8);
if let Err(e) = encode_npdu(&mut buf, &response) {
warn!("Failed to encode Init-Routing-Table-ACK NPDU: {e}");
return;
}
if let Err(e) = send_txs[port_idx].try_send(SendRequest::unicast_with_attributes(
buf.freeze(),
MacAddr::from_slice(source_mac),
ingress_attributes,
)) {
warn!(%e, "Router dropped Init-Routing-Table-ACK: output channel full");
}
} else if msg_type == NetworkMessageType::I_COULD_BE_ROUTER_TO_NETWORK.to_raw() {
if npdu.payload.len() != 3 {
return;
}
let net = u16::from_be_bytes([npdu.payload[0], npdu.payload[1]]);
if !control.authorize(ctx, ControlClass::ICouldBe, &[net], false) {
return;
}
let performance_index = npdu.payload[2];
debug!(
network = net,
performance_index = performance_index,
port = port_idx,
"Received I-Could-Be-Router-To-Network"
);
let mut tbl = table.lock().await;
if tbl.lookup(net).is_none() {
tbl.add_learned(net, port_idx, MacAddr::from_slice(source_mac));
debug!(
network = net,
port = port_idx,
"Stored potential route from I-Could-Be-Router-To-Network"
);
}
} else if msg_type == NetworkMessageType::ESTABLISH_CONNECTION_TO_NETWORK.to_raw() {
if npdu.payload.len() != 3 {
return;
}
let net = u16::from_be_bytes([npdu.payload[0], npdu.payload[1]]);
let termination_time_min = npdu.payload[2];
tracing::info!(
network = net,
termination_time_minutes = termination_time_min,
"Received Establish-Connection-To-Network (PTP not implemented)"
);
} else if msg_type == NetworkMessageType::DISCONNECT_CONNECTION_TO_NETWORK.to_raw() {
if npdu.payload.len() != 2 {
return;
}
let net = u16::from_be_bytes([npdu.payload[0], npdu.payload[1]]);
debug!(
network = net,
"Received Disconnect-Connection-To-Network (PTP not implemented; route removal ignored)"
);
let mut tbl = table.lock().await;
tbl.record_disconnect_removal_ignored();
} else if msg_type == NetworkMessageType::WHAT_IS_NETWORK_NUMBER.to_raw() {
if npdu.source.is_some() || npdu.destination.is_some() {
return;
}
if !npdu.payload.is_empty() {
return;
}
let mut payload = BytesMut::with_capacity(3);
payload.put_u16(port_network);
payload.put_u8(1);
let response = Npdu {
is_network_message: true,
message_type: Some(NetworkMessageType::NETWORK_NUMBER_IS.to_raw()),
payload: payload.freeze(),
..Npdu::default()
};
let mut buf = BytesMut::with_capacity(8);
if let Err(e) = encode_npdu(&mut buf, &response) {
warn!("Failed to encode Network-Number-Is NPDU: {e}");
return;
}
if let Err(e) = send_txs[port_idx].try_send(SendRequest::broadcast_with_attributes(
buf.freeze(),
ingress_attributes,
)) {
warn!(%e, "Router dropped Network-Number-Is: output channel full");
}
} else if msg_type == NetworkMessageType::NETWORK_NUMBER_IS.to_raw() {
if npdu.source.is_some() || npdu.destination.is_some() {
return;
}
if !ctx.link_layer_group {
return;
}
if npdu.payload.len() != 3 {
return;
}
let net = u16::from_be_bytes([npdu.payload[0], npdu.payload[1]]);
let configured = npdu.payload[2];
if net != port_network {
if configured == 1 {
warn!(
local_network = port_network,
peer_network = net,
"Network number conflict: port configured as {} but peer reports {} (configured)",
port_network, net
);
} else {
debug!(
local_network = port_network,
peer_network = net,
"Network-Number-Is from peer (learned, differs from local)"
);
}
}
} else if msg_type == NetworkMessageType::INITIALIZE_ROUTING_TABLE_ACK.to_raw() {
let data = &npdu.payload;
if data.is_empty() {
return;
}
let Some(entries) = parse_routing_table_entries(data) else {
return;
};
{
let targets: Vec<u16> = entries.iter().map(|e| e.network).collect();
if !control.authorize(ctx, ControlClass::InitAck, &targets, false) {
return;
}
}
let mut table = table.lock().await;
let mut replacement_networks = HashSet::new();
for entry in &entries {
let net = entry.network;
if net == 0 || net == 0xFFFF {
continue;
}
if table.len() >= MAX_LEARNED_ROUTES {
table.record_learning_cap();
break;
}
if table
.lookup(net)
.is_some_and(|entry| !entry.directly_connected && entry.port_index != port_idx)
&& !replacement_networks.insert(net)
{
continue;
}
if table.apply_learning_claim(
net,
port_idx,
MacAddr::from_slice(source_mac),
Instant::now(),
) {
table.record_learned();
debug!(
network = net,
port = port_idx,
"Learned route from Init-Routing-Table-Ack"
);
}
}
} else if (0x0A..=0x11).contains(&msg_type) {
debug!(
message_type = msg_type,
"Router received security network message (not implemented)"
);
} else {
debug!(
message_type = msg_type,
"Router rejecting unknown network message type"
);
send_reject(
&Refused::control(send_txs, local.addresses(), ctx),
0,
RejectMessageReason::UNKNOWN_MESSAGE_TYPE,
);
}
}
#[cfg(test)]
#[path = "corroboration_tests.rs"]
mod corroboration_tests;