bacnet_network/router/mod.rs
1//! BACnet half-router — forwards APDUs between BACnet networks.
2//!
3//! A BACnet router (ASHRAE 135-2020 Clause 6.6) sits on several BACnet
4//! networks at once and relays messages among them, rewriting the NPDU
5//! source/destination fields and decrementing the hop count as it goes.
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//!
13//! Local application delivery follows the shared
14//! [receive-queue contract](crate::layer#receive-queue-admission), independently
15//! of forwarding and inline network-message handling.
16//!
17//! Each outgoing port holds at most 256 queued sends, plus one in progress.
18//! Among queued sends, higher network priority goes first; equal priorities
19//! keep their arrival order (Clause 6.5.4). A full queue refuses the new send
20//! without evicting an earlier one. A send in progress is not preempted, and
21//! sustained higher-priority traffic can delay lower-priority traffic.
22//!
23//! # Routing-claim trust model (RB-06)
24//!
25//! Classic BACnet routing claims are unauthenticated: an ingress port, next-hop
26//! MAC or advertised network is not proof that a peer is authorized to control
27//! that route. These local mitigations do not prevent route poisoning or
28//! authenticate a claim (Clauses 6.4 and 6.6.3):
29//! - Direct routes cannot be overwritten by learning, changed by rejects, or
30//! changed by Initialize-Routing-Table management updates.
31//! - Standard convergence applies 6.6.3.2 last-wins immediately: each new
32//! I-Am-Router / Init-ACK advertisement updates routing information, with
33//! flap warnings retained. Same-port refreshes and absent learning stay
34//! immediate; I-Could-Be-Router remains absent-only, within the route cap.
35//! - Hardened convergence ([`Convergence::Hardened`](crate::router_table::Convergence)
36//! set through [`BACnetRouter::table`], explicit opt-in only)
37//! holds cross-port learned moves for a second same-(network, port) claim
38//! within 60s inclusive. Repeats in separate messages suffice; duplicates in
39//! one message cannot corroborate. The old route forwards while pending.
40//! Alternation resets the slot; single-shot advertisers need the 60s reaper
41//! plus a fresh claim. Corroboration is dampening, not identity assurance.
42//! - Initialize-Routing-Table updates are management writes (6.4.7/6.6.3.8):
43//! nonzero Port ID replaces/appends the learned entry, Port ID 0 purges it,
44//! immediately in wire order. They never create/alter direct routes. Wire
45//! controls pass [`control_policy::ControlGate`] (RB-09); direct immunity
46//! stays safety scoping, not auth. LOCAL table calls bypass policy.
47//! - A query (Number of Ports 0) never mutates and answers with the complete
48//! table in ascending-DNET bounded portions (6.6.3.9); wire Port IDs are
49//! `port_index + 1` (0 stays the purge trigger).
50//! - Hardened pending holds one challenger per learned network (bounded by live
51//! learned routes). Slots expire lazily on the next claim plus the 60s aging
52//! reaper; apply/refresh/removal/aging/direct/manual edits clear the slot.
53//! - Freshness is split: `last_seen` (control confirmation) drives 300s expiry;
54//! `last_used` (forwarding use) never extends it. Busy/Available touch
55//! neither. Traffic no longer pins a dead route past the rescue.
56//! - Unknown DNETs solicit once per 5s per DNET (coalesced, max 256 pending,
57//! 30s entry age, reserved never solicited) then fail the caller with
58//! NOT_DIRECTLY_CONNECTED (6.6.3.1/6.5 retryable); packets are never buffered.
59//! `stop` cancels pending discovery.
60//! - Disconnect never removes routes (PTP unimplemented). Reject transitions
61//! are dampened per (ingress port, network) with a 30s hold-down; no-op
62//! rejects never extend busy deadlines. The table's
63//! [`claim_counters`](crate::router_table::Table::claim_counters) stay
64//! count-only saturating and never affect decisions.
65
66mod actions;
67mod application;
68mod application_feed;
69mod application_replies;
70mod application_transactions;
71pub mod control_policy;
72mod counters;
73mod dispatch;
74mod engine;
75mod hosted_host;
76mod limits;
77mod local_control;
78mod local_delivery;
79mod options;
80mod originate;
81mod port_groups;
82mod replies;
83mod send_queue;
84mod send_warning;
85mod sender;
86mod shutdown;
87mod startup;
88pub use application::{RouterApplicationPort, RouterApplicationPorts};
89pub use replies::RouterResponse;
90pub use shutdown::{RouterStopError, RouterStopFailure};
91
92use std::ops::{Deref, DerefMut};
93use std::sync::Arc;
94use std::sync::atomic::AtomicU64;
95
96use bacnet_npdu::Mac;
97pub use bacnet_router_core::router::{
98 Congestion, Duplicates, OriginateError, Request, SourceSelection,
99};
100use bacnet_router_core::{Instant, PortId};
101use bacnet_transport::port::{DataAttribute, TransportPort};
102use bacnet_types::MacAddr;
103use bacnet_types::error::Error;
104use bytes::Bytes;
105pub use counters::Counters;
106pub use originate::OriginateFailure;
107use tokio::sync::mpsc;
108
109use crate::layer::ReceivedNetworkControl;
110use crate::layer::{AdmissionReceiver, AdmissionSender, QueueAdmissionCounters, ReceivedApdu};
111
112pub use control_policy::{ControlAuthContext, ControlAuthorizer, ControlClass};
113pub use control_policy::{ControlDecisionCounters, ControlGate, ControlPolicy};
114pub use control_policy::{ControlServiceCounters, ControlTrust};
115use dispatch::{Outputs, PortDispatch, Shared};
116use engine::{Engine, EngineGuard, PORTS, tokio_clock};
117pub use options::{LocalApduReceiver, NetworkControlReceiver, RouterOptions, StartedRouter};
118use port_groups::PortGroups;
119
120// The suites that start a router take these names from here, as before the
121// engine.
122#[cfg(test)]
123use {
124 bacnet_encoding::npdu::{Npdu, encode_npdu},
125 bacnet_npdu::NetworkMessageType,
126 bacnet_transport::port::TransportProvenance,
127 bytes::BytesMut,
128 std::time::Duration,
129};
130
131/// A send request to be forwarded on a port.
132#[derive(Debug)]
133enum SendRequest {
134 Unicast {
135 npdu: Bytes,
136 mac: MacAddr,
137 data_attributes: Vec<DataAttribute>,
138 reject_origin: Option<bacnet_router_core::router::RejectOrigin>,
139 response: Option<replies::ReplyCompletion>,
140 },
141 Broadcast {
142 npdu: Bytes,
143 data_attributes: Vec<DataAttribute>,
144 reject_origin: Option<bacnet_router_core::router::RejectOrigin>,
145 },
146}
147
148/// A router port: a transport bound to a specific BACnet network number.
149///
150/// [`BACnetRouter::start`] calls
151/// [`TransportPort::prepare_for_routing`] on each port's transport before it
152/// starts it, so an MS/TP port takes and sends extended frames whatever its
153/// `MstpConfig` says: Clause 9.3 requires them of a routing node. Once the
154/// transport has started, it reads [`TransportPort::max_npdu`], and answers
155/// an NPDU too long for the port with reject reason 4 (#1665). It follows
156/// the limit's changes after that, for a transport that publishes them
157/// ([`TransportPort::max_npdu_changes`], #1696).
158///
159/// `T` defaults to [`AnyTransport`], so `RouterPort` alone names a port of a
160/// router that mixes link types; `RouterPort<BipTransport>` and the like
161/// name one of a router whose ports share a concrete transport.
162///
163/// [`AnyTransport`]: bacnet_transport::any::AnyTransport
164/// [`BipTransport`]: bacnet_transport::bip::BipTransport
165pub struct RouterPort<T: TransportPort = bacnet_transport::any::AnyTransport> {
166 /// The transport for this port.
167 pub transport: T,
168 /// The network number assigned to this port: 1 to 65534.
169 /// [`BACnetRouter::start`] refuses 0, which is no network, and 0xFFFF,
170 /// the global broadcast DNET, which can't be the SNET the router puts
171 /// in what it relays from the port (Clause 6.2.2.1).
172 pub network_number: u16,
173}
174
175/// An owned address snapshot captured after a router port starts.
176///
177/// This contains no transport or socket owner. Its index is the configured
178/// input position and the MAC is the transport's actual post-start address.
179#[derive(Clone, Debug, PartialEq, Eq)]
180pub struct RouterPortInfo {
181 index: u8,
182 network_number: u16,
183 mac: MacAddr,
184}
185
186impl RouterPortInfo {
187 /// Zero-based configured input position, also the route's egress port ID.
188 pub fn index(&self) -> u8 {
189 self.index
190 }
191 /// The directly attached BACnet network number.
192 pub fn network_number(&self) -> u16 {
193 self.network_number
194 }
195 /// The actual local MAC captured after successful transport startup.
196 pub fn local_mac(&self) -> &[u8] {
197 &self.mac
198 }
199}
200
201/// BACnet router connecting multiple networks.
202///
203/// The router holds multiple ports, each bound to a different BACnet network.
204/// When an NPDU arrives on one port with a destination network that maps to
205/// another port, the router forwards the message.
206/// See the [receive-queue contract](crate::layer#receive-queue-admission) for
207/// raw/tracked local receivers, admission limits, counters and lifecycle.
208///
209/// Every routing decision is `bacnet-router-core`'s engine (#1679), which
210/// the router keeps behind one lock: each port's dispatch task takes it for
211/// one frame at a time, as the sweep and [`Self::table`] do, and nothing
212/// holds it across an await. The engine's timers run on Tokio time from the
213/// router's start. It follows Clause 6 where the router before #1679 didn't
214/// (R1 to R7 of #1679): egress limits, rejects that keep routes,
215/// network numbers at startup, reachable-only and arrival-port-aware
216/// route answers, the hop count, and the router's own address on any port.
217pub struct BACnetRouter {
218 replies: Arc<replies::ReplyLifetime>,
219 /// The port queues, for the router's own requests ([`Self::originate`]):
220 /// held weakly, as replies hold them, so stopping the dispatch tasks
221 /// still closes the local receivers.
222 outputs: std::sync::Weak<Outputs>,
223 /// The engine, the gate and the ports' groups.
224 shared: Arc<Shared>,
225 lifecycle: shutdown::Lifecycle,
226 port_info: Vec<RouterPortInfo>,
227 /// Last sequence given to a control for the router's own consumer.
228 network_control_ingress_sequence: Arc<AtomicU64>,
229}
230
231/// The routing table of a running [`BACnetRouter`], borrowed for local
232/// management (see [`BACnetRouter::table`]).
233///
234/// While it lives, every port's dispatch and the sweep wait for it: read or
235/// change what you need, then drop it. It can't be held across an await in
236/// a spawned task.
237///
238/// The lock behind it isn't reentrant. Don't call [`BACnetRouter::table`]
239/// again while holding one, nor from a [`ControlAuthorizer`], which runs
240/// while that lock is held: the call waits for the lock already held, and
241/// on the same thread that is forever. The router's other `&self` methods
242/// take no lock, so they are safe to call meanwhile: the counts
243/// ([`BACnetRouter::counters`] and the four drop counts),
244/// [`BACnetRouter::now`] and [`BACnetRouter::control_snapshot`].
245pub struct RouterTableGuard<'a> {
246 router: EngineGuard<'a>,
247}
248
249impl Deref for RouterTableGuard<'_> {
250 type Target = crate::router_table::RouterTable;
251
252 fn deref(&self) -> &Self::Target {
253 self.router.table()
254 }
255}
256
257impl DerefMut for RouterTableGuard<'_> {
258 fn deref_mut(&mut self) -> &mut Self::Target {
259 self.router.table_mut()
260 }
261}
262
263impl std::fmt::Debug for RouterTableGuard<'_> {
264 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 self.router.table().fmt(f)
266 }
267}
268
269impl BACnetRouter {
270 /// Create and start a router from a list of ports, set up as `options`
271 /// asks (#1220).
272 ///
273 /// Returns the router with its local APDU receiver, for messages without
274 /// a remote destination or for which this router is the final hop, plus
275 /// the network-control receiver when the options ask for it.
276 /// [`RouterOptions::new`] is the plain router; see [`RouterOptions`] for
277 /// the tracked APDU receiver, the wire-control policy and the
278 /// network-control receiver, which combine freely.
279 ///
280 /// Fails if there are no ports or more than 255 (a routing table's Port
281 /// ID names at most 255), if a port's network number is 0 or 0xFFFF
282 /// ([`RouterPort::network_number`]), or if two ports share one, all
283 /// before any transport starts; and if a transport fails to start, or
284 /// if a transport's own MAC is longer than
285 /// [`MAX_MAC_LEN`](bacnet_npdu::MAX_MAC_LEN)
286 /// octets. Failed startup awaits cleanup of every attempted port, including
287 /// a partially started failing port. The original startup error takes
288 /// precedence; cleanup failures are logged with port identity.
289 ///
290 /// Cancelling startup initiates retained cleanup on the current runtime.
291 /// It does not imply cleanup has completed or resources can be reused.
292 /// Dropping a running router similarly initiates best-effort cleanup;
293 /// await [`Self::stop`] for a successful cleanup barrier.
294 pub async fn start<T, A, C>(
295 ports: Vec<RouterPort<T>>,
296 options: RouterOptions<A, C>,
297 ) -> Result<StartedRouter<A, C>, Error>
298 where
299 T: TransportPort + 'static,
300 A: LocalApduReceiver,
301 C: NetworkControlReceiver,
302 {
303 let (control_tx, network_control) = if options.wants_network_control() {
304 let (tx, rx, counters) = AdmissionReceiver::channel(C::TRACKED);
305 (Some(tx), Some(C::from_queue(rx, counters)))
306 } else {
307 (None, None)
308 };
309 let gate = Arc::new(options.control_gate());
310 let (router, rx, counters) = Self::start_dispatch(
311 ports,
312 A::TRACKED,
313 gate,
314 control_tx,
315 options.engine_options(),
316 )
317 .await?;
318 Ok(StartedRouter {
319 router,
320 apdus: A::from_queue(rx, counters),
321 network_control,
322 })
323 }
324
325 /// Count-only RB-09 decision totals (allow/deny/policy-deny per class).
326 pub fn control_snapshot(&self) -> control_policy::ControlDecisionCounters {
327 self.shared.gate.snapshot()
328 }
329
330 /// The engine's counts since start: the four drop counts below, and
331 /// what else the engine counts, such as the frames it refused as
332 /// malformed, the relays a spent hop count stopped, the rejects it sent,
333 /// the sends too long for their egress port (`egress_too_long`: the
334 /// limit its transport's [`TransportPort::max_npdu`] reports, #1665, or
335 /// a definite pre-send length refusal reported by the transport, including
336 /// BACnet/SC frame attributes and direct-peer bounds, #1716),
337 /// and the rewrites it couldn't write (`write_failures`: longer than its
338 /// 65,556-octet scratch, or with a field no NPDU may carry). They
339 /// saturate and name no peer. The table's own counts are
340 /// [`claim_counters`](crate::router_table::Table::claim_counters).
341 ///
342 /// They are the totals as of the last frame, send refusal, sweep or
343 /// table change the engine finished: the engine's own counts are `u32`s
344 /// it starts again each time they are taken, and the router takes them,
345 /// and adds them to these `u64` totals, before it lets the engine's lock
346 /// go (#1767). So reading them takes no lock: it is safe while a
347 /// [`RouterTableGuard`] lives, and so are the four drop counts below,
348 /// which read them.
349 pub fn counters(&self) -> Counters {
350 self.shared.engine.counters()
351 }
352
353 /// NPDUs refused on any port since start because an address in them was
354 /// past [`MAX_MAC_LEN`](bacnet_npdu::MAX_MAC_LEN):
355 /// their DLEN or SLEN (#1141), or the link-layer source MAC the port's
356 /// transport reported (#1198). Saturates at `u64::MAX`.
357 ///
358 /// Such an NPDU is neither forwarded nor delivered locally, and a network
359 /// message in it changes no route. When its DLEN or SLEN is the long one
360 /// and it names a specific DNET, the router also answers the sender with
361 /// Reject-Message-To-Network reason 6, `ADDRESSING_ERROR` (Clause 6.4.4),
362 /// the way it rejects a DNET it cannot reach. A global broadcast, or an
363 /// NPDU without a DNET, is only dropped. So is a frame from an over-long
364 /// link-layer source MAC: that is a fault in the transport, not in the
365 /// NPDU, and the router sends nothing back to such a MAC. No built-in
366 /// transport reports one.
367 pub fn address_length_drops(&self) -> u64 {
368 self.counters().address_length_drops
369 }
370
371 /// NPDUs dropped on any port since start because their DNET was 0xFFFF
372 /// and they also carried a DADR (#1379). Saturates at `u64::MAX`.
373 ///
374 /// DNET 0xFFFF already names every device on every network (Clauses
375 /// 6.2.2 and 6.3.2), so a DADR beside it contradicts it. The router does
376 /// not pass such an NPDU on to its other ports, deliver it locally,
377 /// act on a network message in it, or answer it with a reject: a
378 /// global broadcast never draws one. The address lengths are within
379 /// bounds, so the drop is counted here and not in
380 /// [`Self::address_length_drops`].
381 pub fn global_broadcast_dadr_drops(&self) -> u64 {
382 self.counters().global_broadcast_dadr_drops
383 }
384
385 /// NPDUs dropped on any port since start because they were addressed to
386 /// a broadcast, global (DNET 0xFFFF) or remote (a DNET with DLEN 0), and
387 /// carried an APDU other than an Unconfirmed-Request (#1491). Saturates
388 /// at `u64::MAX`.
389 ///
390 /// Only an Unconfirmed-Request may go to a broadcast network address
391 /// (Clause 6.3). Anything else belongs to one peer's transaction, so the
392 /// router neither passes it on to any network nor delivers it locally,
393 /// and answers it with no reject. The PDU type comes from the first APDU
394 /// octet, with no decode; network messages are not affected.
395 pub fn broadcast_pdu_type_drops(&self) -> u64 {
396 self.counters().broadcast_pdu_type_drops
397 }
398
399 /// NPDUs dropped on any port since start because the router would have
400 /// delivered them, on a directly connected port, to a DADR that is a
401 /// group destination there, such as a B/IP multicast address, and their
402 /// APDU isn't an Unconfirmed-Request (#1504). Saturates at `u64::MAX`.
403 ///
404 /// Sent as one unicast, such an NPDU would reach every node in the
405 /// group without the broadcast network addresses that
406 /// [`Self::broadcast_pdu_type_drops`] filters: a confirmed request would
407 /// draw an answer from each node, and an answer would reach nodes that
408 /// asked nothing. The port's rule is
409 /// [`TransportPort::group_destinations`],
410 /// so a custom transport that keeps the default recognizes none. The
411 /// router delivers it nowhere and answers it with no reject. A network
412 /// message routed to such a DADR, or a Reject-Message-To-Network relayed
413 /// to one, is dropped and counted too: it must not reach every router on
414 /// that network as a broadcast. An Unconfirmed-Request for one of the
415 /// groups the port's broadcast stands for
416 /// ([`TransportPort::local_broadcast_destinations`]) isn't counted: it
417 /// goes out on that port as a local broadcast, and reaches this router's
418 /// application where a DLEN 0 broadcast would (#1677). On B/IP, one for a
419 /// group at another UDP port, another B/IP network, is counted.
420 pub fn group_dadr_drops(&self) -> u64 {
421 self.counters().group_dadr_drops
422 }
423
424 /// Immutable post-start address snapshots in configured-port order.
425 ///
426 /// Copies retain no live transport resources. The snapshots remain readable
427 /// after stop and describe that generation, not a new bind or live lookup.
428 pub fn ports(&self) -> &[RouterPortInfo] {
429 &self.port_info
430 }
431
432 /// The routing table, locked for local management: read routes, or
433 /// change them with no wire-control policy in the way.
434 ///
435 /// Every port's dispatch waits while the guard lives, so drop it soon,
436 /// and never hold it across an await. Times in it are the router's own
437 /// ([`Self::now`]).
438 ///
439 /// Don't call it again while a guard lives on the same thread, or from
440 /// a [`ControlAuthorizer`]: the lock isn't reentrant, so that call
441 /// never returns. The counts, [`Self::now`] and
442 /// [`Self::control_snapshot`] take no lock and are safe to call while
443 /// the guard lives.
444 pub fn table(&self) -> RouterTableGuard<'_> {
445 RouterTableGuard {
446 router: self.shared.engine.lock(),
447 }
448 }
449
450 /// The router's time now: the clock its table and timers run on, Tokio
451 /// time since the router started.
452 pub fn now(&self) -> Instant {
453 self.shared.engine.now()
454 }
455
456 /// Stop the router and await every transport's async cleanup.
457 ///
458 /// Seals forwarding and replies, interrupts pending sends, joins router
459 /// workers and awaits every transport stop, even when another fails. `Ok`
460 /// is a cleanup completion barrier; `Err` retains typed failures and makes
461 /// no resource-release promise for the failed owners. Repeated calls replay
462 /// the same result without retrying. Cancelling this wait leaves cleanup
463 /// running; another call can await its completion.
464 ///
465 /// Queued local APDUs remain drainable, but their replies are sealed. Stop
466 /// does not clear admission totals or wait for local consumers, even with
467 /// a full queue. See the [receive-queue contract](crate::layer#receive-queue-admission).
468 pub async fn stop(&mut self) -> Result<(), RouterStopError> {
469 self.replies.seal();
470 self.shared.engine.lock().cancel_discovery();
471 self.lifecycle.stop().await
472 }
473
474 /// How many unknown networks the router is looking for.
475 #[cfg(test)]
476 fn searches(&self) -> (usize, bool) {
477 let router = self.shared.engine.lock();
478 (router.discovery().len(), router.discovery().is_cancelled())
479 }
480}
481
482#[cfg(test)]
483mod admission_tests;
484#[cfg(test)]
485mod bip_group_tests;
486#[cfg(test)]
487mod busy_scope_tests;
488#[cfg(test)]
489mod claim_tests;
490#[cfg(test)]
491mod congestion_tests;
492#[cfg(test)]
493mod corroboration_tests;
494#[cfg(test)]
495mod cutover_tests;
496#[cfg(test)]
497mod driven_port;
498#[cfg(test)]
499mod envelope_control_tests;
500#[cfg(test)]
501mod envelope_discovery_tests;
502#[cfg(test)]
503mod envelope_harness;
504#[cfg(test)]
505mod fixes_tests;
506#[cfg(test)]
507mod hardening_tests;
508#[cfg(test)]
509mod ingress_harness;
510#[cfg(test)]
511mod init_routing_table_tests;
512#[cfg(test)]
513mod late_refusal_tests;
514#[cfg(test)]
515mod limit_tests;
516#[cfg(test)]
517mod mstp_extended_tests;
518#[cfg(test)]
519mod originate_tests;
520#[cfg(test)]
521mod owned_bip_tests;
522#[cfg(test)]
523mod owned_fixture;
524#[cfg(test)]
525mod owned_lifecycle_tests;
526#[cfg(test)]
527mod owned_startup_tests;
528#[cfg(test)]
529mod priority_harness;
530#[cfg(test)]
531mod priority_tests;
532#[cfg(test)]
533mod r7_reply_tests;
534#[cfg(test)]
535mod rb06_convergence_tests;
536#[cfg(test)]
537mod rb09_control_policy_tests;
538#[cfg(test)]
539mod reject_tests;
540#[cfg(test)]
541mod tests;
542
543impl Drop for BACnetRouter {
544 fn drop(&mut self) {
545 self.replies.seal();
546 self.shared.engine.lock().cancel_discovery();
547 self.lifecycle.begin();
548 }
549}