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::{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;