Skip to main content

bacnet_network/router/
options.rs

1//! What [`BACnetRouter::start`] sets up, and what it hands back (#1220).
2//!
3//! A router can be started with a tracked local APDU receiver, a wire-control
4//! policy and authorizer, and its own network-control receiver, raw or tracked
5//! (#1242), in any combination. [`RouterOptions`] picks them and
6//! [`StartedRouter`] carries the receivers that were asked for.
7
8use std::fmt;
9use std::marker::PhantomData;
10
11use tokio::sync::mpsc;
12
13use bacnet_router_core::router::{Congestion, Duplicates, Options};
14
15use super::BACnetRouter;
16use super::control_policy::{ControlAuthorizer, ControlGate, ControlPolicy};
17use super::engine::OPTIONS;
18use crate::layer::{
19    AdmissionReceiver, QueueAdmissionCounters, ReceivedApdu, ReceivedNetworkControl,
20};
21
22/// How [`BACnetRouter::start`] sets up a router.
23///
24/// [`Self::new`] gives the plain router: a raw local APDU receiver, the
25/// permissive wire-control policy with no authorizer, and no network-control
26/// receiver. Each builder method turns on one option, and they combine freely.
27///
28/// `A` is the local APDU receiver type the start returns, a raw
29/// [`mpsc::Receiver`] unless [`Self::track_admission`] switches it to an
30/// [`AdmissionReceiver`]. `C` is the network-control receiver type in the same
31/// way: raw, unless [`Self::network_control_receiver_with_admission`] asks for
32/// the tracked one.
33///
34/// ```no_run
35/// # use bacnet_network::router::{BACnetRouter, RouterOptions, RouterPort, StartedRouter};
36/// # use bacnet_network::router::control_policy::ControlPolicy;
37/// # use bacnet_transport::loopback::LoopbackTransport;
38/// # async fn run(ports: Vec<RouterPort<LoopbackTransport>>) -> Result<(), bacnet_types::error::Error> {
39/// let options = RouterOptions::new()
40///     .track_admission()
41///     .control_policy(ControlPolicy::Hardened)
42///     .network_control_receiver();
43/// let StartedRouter { router, apdus, network_control } =
44///     BACnetRouter::start(ports, options).await?;
45/// let counters = apdus.counters();
46/// # Ok(())
47/// # }
48/// ```
49pub struct RouterOptions<
50    A = mpsc::Receiver<ReceivedApdu>,
51    C = mpsc::Receiver<ReceivedNetworkControl>,
52> {
53    policy: ControlPolicy,
54    authorizer: Option<ControlAuthorizer>,
55    network_control: bool,
56    engine: Options,
57    receivers: PhantomData<fn() -> (A, C)>,
58}
59
60impl RouterOptions {
61    /// The plain router, with every option off.
62    ///
63    /// Its local APDU receiver is a raw [`mpsc::Receiver`]. All ports share
64    /// one 256-item local queue. Full/Closed admission drops the arriving APDU
65    /// with its payload, metadata and reply sender without sending reply bytes
66    /// or a wire rejection, never evicting an older item. Admission never
67    /// waits for the consumer or stops forwarding/inline control handling.
68    /// This raw receiver has no per-source quota, admission snapshot or
69    /// depth/high-water tracking. Full/Closed drops are counted internally,
70    /// with Closed > Full precedence.
71    ///
72    /// Closing retains queued items; dropping discards them. Both leave
73    /// routing active, and each later local admission attempt counts as
74    /// Closed. [`BACnetRouter::stop`] also leaves queued items drainable. Use
75    /// [`Self::track_admission`] for snapshots and per-source fairness; see the
76    /// [receive-queue contract](crate::layer#receive-queue-admission) for the
77    /// raw/tracked matrix and ownership/lifecycle details.
78    pub fn new() -> Self {
79        Self {
80            policy: ControlPolicy::default(),
81            authorizer: None,
82            network_control: false,
83            engine: OPTIONS,
84            receivers: PhantomData,
85        }
86    }
87}
88
89impl Default for RouterOptions {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95impl<A, C> RouterOptions<A, C> {
96    /// Return the local APDU receiver as an [`AdmissionReceiver`], which
97    /// exposes queue-admission snapshots and enforces a per-source quota.
98    ///
99    /// Routing and the router's lifecycle are unchanged. All ports share one
100    /// 256-item queue and one accounting state; [`AdmissionReceiver::counters`]
101    /// returns cloneable count-only handles for depth/high-water and
102    /// admission-drop totals, readable after receiver or router drop. Failed
103    /// admission drops the arriving APDU, never an older one, and releases its
104    /// payload, metadata and reply sender without reply bytes or wire
105    /// rejections. Forwarding and inline control handling remain independent.
106    /// Closing the receiver retains queued items for draining and counts each
107    /// subsequent local arrival as a closed drop while forwarding continues.
108    /// [`BACnetRouter::stop`] also leaves queued items drainable. Dropping the
109    /// receiver discards queued items and releases their reply channels
110    /// without sending reply bytes; discarding queued items is not an
111    /// admission drop.
112    ///
113    /// Each ([ingress port network number](ReceivedApdu::ingress_network),
114    /// complete [`source_mac`](ReceivedApdu::source_mac) byte value) may hold
115    /// at most 16 queued local APDUs, never keyed by routed NPDU SNET/SADR.
116    /// Identical MAC bytes on different ports have separate quotas.
117    /// Dequeuing releases one slot, even if the consumer retains the APDU.
118    /// **Closed > fairness > Full** selects one drop reason; over-quota
119    /// arrivals increment
120    /// [`fairness_drops`](crate::layer::QueueAdmissionSnapshot::fairness_drops)
121    /// even if the queue is also globally full, unless admission is closed.
122    /// The raw receiver from [`RouterOptions::new`] does not enforce a quota.
123    /// See the [receive-queue contract](crate::layer#receive-queue-admission)
124    /// for exact keys, the raw/tracked matrix and complete ownership/lifecycle
125    /// rules.
126    pub fn track_admission(self) -> RouterOptions<AdmissionReceiver<ReceivedApdu>, C> {
127        self.receivers()
128    }
129
130    /// RB-09: the wire-control policy for state-changing routing controls.
131    ///
132    /// [`ControlPolicy::Permissive`], the default, admits them.
133    /// [`ControlPolicy::Hardened`] denies a protected control unless an
134    /// authorizer approves it, as a silent drop. Read the totals with
135    /// [`BACnetRouter::control_snapshot`].
136    pub fn control_policy(mut self, policy: ControlPolicy) -> Self {
137        self.policy = policy;
138        self
139    }
140
141    /// RB-09: a callback that decides every protected wire control, whatever
142    /// the [policy](Self::control_policy). See [`ControlAuthorizer`] for what
143    /// it may and may not do.
144    pub fn control_authorizer(mut self, authorizer: ControlAuthorizer) -> Self {
145        self.authorizer = Some(authorizer);
146        self
147    }
148
149    /// Also return the router's own network-control receiver (#1175), in
150    /// [`StartedRouter::network_control`].
151    ///
152    /// The receiver gets each Reject-Message-To-Network addressed to the
153    /// router itself: one with no DNET, or whose DADR is the router's MAC on
154    /// the port attached to that DNET. Such a reject still updates the routing
155    /// table and is never relayed. Without this receiver, the table update is
156    /// all that happens. Every other network message is still handled inline.
157    ///
158    /// Records match what a non-router
159    /// [`NetworkLayer::enable_network_control_receiver`](crate::layer::NetworkLayer::enable_network_control_receiver)
160    /// delivers, numbered from [`BACnetRouter::network_control_ingress_sequence`].
161    /// The queue holds 256 controls, apart from the APDU queue. Admission never
162    /// waits: a full or closed receiver drops the arriving control, and
163    /// routing carries on. This receiver is a raw [`mpsc::Receiver`], which
164    /// counts its drops internally only; use
165    /// [`Self::network_control_receiver_with_admission`] to read them. The two
166    /// are alternatives, and the later call decides the receiver type.
167    pub fn network_control_receiver(
168        mut self,
169    ) -> RouterOptions<A, mpsc::Receiver<ReceivedNetworkControl>> {
170        self.network_control = true;
171        self.receivers()
172    }
173
174    /// [`Self::network_control_receiver`], with the receiver as an
175    /// [`AdmissionReceiver`] that reports on its queue (#1242), as
176    /// [`NetworkLayer::enable_network_control_receiver_with_admission`](crate::layer::NetworkLayer::enable_network_control_receiver_with_admission)
177    /// does for a non-router.
178    ///
179    /// [`AdmissionReceiver::counters`] reads the queue's depth, its high-water
180    /// mark and its full and closed drop totals. Controls have no per-source
181    /// quota, so the fairness total stays zero. The records, the 256-control
182    /// queue and the drop-arriving admission are those of the raw receiver;
183    /// see the [receive-queue contract](crate::layer#receive-queue-admission)
184    /// for what closing, dropping and stopping do to the counts.
185    pub fn network_control_receiver_with_admission(
186        mut self,
187    ) -> RouterOptions<A, AdmissionReceiver<ReceivedNetworkControl>> {
188        self.network_control = true;
189        self.receivers()
190    }
191
192    /// The router's own Router-Busy for a clogged port (#1697): off unless
193    /// this sets a policy.
194    ///
195    /// Each port queues at most 256 sends. With a policy set, a port whose
196    /// queue stays full for [`Congestion::clog_after`] counts as clogged:
197    /// the router broadcasts Router-Busy-To-Network on its other ports for
198    /// the networks it reaches through that one, answers each send the full
199    /// queue then refuses, when it has an origin to answer, with
200    /// Reject-Message-To-Network reason 2, and broadcasts
201    /// Router-Available-To-Network once the queue falls under the policy's
202    /// low-water mark (Clauses 6.6.3.6, 6.6.3.7, 6.6.4).
203    /// [`Congestion::DEFAULT`] clogs after 2 s and drains under half.
204    /// The counts are in [`Counters`](super::Counters).
205    pub fn congestion(mut self, policy: Congestion) -> Self {
206        self.engine.congestion = Some(policy);
207        self
208    }
209
210    /// Apply a received Reject-Message-To-Network to a learned route only
211    /// when it came from that route's next router (#1698).
212    ///
213    /// Off by default: Clause 6.6.3.5 has a router update the route a
214    /// reject names whoever sent it, so a reject relayed by any router
215    /// changes it. On, one from another router is still passed on toward
216    /// its destination, but leaves the route as it was, and is counted in
217    /// [`Counters::not_next_hop`](super::Counters::not_next_hop).
218    /// Router-Busy and Router-Available are always scoped to the routes
219    /// through their sender.
220    pub fn next_hop_rejects(mut self) -> Self {
221        self.engine.next_hop_rejects = true;
222        self
223    }
224
225    /// Drop a global broadcast that repeats one seen within
226    /// [`Duplicates::ttl`] (#1698): off unless this sets it.
227    ///
228    /// In a topology with a loop, routers can pass one global broadcast
229    /// around it until its hop count runs out. With this on, the router
230    /// remembers the last 512 global broadcasts by origin (SNET and SADR,
231    /// or the arrival network and link source) and a hash of the APDU,
232    /// and drops a repeat seen within the time to live, 1 s for
233    /// [`Duplicates::DEFAULT`]: not relayed, not delivered, counted in
234    /// [`Counters::duplicate_drops`](super::Counters::duplicate_drops).
235    /// The standard defines no such suppression, and a sender's own
236    /// identical retry within the time to live is dropped too.
237    ///
238    /// The same record covers the network messages the router passes on
239    /// with no origin, I-Am-Router, Router-Busy and Router-Available
240    /// (#1810), which carry no hop count, so around a loop nothing else
241    /// stops them. A repeat within the time to live is neither applied to
242    /// the table nor passed on. An I-Am-Router is kept by its data alone,
243    /// so two routers announcing the same networks within one time to live
244    /// look like one, and the second is dropped until it announces again.
245    /// A Router-Busy or Router-Available is kept by its sender (arrival
246    /// network and link source) and data, so another router's identical
247    /// message is still applied, and accepting one forgets its opposite
248    /// from the same sender, so Busy, Available, Busy within one time to
249    /// live applies all three; at most four such resets per sender within
250    /// one time to live, so a pair looping round a ring still ends. A
251    /// Who-Is-Router the router passes on for a network it doesn't know
252    /// (#2020) is kept, like a global broadcast, by the origin its SNET and
253    /// SADR name, and its data.
254    ///
255    /// Without this option too, a global broadcast or Who-Is-Router whose
256    /// SNET and SADR name the router itself is dropped, as its own come
257    /// back round a loop, and counted in
258    /// [`Counters::own_source_drops`](super::Counters::own_source_drops)
259    /// (#2021).
260    ///
261    /// Entries are keyed by origin, so in a looped site this router's own
262    /// global broadcast sent from each port's own address (a bare copy per
263    /// port) is a different origin per port to the other routers, and may
264    /// reach their applications once per port. Send it from one port's
265    /// address there
266    /// ([`SourceSelection::Port`](super::SourceSelection::Port)), so every
267    /// copy names one origin.
268    ///
269    /// Sizing: the record must hold every global broadcast the router sees
270    /// within one time to live, or the oldest is forgotten before its
271    /// looped copy returns. 512 covers 512 broadcasts a second at the
272    /// default; a longer time to live needs proportionally fewer a second.
273    /// The relayed Who-Is-Router, I-Am-Router, Router-Busy and
274    /// Router-Available entries share those 512 slots with the global
275    /// broadcasts, the flow-control ones one per sender and message.
276    pub fn duplicates(mut self, policy: Duplicates) -> Self {
277        self.engine.duplicates = Some(policy);
278        self
279    }
280
281    /// The engine's options these select.
282    pub(super) fn engine_options(&self) -> Options {
283        self.engine
284    }
285
286    /// The wire-control gate these options select.
287    pub(super) fn control_gate(&self) -> ControlGate {
288        ControlGate::new(self.policy, self.authorizer.clone())
289    }
290
291    /// Whether a network-control receiver was asked for.
292    pub(super) fn wants_network_control(&self) -> bool {
293        self.network_control
294    }
295
296    /// These options, returning other receiver types.
297    fn receivers<A2, C2>(self) -> RouterOptions<A2, C2> {
298        RouterOptions {
299            policy: self.policy,
300            authorizer: self.authorizer,
301            network_control: self.network_control,
302            engine: self.engine,
303            receivers: PhantomData,
304        }
305    }
306}
307
308impl<A, C> fmt::Debug for RouterOptions<A, C> {
309    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310        f.debug_struct("RouterOptions")
311            .field("policy", &self.policy)
312            .field(
313                "authorizer",
314                &self.authorizer.as_ref().map(|_| "<callback>"),
315            )
316            .field("network_control", &self.network_control)
317            .field("engine", &self.engine)
318            .field("apdus", &std::any::type_name::<A>())
319            .field("controls", &std::any::type_name::<C>())
320            .finish()
321    }
322}
323
324/// A running router and the receivers its [`RouterOptions`] asked for.
325pub struct StartedRouter<
326    A = mpsc::Receiver<ReceivedApdu>,
327    C = mpsc::Receiver<ReceivedNetworkControl>,
328> {
329    /// The router.
330    pub router: BACnetRouter,
331    /// APDUs for local applications: those without a remote destination, or
332    /// for which this router is the final hop.
333    pub apdus: A,
334    /// The router's own network-control receiver, when
335    /// [`RouterOptions::network_control_receiver`] or
336    /// [`RouterOptions::network_control_receiver_with_admission`] asked for it.
337    pub network_control: Option<C>,
338}
339
340/// A local APDU receiver type [`BACnetRouter::start`] can return: the raw
341/// [`mpsc::Receiver`] or, after [`RouterOptions::track_admission`], an
342/// [`AdmissionReceiver`]. Sealed.
343pub trait LocalApduReceiver: sealed::Sealed<ReceivedApdu> + sealed::ApduQueue {}
344
345impl LocalApduReceiver for mpsc::Receiver<ReceivedApdu> {}
346impl LocalApduReceiver for AdmissionReceiver<ReceivedApdu> {}
347
348/// A network-control receiver type [`BACnetRouter::start`] can return: the
349/// raw [`mpsc::Receiver`] from [`RouterOptions::network_control_receiver`] or
350/// the [`AdmissionReceiver`] from
351/// [`RouterOptions::network_control_receiver_with_admission`]. Sealed.
352pub trait NetworkControlReceiver: sealed::Sealed<ReceivedNetworkControl> {}
353
354impl NetworkControlReceiver for mpsc::Receiver<ReceivedNetworkControl> {}
355impl NetworkControlReceiver for AdmissionReceiver<ReceivedNetworkControl> {}
356
357pub(super) mod sealed {
358    use super::*;
359
360    /// How the start builds each receiver type from its queue of `T`.
361    pub trait Sealed<T>: Sized {
362        /// Whether the queue keeps depth accounting (and, for APDUs,
363        /// per-source accounting).
364        const TRACKED: bool;
365
366        /// Wrap the queue's receiving end.
367        fn from_queue(rx: mpsc::Receiver<T>, counters: QueueAdmissionCounters) -> Self;
368    }
369
370    impl<T> Sealed<T> for mpsc::Receiver<T> {
371        const TRACKED: bool = false;
372
373        fn from_queue(rx: mpsc::Receiver<T>, _: QueueAdmissionCounters) -> Self {
374            rx
375        }
376    }
377
378    impl Sealed<ReceivedApdu> for AdmissionReceiver<ReceivedApdu> {
379        const TRACKED: bool = true;
380
381        fn from_queue(rx: mpsc::Receiver<ReceivedApdu>, counters: QueueAdmissionCounters) -> Self {
382            AdmissionReceiver::from_apdu_parts(rx, counters)
383        }
384    }
385
386    /// Either local APDU receiver, as one type, for the router's
387    /// [application port](crate::router::RouterApplicationPort).
388    pub enum ApduSource {
389        /// The raw queue.
390        Raw(mpsc::Receiver<ReceivedApdu>),
391        /// The tracked queue, whose accounting it keeps.
392        Tracked(AdmissionReceiver<ReceivedApdu>),
393    }
394
395    impl ApduSource {
396        /// The next APDU, or `None` once the queue is closed and drained.
397        pub(crate) async fn recv(&mut self) -> Option<ReceivedApdu> {
398            match self {
399                Self::Raw(rx) => rx.recv().await,
400                Self::Tracked(rx) => rx.recv().await,
401            }
402        }
403    }
404
405    /// How a local APDU receiver becomes an [`ApduSource`].
406    pub trait ApduQueue {
407        /// This receiver as an [`ApduSource`].
408        fn into_source(self) -> ApduSource;
409    }
410
411    impl ApduQueue for mpsc::Receiver<ReceivedApdu> {
412        fn into_source(self) -> ApduSource {
413            ApduSource::Raw(self)
414        }
415    }
416
417    impl ApduQueue for AdmissionReceiver<ReceivedApdu> {
418        fn into_source(self) -> ApduSource {
419            ApduSource::Tracked(self)
420        }
421    }
422
423    impl Sealed<ReceivedNetworkControl> for AdmissionReceiver<ReceivedNetworkControl> {
424        const TRACKED: bool = true;
425
426        fn from_queue(
427            rx: mpsc::Receiver<ReceivedNetworkControl>,
428            counters: QueueAdmissionCounters,
429        ) -> Self {
430            AdmissionReceiver::from_parts(rx, counters)
431        }
432    }
433}
434
435#[cfg(test)]
436#[path = "options_tests.rs"]
437mod tests;