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