bacnet_network/router/mod.rs
1//! BACnet half-router — forwards APDUs between BACnet networks.
2//!
3//! Per ASHRAE 135-2020 Clause 6.4, a BACnet router connects two or more
4//! BACnet networks. It forwards messages between them by manipulating
5//! the NPDU source/destination fields and decrementing the hop count.
6//!
7//! This implementation supports:
8//! - Forwarding APDUs between directly-connected networks
9//! - Who-Is-Router-To-Network / I-Am-Router-To-Network messages
10//! - Reject-Message-To-Network for unknown routes
11//! - Learned routes from I-Am-Router-To-Network announcements
12
13use std::sync::Arc;
14use std::time::Duration;
15
16use bacnet_encoding::npdu::{decode_npdu, encode_npdu, Npdu};
17use bacnet_transport::port::{DataAttribute, TransportPort};
18use bacnet_types::enums::{NetworkMessageType, RejectMessageReason};
19use bacnet_types::error::Error;
20use bacnet_types::MacAddr;
21use bytes::{BufMut, Bytes, BytesMut};
22use tokio::sync::{mpsc, Mutex};
23use tokio::task::JoinHandle;
24use tracing::{debug, warn};
25
26use crate::layer::ReceivedApdu;
27use crate::router_table::{ReachabilityStatus, RouterTable};
28
29mod control_messages;
30mod forwarding;
31
32use control_messages::handle_network_message;
33use forwarding::{forward_broadcast, forward_unicast, send_reject};
34
35/// A send request to be forwarded on a port.
36#[derive(Debug)]
37enum SendRequest {
38 Unicast {
39 npdu: Bytes,
40 mac: MacAddr,
41 data_attributes: Vec<DataAttribute>,
42 },
43 Broadcast {
44 npdu: Bytes,
45 data_attributes: Vec<DataAttribute>,
46 },
47}
48
49impl SendRequest {
50 fn unicast(npdu: Bytes, mac: MacAddr) -> Self {
51 Self::Unicast {
52 npdu,
53 mac,
54 data_attributes: Vec::new(),
55 }
56 }
57
58 fn broadcast(npdu: Bytes) -> Self {
59 Self::Broadcast {
60 npdu,
61 data_attributes: Vec::new(),
62 }
63 }
64}
65
66/// A router port: a transport bound to a specific BACnet network number.
67pub struct RouterPort<T: TransportPort> {
68 /// The transport for this port.
69 pub transport: T,
70 /// The network number assigned to this port.
71 pub network_number: u16,
72}
73
74/// BACnet router connecting multiple networks.
75///
76/// The router holds multiple ports, each bound to a different BACnet network.
77/// When an NPDU arrives on one port with a destination network that maps to
78/// another port, the router forwards the message.
79pub struct BACnetRouter {
80 /// Shared routing table.
81 table: Arc<Mutex<RouterTable>>,
82 /// Dispatch tasks (one per port).
83 dispatch_tasks: Vec<JoinHandle<()>>,
84 /// Sender tasks (one per port, owns the transport for outgoing messages).
85 sender_tasks: Vec<JoinHandle<()>>,
86 /// Background task that purges stale learned routes.
87 aging_task: Option<JoinHandle<()>>,
88}
89
90impl BACnetRouter {
91 /// Create and start a router from a list of ports.
92 ///
93 /// Returns the router and a receiver for APDUs destined to local
94 /// applications (messages without remote destination or where this
95 /// router is the final hop).
96 pub async fn start<T: TransportPort + 'static>(
97 mut ports: Vec<RouterPort<T>>,
98 ) -> Result<(Self, mpsc::Receiver<ReceivedApdu>), Error> {
99 let mut table = RouterTable::new();
100
101 // Reject duplicate network numbers
102 {
103 let mut seen = std::collections::HashSet::new();
104 for port in &ports {
105 if !seen.insert(port.network_number) {
106 return Err(Error::Encoding(format!(
107 "Duplicate network number {} in router ports",
108 port.network_number
109 )));
110 }
111 }
112 }
113
114 // Register directly-connected networks
115 for (idx, port) in ports.iter().enumerate() {
116 table.add_direct(port.network_number, idx);
117 }
118
119 let table = Arc::new(Mutex::new(table));
120 let (local_tx, local_rx) = mpsc::channel(256);
121
122 // Start each transport, set up send channels
123 let mut port_receivers = Vec::new();
124 let mut send_txs: Vec<mpsc::Sender<SendRequest>> = Vec::new();
125 let mut sender_tasks = Vec::new();
126 let mut port_networks = Vec::new();
127 let mut port_local_macs = Vec::new();
128
129 for port in &mut ports {
130 let rx = port.transport.start().await?;
131 port_receivers.push(rx);
132 port_networks.push(port.network_number);
133 port_local_macs.push(MacAddr::from_slice(port.transport.local_mac()));
134 }
135
136 // Move transports into sender tasks
137 for port in ports {
138 let (send_tx, mut send_rx) = mpsc::channel::<SendRequest>(256);
139 send_txs.push(send_tx);
140
141 let transport = port.transport;
142 let task = tokio::spawn(async move {
143 while let Some(req) = send_rx.recv().await {
144 match req {
145 SendRequest::Unicast {
146 npdu,
147 mac,
148 data_attributes,
149 } => {
150 if let Err(e) = transport
151 .send_unicast_with_data_attributes(&npdu, &mac, &data_attributes)
152 .await
153 {
154 warn!(error = %e, "Router send_unicast failed");
155 }
156 }
157 SendRequest::Broadcast {
158 npdu,
159 data_attributes,
160 } => {
161 if let Err(e) = transport
162 .send_broadcast_with_data_attributes(&npdu, &data_attributes)
163 .await
164 {
165 warn!(error = %e, "Router send_broadcast failed");
166 }
167 }
168 }
169 }
170 });
171 sender_tasks.push(task);
172 }
173
174 let send_txs = Arc::new(send_txs);
175
176 // Announce I-Am-Router-To-Network on each port listing networks reachable via other ports.
177 for (port_idx, tx) in send_txs.iter().enumerate() {
178 let other_networks: Vec<u16> = port_networks
179 .iter()
180 .enumerate()
181 .filter(|(idx, _)| *idx != port_idx)
182 .map(|(_, net)| *net)
183 .collect();
184
185 if other_networks.is_empty() {
186 continue;
187 }
188
189 let mut payload = BytesMut::with_capacity(other_networks.len() * 2);
190 for net in &other_networks {
191 payload.put_u16(*net);
192 }
193
194 let payload_len = payload.len();
195 let response = Npdu {
196 is_network_message: true,
197 message_type: Some(NetworkMessageType::I_AM_ROUTER_TO_NETWORK.to_raw()),
198 payload: payload.freeze(),
199 ..Npdu::default()
200 };
201
202 let mut buf = BytesMut::with_capacity(8 + payload_len);
203 if let Err(e) = encode_npdu(&mut buf, &response) {
204 warn!("Failed to encode I-Am-Router NPDU: {e}");
205 continue;
206 }
207
208 if let Err(e) = tx.try_send(SendRequest::broadcast(buf.freeze())) {
209 warn!(%e, "Router dropped I-Am-Router announcement: output channel full");
210 }
211 }
212
213 let mut dispatch_tasks = Vec::new();
214
215 for (port_idx, mut rx) in port_receivers.into_iter().enumerate() {
216 let table = Arc::clone(&table);
217 let local_tx = local_tx.clone();
218 let send_txs = Arc::clone(&send_txs);
219 let port_network = port_networks[port_idx];
220 let local_mac = port_local_macs[port_idx].clone();
221
222 let task = tokio::spawn(async move {
223 while let Some(received) = rx.recv().await {
224 match decode_npdu(received.npdu.clone()) {
225 Ok(npdu) => {
226 if npdu.is_network_message {
227 // Proprietary network messages (type >= 0x80) with DNET
228 // should be forwarded, not processed locally.
229 let is_proprietary =
230 npdu.message_type.map(|t| t >= 0x80).unwrap_or(false);
231 let has_remote_dest = npdu
232 .destination
233 .as_ref()
234 .is_some_and(|d| d.network != 0xFFFF);
235 if is_proprietary && has_remote_dest {
236 // Fall through to normal DNET routing below
237 } else {
238 handle_network_message(
239 &table,
240 &send_txs,
241 port_idx,
242 port_network,
243 &received.source_mac,
244 &npdu,
245 )
246 .await;
247 continue;
248 }
249 }
250
251 if let Some(ref dest) = npdu.destination {
252 let dest_net = dest.network;
253
254 // Global broadcast — forward to all other ports
255 if dest_net == 0xFFFF {
256 forward_broadcast(
257 &send_txs,
258 port_idx,
259 port_network,
260 &received.source_mac,
261 &npdu,
262 &received.data_attributes,
263 );
264
265 // Deliver locally as well
266 let apdu = ReceivedApdu {
267 apdu: npdu.payload,
268 source_mac: received.source_mac,
269 source_network: npdu.source,
270 data_attributes: received.data_attributes,
271 reply_tx: received.reply_tx,
272 };
273 let _ = local_tx.send(apdu).await;
274 continue;
275 }
276
277 // Route lookup for destination network
278 let (route, reachability) = {
279 let mut tbl = table.lock().await;
280 let route = tbl.lookup(dest_net).cloned();
281 let reachability = tbl.effective_reachability(dest_net);
282 if route.is_some() {
283 tbl.touch(dest_net);
284 }
285 (route, reachability)
286 };
287
288 if let Some(route) = route {
289 // Check reachability before forwarding (spec 6.6.3.6)
290 match reachability.unwrap_or(ReachabilityStatus::Reachable) {
291 ReachabilityStatus::Busy => {
292 send_reject(
293 &send_txs[port_idx],
294 &received.source_mac,
295 dest_net,
296 RejectMessageReason::ROUTER_BUSY,
297 );
298 continue;
299 }
300 ReachabilityStatus::Unreachable => {
301 send_reject(
302 &send_txs[port_idx],
303 &received.source_mac,
304 dest_net,
305 RejectMessageReason::NOT_DIRECTLY_CONNECTED,
306 );
307 continue;
308 }
309 ReachabilityStatus::Reachable => {}
310 }
311 if route.port_index == port_idx && route.directly_connected {
312 let dest_mac = npdu
313 .destination
314 .as_ref()
315 .map(|d| &d.mac_address[..])
316 .unwrap_or(&[]);
317 if dest_mac == &local_mac[..] {
318 // DADR matches our MAC: deliver locally
319 let apdu = ReceivedApdu {
320 apdu: npdu.payload,
321 source_mac: received.source_mac,
322 source_network: npdu.source,
323 data_attributes: received.data_attributes,
324 reply_tx: received.reply_tx,
325 };
326 let _ = local_tx.send(apdu).await;
327 } else {
328 // Remote broadcast to our network (DLEN=0):
329 // deliver locally AND forward
330 if dest_mac.is_empty() {
331 let apdu = ReceivedApdu {
332 apdu: npdu.payload.clone(),
333 source_mac: received.source_mac.clone(),
334 source_network: npdu.source.clone(),
335 data_attributes: received
336 .data_attributes
337 .clone(),
338 reply_tx: None,
339 };
340 let _ = local_tx.send(apdu).await;
341 }
342 forward_unicast(
343 &send_txs,
344 &route,
345 port_network,
346 &received.source_mac,
347 npdu,
348 port_idx,
349 &received.data_attributes,
350 );
351 }
352 } else {
353 forward_unicast(
354 &send_txs,
355 &route,
356 port_network,
357 &received.source_mac,
358 npdu,
359 port_idx,
360 &received.data_attributes,
361 );
362 }
363 } else {
364 // Unknown network: send reject
365 send_reject(
366 &send_txs[port_idx],
367 &received.source_mac,
368 dest_net,
369 RejectMessageReason::NOT_DIRECTLY_CONNECTED,
370 );
371 }
372 } else {
373 let apdu = ReceivedApdu {
374 apdu: npdu.payload,
375 source_mac: received.source_mac,
376 source_network: npdu.source,
377 data_attributes: received.data_attributes,
378 reply_tx: received.reply_tx,
379 };
380 let _ = local_tx.send(apdu).await;
381 }
382 }
383 Err(e) => {
384 warn!(error = %e, port = port_idx, "Router decode failed");
385 }
386 }
387 }
388 });
389
390 dispatch_tasks.push(task);
391 }
392
393 // Periodically purge stale learned routes.
394 let aging_table = Arc::clone(&table);
395 let aging_task = tokio::spawn(async move {
396 let mut interval = tokio::time::interval(Duration::from_secs(60));
397 let max_age = Duration::from_secs(300); // 5 minutes
398 loop {
399 interval.tick().await;
400 let mut tbl = aging_table.lock().await;
401 let purged = tbl.purge_stale(max_age);
402 tbl.clear_expired_busy();
403 drop(tbl);
404 for net in purged {
405 debug!(network = net, "Purged stale route");
406 }
407 }
408 });
409
410 Ok((
411 Self {
412 table,
413 dispatch_tasks,
414 sender_tasks,
415 aging_task: Some(aging_task),
416 },
417 local_rx,
418 ))
419 }
420
421 /// Get a reference to the routing table.
422 pub fn table(&self) -> &Arc<Mutex<RouterTable>> {
423 &self.table
424 }
425
426 /// Stop the router.
427 pub async fn stop(&mut self) {
428 for task in self.dispatch_tasks.drain(..) {
429 task.abort();
430 let _ = task.await;
431 }
432 for task in self.sender_tasks.drain(..) {
433 task.abort();
434 let _ = task.await;
435 }
436 if let Some(task) = self.aging_task.take() {
437 task.abort();
438 let _ = task.await;
439 }
440 }
441}
442
443#[cfg(test)]
444mod tests;