Skip to main content

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;