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;
67pub mod control_policy;
68mod counters;
69mod dispatch;
70mod engine;
71mod hosted_host;
72mod limits;
73mod local_control;
74mod local_delivery;
75mod options;
76mod originate;
77mod port_groups;
78mod replies;
79mod send_queue;
80mod send_warning;
81mod sender;
82pub use replies::RouterResponse;
83
84use std::ops::{Deref, DerefMut};
85use std::sync::atomic::AtomicU64;
86use std::sync::Arc;
87
88use bacnet_npdu::Mac;
89pub use bacnet_router_core::router::{
90 Congestion, Duplicates, OriginateError, Request, SourceSelection,
91};
92use bacnet_router_core::{Instant, PortId};
93use bacnet_transport::port::{DataAttribute, TransportPort};
94use bacnet_types::error::Error;
95use bacnet_types::MacAddr;
96use bytes::Bytes;
97pub use counters::Counters;
98pub use originate::OriginateFailure;
99use tokio::sync::mpsc;
100use tokio::task::JoinHandle;
101
102use crate::layer::ReceivedNetworkControl;
103use crate::layer::{AdmissionReceiver, AdmissionSender, QueueAdmissionCounters, ReceivedApdu};
104
105pub use control_policy::{ControlAuthContext, ControlAuthorizer, ControlClass};
106pub use control_policy::{ControlDecisionCounters, ControlGate, ControlPolicy};
107pub use control_policy::{ControlServiceCounters, ControlTrust};
108use dispatch::{Outputs, PortDispatch, Shared};
109use engine::{tokio_clock, Engine, EngineGuard, PORTS};
110pub use options::{LocalApduReceiver, NetworkControlReceiver, RouterOptions, StartedRouter};
111use port_groups::PortGroups;
112
113// The suites that start a router take these names from here, as before the
114// engine.
115#[cfg(test)]
116use {
117 bacnet_encoding::npdu::{encode_npdu, Npdu},
118 bacnet_npdu::NetworkMessageType,
119 bacnet_transport::port::TransportProvenance,
120 bytes::BytesMut,
121 std::time::Duration,
122};
123
124/// A send request to be forwarded on a port.
125#[derive(Debug)]
126enum SendRequest {
127 Unicast {
128 npdu: Bytes,
129 mac: MacAddr,
130 data_attributes: Vec<DataAttribute>,
131 reject_origin: Option<bacnet_router_core::router::RejectOrigin>,
132 response: Option<replies::ReplyCompletion>,
133 },
134 Broadcast {
135 npdu: Bytes,
136 data_attributes: Vec<DataAttribute>,
137 reject_origin: Option<bacnet_router_core::router::RejectOrigin>,
138 },
139}
140
141/// A router port: a transport bound to a specific BACnet network number.
142///
143/// [`BACnetRouter::start`] calls
144/// [`TransportPort::prepare_for_routing`] on each port's transport before it
145/// starts it, so an MS/TP port takes and sends extended frames whatever its
146/// `MstpConfig` says: Clause 9.3 requires them of a routing node. Once the
147/// transport has started, it reads [`TransportPort::max_npdu`], and answers
148/// an NPDU too long for the port with reject reason 4 (#1665). It follows
149/// the limit's changes after that, for a transport that publishes them
150/// ([`TransportPort::max_npdu_changes`], #1696).
151pub struct RouterPort<T: TransportPort> {
152 /// The transport for this port.
153 pub transport: T,
154 /// The network number assigned to this port: 1 to 65534.
155 /// [`BACnetRouter::start`] refuses 0, which is no network, and 0xFFFF,
156 /// the global broadcast DNET, which can't be the SNET the router puts
157 /// in what it relays from the port (Clause 6.2.2.1).
158 pub network_number: u16,
159}
160
161/// BACnet router connecting multiple networks.
162///
163/// The router holds multiple ports, each bound to a different BACnet network.
164/// When an NPDU arrives on one port with a destination network that maps to
165/// another port, the router forwards the message.
166/// See the [receive-queue contract](crate::layer#receive-queue-admission) for
167/// raw/tracked local receivers, admission limits, counters and lifecycle.
168///
169/// Every routing decision is `bacnet-router-core`'s engine (#1679), which
170/// the router keeps behind one lock: each port's dispatch task takes it for
171/// one frame at a time, as the sweep and [`Self::table`] do, and nothing
172/// holds it across an await. The engine's timers run on Tokio time from the
173/// router's start. It follows Clause 6 where the router before #1679 didn't
174/// (R1 to R7 of #1679): egress limits, rejects that keep routes,
175/// network numbers at startup, reachable-only and arrival-port-aware
176/// route answers, the hop count, and the router's own address on any port.
177pub struct BACnetRouter {
178 replies: Arc<replies::ReplyLifetime>,
179 /// The port queues, for the router's own requests ([`Self::originate`]):
180 /// held weakly, as replies hold them, so stopping the dispatch tasks
181 /// still closes the local receivers.
182 outputs: std::sync::Weak<Outputs>,
183 /// The engine, the gate and the ports' groups.
184 shared: Arc<Shared>,
185 /// Dispatch tasks (one per port).
186 dispatch_tasks: Vec<JoinHandle<()>>,
187 /// Sender tasks (one per port, owns the transport for outgoing messages).
188 sender_tasks: Vec<JoinHandle<()>>,
189 /// Background task that runs the engine's sweep when it is due.
190 aging_task: Option<JoinHandle<()>>,
191 /// Tasks that follow a port's NPDU limit (one per port whose transport
192 /// publishes its changes).
193 limit_tasks: Vec<JoinHandle<()>>,
194 /// Last sequence given to a control for the router's own consumer.
195 network_control_ingress_sequence: Arc<AtomicU64>,
196}
197
198/// The routing table of a running [`BACnetRouter`], borrowed for local
199/// management (see [`BACnetRouter::table`]).
200///
201/// While it lives, every port's dispatch and the sweep wait for it: read or
202/// change what you need, then drop it. It can't be held across an await in
203/// a spawned task.
204///
205/// The lock behind it isn't reentrant. Don't call [`BACnetRouter::table`]
206/// again while holding one, nor from a [`ControlAuthorizer`], which runs
207/// while that lock is held: the call waits for the lock already held, and
208/// on the same thread that is forever. The router's other `&self` methods
209/// take no lock, so they are safe to call meanwhile: the counts
210/// ([`BACnetRouter::counters`] and the four drop counts),
211/// [`BACnetRouter::now`] and [`BACnetRouter::control_snapshot`].
212pub struct RouterTableGuard<'a> {
213 router: EngineGuard<'a>,
214}
215
216impl Deref for RouterTableGuard<'_> {
217 type Target = crate::router_table::RouterTable;
218
219 fn deref(&self) -> &Self::Target {
220 self.router.table()
221 }
222}
223
224impl DerefMut for RouterTableGuard<'_> {
225 fn deref_mut(&mut self) -> &mut Self::Target {
226 self.router.table_mut()
227 }
228}
229
230impl std::fmt::Debug for RouterTableGuard<'_> {
231 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232 self.router.table().fmt(f)
233 }
234}
235
236impl BACnetRouter {
237 /// Create and start a router from a list of ports, set up as `options`
238 /// asks (#1220).
239 ///
240 /// Returns the router with its local APDU receiver, for messages without
241 /// a remote destination or for which this router is the final hop, plus
242 /// the network-control receiver when the options ask for it.
243 /// [`RouterOptions::new`] is the plain router; see [`RouterOptions`] for
244 /// the tracked APDU receiver, the wire-control policy and the
245 /// network-control receiver, which combine freely.
246 ///
247 /// Fails if there are no ports or more than 255 (a routing table's Port
248 /// ID names at most 255), if a port's network number is 0 or 0xFFFF
249 /// ([`RouterPort::network_number`]), or if two ports share one, all
250 /// before any transport starts; and if a transport fails to start, or
251 /// if a transport's own MAC is longer than
252 /// [`MAX_MAC_LEN`](bacnet_npdu::MAX_MAC_LEN)
253 /// octets.
254 pub async fn start<T, A, C>(
255 ports: Vec<RouterPort<T>>,
256 options: RouterOptions<A, C>,
257 ) -> Result<StartedRouter<A, C>, Error>
258 where
259 T: TransportPort + 'static,
260 A: LocalApduReceiver,
261 C: NetworkControlReceiver,
262 {
263 let (control_tx, network_control) = if options.wants_network_control() {
264 let (tx, rx, counters) = AdmissionReceiver::channel(C::TRACKED);
265 (Some(tx), Some(C::from_queue(rx, counters)))
266 } else {
267 (None, None)
268 };
269 let gate = Arc::new(options.control_gate());
270 let (router, rx, counters) = Self::start_dispatch(
271 ports,
272 A::TRACKED,
273 gate,
274 control_tx,
275 options.engine_options(),
276 )
277 .await?;
278 Ok(StartedRouter {
279 router,
280 apdus: A::from_queue(rx, counters),
281 network_control,
282 })
283 }
284
285 /// Count-only RB-09 decision totals (allow/deny/policy-deny per class).
286 pub fn control_snapshot(&self) -> control_policy::ControlDecisionCounters {
287 self.shared.gate.snapshot()
288 }
289
290 /// The engine's counts since start: the four drop counts below, and
291 /// what else the engine counts, such as the frames it refused as
292 /// malformed, the relays a spent hop count stopped, the rejects it sent,
293 /// the sends too long for their egress port (`egress_too_long`: the
294 /// limit its transport's [`TransportPort::max_npdu`] reports, #1665, or
295 /// a definite pre-send length refusal reported by the transport, including
296 /// BACnet/SC frame attributes and direct-peer bounds, #1716),
297 /// and the rewrites it couldn't write (`write_failures`: longer than its
298 /// 65,556-octet scratch, or with a field no NPDU may carry). They
299 /// saturate and name no peer. The table's own counts are
300 /// [`claim_counters`](crate::router_table::Table::claim_counters).
301 ///
302 /// They are the totals as of the last frame, send refusal, sweep or
303 /// table change the engine finished: the engine's own counts are `u32`s
304 /// it starts again each time they are taken, and the router takes them,
305 /// and adds them to these `u64` totals, before it lets the engine's lock
306 /// go (#1767). So reading them takes no lock: it is safe while a
307 /// [`RouterTableGuard`] lives, and so are the four drop counts below,
308 /// which read them.
309 pub fn counters(&self) -> Counters {
310 self.shared.engine.counters()
311 }
312
313 /// NPDUs refused on any port since start because an address in them was
314 /// past [`MAX_MAC_LEN`](bacnet_npdu::MAX_MAC_LEN):
315 /// their DLEN or SLEN (#1141), or the link-layer source MAC the port's
316 /// transport reported (#1198). Saturates at `u64::MAX`.
317 ///
318 /// Such an NPDU is neither forwarded nor delivered locally, and a network
319 /// message in it changes no route. When its DLEN or SLEN is the long one
320 /// and it names a specific DNET, the router also answers the sender with
321 /// Reject-Message-To-Network reason 6, `ADDRESSING_ERROR` (Clause 6.4.4),
322 /// the way it rejects a DNET it cannot reach. A global broadcast, or an
323 /// NPDU without a DNET, is only dropped. So is a frame from an over-long
324 /// link-layer source MAC: that is a fault in the transport, not in the
325 /// NPDU, and the router sends nothing back to such a MAC. No built-in
326 /// transport reports one.
327 pub fn address_length_drops(&self) -> u64 {
328 self.counters().address_length_drops
329 }
330
331 /// NPDUs dropped on any port since start because their DNET was 0xFFFF
332 /// and they also carried a DADR (#1379). Saturates at `u64::MAX`.
333 ///
334 /// DNET 0xFFFF already names every device on every network (Clauses
335 /// 6.2.2 and 6.3.2), so a DADR beside it contradicts it. The router does
336 /// not pass such an NPDU on to its other ports, deliver it locally,
337 /// act on a network message in it, or answer it with a reject: a
338 /// global broadcast never draws one. The address lengths are within
339 /// bounds, so the drop is counted here and not in
340 /// [`Self::address_length_drops`].
341 pub fn global_broadcast_dadr_drops(&self) -> u64 {
342 self.counters().global_broadcast_dadr_drops
343 }
344
345 /// NPDUs dropped on any port since start because they were addressed to
346 /// a broadcast, global (DNET 0xFFFF) or remote (a DNET with DLEN 0), and
347 /// carried an APDU other than an Unconfirmed-Request (#1491). Saturates
348 /// at `u64::MAX`.
349 ///
350 /// Only an Unconfirmed-Request may go to a broadcast network address
351 /// (Clause 6.3). Anything else belongs to one peer's transaction, so the
352 /// router neither passes it on to any network nor delivers it locally,
353 /// and answers it with no reject. The PDU type comes from the first APDU
354 /// octet, with no decode; network messages are not affected.
355 pub fn broadcast_pdu_type_drops(&self) -> u64 {
356 self.counters().broadcast_pdu_type_drops
357 }
358
359 /// NPDUs dropped on any port since start because the router would have
360 /// delivered them, on a directly connected port, to a DADR that is a
361 /// group destination there, such as a B/IP multicast address, and their
362 /// APDU isn't an Unconfirmed-Request (#1504). Saturates at `u64::MAX`.
363 ///
364 /// Sent as one unicast, such an NPDU would reach every node in the
365 /// group without the broadcast network addresses that
366 /// [`Self::broadcast_pdu_type_drops`] filters: a confirmed request would
367 /// draw an answer from each node, and an answer would reach nodes that
368 /// asked nothing. The port's rule is
369 /// [`TransportPort::group_destinations`],
370 /// so a custom transport that keeps the default recognizes none. The
371 /// router delivers it nowhere and answers it with no reject. A network
372 /// message routed to such a DADR, or a Reject-Message-To-Network relayed
373 /// to one, is dropped and counted too: it must not reach every router on
374 /// that network as a broadcast. An Unconfirmed-Request for one of the
375 /// groups the port's broadcast stands for
376 /// ([`TransportPort::local_broadcast_destinations`]) isn't counted: it
377 /// goes out on that port as a local broadcast, and reaches this router's
378 /// application where a DLEN 0 broadcast would (#1677). On B/IP, one for a
379 /// group at another UDP port, another B/IP network, is counted.
380 pub fn group_dadr_drops(&self) -> u64 {
381 self.counters().group_dadr_drops
382 }
383
384 async fn start_dispatch<T: TransportPort + 'static>(
385 mut ports: Vec<RouterPort<T>>,
386 track_depth: bool,
387 control: Arc<control_policy::ControlGate>,
388 network_control: Option<AdmissionSender<ReceivedNetworkControl>>,
389 engine_options: bacnet_router_core::router::Options,
390 ) -> Result<(Self, mpsc::Receiver<ReceivedApdu>, QueueAdmissionCounters), Error> {
391 if ports.is_empty() || ports.len() > PORTS {
392 return Err(Error::Encoding(format!(
393 "A router needs 1 to {PORTS} ports, not {}",
394 ports.len()
395 )));
396 }
397 // Refuse reserved and duplicate network numbers
398 {
399 let mut seen = std::collections::HashSet::new();
400 for port in &ports {
401 if matches!(port.network_number, 0 | 0xFFFF) {
402 return Err(Error::Encoding(format!(
403 "A router port can't be on network {}: ports take 1 to 65534",
404 port.network_number
405 )));
406 }
407 if !seen.insert(port.network_number) {
408 return Err(Error::Encoding(format!(
409 "Duplicate network number {} in router ports",
410 port.network_number
411 )));
412 }
413 }
414 }
415
416 let (local_tx, local_rx, counters) = AdmissionReceiver::channel(track_depth);
417
418 // Start each transport, and note what the engine and its hooks need.
419 let mut port_receivers = Vec::new();
420 let mut configs = Vec::new();
421 let mut networks = Vec::new();
422 let mut limit_changes = Vec::new();
423
424 for port in &mut ports {
425 // A routing node may owe its data link more than other nodes:
426 // MS/TP turns on extended frames here (Clause 9.3).
427 port.transport.prepare_for_routing();
428 let rx = port.transport.start().await?;
429 port_receivers.push(rx);
430 let mac = Mac::from_slice(port.transport.local_mac()).map_err(|_| {
431 Error::Encoding(format!(
432 "Router port on network {} has a MAC longer than {} octets",
433 port.network_number,
434 bacnet_npdu::MAX_MAC_LEN
435 ))
436 })?;
437 // Read once the transport has started: MS/TP's grows with the
438 // extended frames routing turned on, and BACnet/SC's comes from
439 // its hub connection.
440 configs.push(Engine::port(
441 port.network_number,
442 mac,
443 port.transport.max_npdu(),
444 PortGroups {
445 groups: port.transport.group_destinations(),
446 local_broadcasts: port.transport.local_broadcast_destinations(),
447 },
448 ));
449 // Taken after that read; the task that follows it applies the
450 // value it holds first, so a change in between isn't lost.
451 limit_changes.push(port.transport.max_npdu_changes());
452 networks.push(port.network_number);
453 }
454 let engine = Engine::new(&configs, tokio_clock(), engine_options)
455 .map_err(|error| Error::Encoding(error.to_string()))?;
456
457 let network_control_ingress_sequence = Arc::new(AtomicU64::new(0));
458 let shared = Arc::new(Shared {
459 engine,
460 gate: control,
461 networks,
462 warnings: Default::default(),
463 });
464
465 let (send_txs, send_rxs): (Vec<_>, Vec<_>) = (0..ports.len())
466 .map(|_| send_queue::channel(send_queue::CAPACITY))
467 .unzip();
468 let replies = Arc::new(replies::ReplyLifetime::default());
469 let outputs = Arc::new(Outputs {
470 replies: Arc::clone(&replies),
471 send_txs,
472 local_tx,
473 local_control: local_control::LocalControl::new(
474 network_control,
475 Arc::clone(&network_control_ingress_sequence),
476 ),
477 });
478 // Senders may return a late rejection through these queues, but
479 // must not keep them or the local consumers alive after dispatch.
480 let sender_tasks = ports
481 .into_iter()
482 .zip(send_rxs)
483 .enumerate()
484 .map(|(index, (port, send_rx))| {
485 tokio::spawn(sender::run(
486 port.transport,
487 // At most 255 ports, checked above.
488 PortId(index as u8),
489 send_rx,
490 Arc::clone(&shared),
491 Arc::downgrade(&outputs),
492 ))
493 })
494 .collect();
495
496 // Follow the limits of the ports whose transports publish them.
497 let limit_tasks = limit_changes
498 .into_iter()
499 .enumerate()
500 .filter_map(|(index, changes)| {
501 // At most 255 ports, checked above.
502 let port = PortId(index as u8);
503 let task = limits::follow(Arc::clone(&shared), port, changes?);
504 Some(tokio::spawn(task))
505 })
506 .collect();
507
508 // Startup: each port hears its own network number, in a
509 // Network-Number-Is (R3, Clause 6.4.20), then, in an I-Am-Router,
510 // which networks lie beyond the other ports.
511 shared.announce(&outputs.send_txs);
512
513 let dispatch_tasks = port_receivers
514 .into_iter()
515 .enumerate()
516 .map(|(index, rx)| {
517 let port = PortDispatch {
518 shared: Arc::clone(&shared),
519 outputs: Arc::clone(&outputs),
520 // At most 255 ports, checked above.
521 port: index as u8,
522 };
523 tokio::spawn(port.run(rx))
524 })
525 .collect();
526
527 // Run the engine's sweep at each of its deadlines: it purges stale
528 // learned routes, lapsed busy marks, hardened pending slots and
529 // discovery entries. `last_used` never extends `last_seen` expiry.
530 //
531 // The task sleeps until the deadline it last read, and nothing
532 // wakes it early. That holds because only the sweep itself
533 // (`on_deadline`) moves the engine's `next_deadline`; if the core
534 // ever lets another call move it, that call's site must wake this
535 // task (a `Notify`, say), or the sweep runs late.
536 let aging = Arc::clone(&shared);
537 let aging_task = tokio::spawn(async move {
538 let clock = aging.engine.clock();
539 // A deadline past the host clock's range never comes.
540 let wake = |next: Option<Instant>| next.and_then(|at| clock.to_std(at));
541 let mut next = wake(aging.engine.lock().next_deadline());
542 loop {
543 match next {
544 Some(at) => tokio::time::sleep_until(at.into()).await,
545 None => std::future::pending().await,
546 }
547 next = wake(aging.sweep());
548 }
549 });
550
551 Ok((
552 Self {
553 replies,
554 outputs: Arc::downgrade(&outputs),
555 shared,
556 dispatch_tasks,
557 sender_tasks,
558 aging_task: Some(aging_task),
559 limit_tasks,
560 network_control_ingress_sequence,
561 },
562 local_rx,
563 counters,
564 ))
565 }
566
567 /// The routing table, locked for local management: read routes, or
568 /// change them with no wire-control policy in the way.
569 ///
570 /// Every port's dispatch waits while the guard lives, so drop it soon,
571 /// and never hold it across an await. Times in it are the router's own
572 /// ([`Self::now`]).
573 ///
574 /// Don't call it again while a guard lives on the same thread, or from
575 /// a [`ControlAuthorizer`]: the lock isn't reentrant, so that call
576 /// never returns. The counts, [`Self::now`] and
577 /// [`Self::control_snapshot`] take no lock and are safe to call while
578 /// the guard lives.
579 pub fn table(&self) -> RouterTableGuard<'_> {
580 RouterTableGuard {
581 router: self.shared.engine.lock(),
582 }
583 }
584
585 /// The router's time now: the clock its table and timers run on, Tokio
586 /// time since the router started.
587 pub fn now(&self) -> Instant {
588 self.shared.engine.now()
589 }
590
591 /// Stop the router.
592 ///
593 /// Aborts and awaits dispatch, sender, aging and limit tasks without
594 /// waiting for the local application consumer, even with a full queue.
595 /// Queued local APDUs remain drainable, retaining their reply senders
596 /// until consumed or the receiver is dropped. Stop is not an admission
597 /// drop and does not clear depth/high-water/drop totals. See the
598 /// [receive-queue contract](crate::layer#receive-queue-admission).
599 pub async fn stop(&mut self) {
600 self.replies.seal();
601 for task in self.dispatch_tasks.drain(..) {
602 task.abort();
603 let _ = task.await;
604 }
605 for task in self.sender_tasks.drain(..) {
606 task.abort();
607 let _ = task.await;
608 }
609 if let Some(task) = self.aging_task.take() {
610 task.abort();
611 let _ = task.await;
612 }
613 for task in self.limit_tasks.drain(..) {
614 task.abort();
615 let _ = task.await;
616 }
617 // Cancel pending unknown-destination discovery so no further
618 // solicitation is emitted and coalesced waiters observe shutdown.
619 self.shared.engine.lock().cancel_discovery();
620 }
621
622 /// How many unknown networks the router is looking for.
623 #[cfg(test)]
624 fn searches(&self) -> (usize, bool) {
625 let router = self.shared.engine.lock();
626 (router.discovery().len(), router.discovery().is_cancelled())
627 }
628}
629
630#[cfg(test)]
631mod admission_tests;
632#[cfg(test)]
633mod bip_group_tests;
634#[cfg(test)]
635mod busy_scope_tests;
636#[cfg(test)]
637mod claim_tests;
638#[cfg(test)]
639mod congestion_tests;
640#[cfg(test)]
641mod corroboration_tests;
642#[cfg(test)]
643mod cutover_tests;
644#[cfg(test)]
645mod driven_port;
646#[cfg(test)]
647mod envelope_control_tests;
648#[cfg(test)]
649mod envelope_discovery_tests;
650#[cfg(test)]
651mod envelope_harness;
652#[cfg(test)]
653mod fixes_tests;
654#[cfg(test)]
655mod hardening_tests;
656#[cfg(test)]
657mod ingress_harness;
658#[cfg(test)]
659mod init_routing_table_tests;
660#[cfg(test)]
661mod late_refusal_tests;
662#[cfg(test)]
663mod limit_tests;
664#[cfg(test)]
665mod mstp_extended_tests;
666#[cfg(test)]
667mod originate_tests;
668#[cfg(test)]
669mod priority_harness;
670#[cfg(test)]
671mod priority_tests;
672#[cfg(test)]
673mod r7_reply_tests;
674#[cfg(test)]
675mod rb06_convergence_tests;
676#[cfg(test)]
677mod rb09_control_policy_tests;
678#[cfg(test)]
679mod reject_tests;
680#[cfg(test)]
681mod tests;
682
683impl Drop for BACnetRouter {
684 fn drop(&mut self) {
685 self.replies.seal();
686 }
687}