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 super::control_policy::{ControlAuthorizer, ControlGate, ControlPolicy};
14use super::BACnetRouter;
15use crate::layer::{
16 AdmissionReceiver, QueueAdmissionCounters, ReceivedApdu, ReceivedNetworkControl,
17};
18
19/// How [`BACnetRouter::start`] sets up a router.
20///
21/// [`Self::new`] gives the plain router: a raw local APDU receiver, the
22/// permissive wire-control policy with no authorizer, and no network-control
23/// receiver. Each builder method turns on one option, and they combine freely.
24///
25/// `A` is the local APDU receiver type the start returns, a raw
26/// [`mpsc::Receiver`] unless [`Self::track_admission`] switches it to an
27/// [`AdmissionReceiver`]. `C` is the network-control receiver type in the same
28/// way: raw, unless [`Self::network_control_receiver_with_admission`] asks for
29/// the tracked one.
30///
31/// ```no_run
32/// # use bacnet_network::router::{BACnetRouter, RouterOptions, RouterPort, StartedRouter};
33/// # use bacnet_network::router::control_policy::ControlPolicy;
34/// # use bacnet_transport::loopback::LoopbackTransport;
35/// # async fn run(ports: Vec<RouterPort<LoopbackTransport>>) -> Result<(), bacnet_types::error::Error> {
36/// let options = RouterOptions::new()
37/// .track_admission()
38/// .control_policy(ControlPolicy::Hardened)
39/// .network_control_receiver();
40/// let StartedRouter { router, apdus, network_control } =
41/// BACnetRouter::start(ports, options).await?;
42/// let counters = apdus.counters();
43/// # Ok(())
44/// # }
45/// ```
46pub struct RouterOptions<
47 A = mpsc::Receiver<ReceivedApdu>,
48 C = mpsc::Receiver<ReceivedNetworkControl>,
49> {
50 policy: ControlPolicy,
51 authorizer: Option<ControlAuthorizer>,
52 network_control: bool,
53 receivers: PhantomData<fn() -> (A, C)>,
54}
55
56impl RouterOptions {
57 /// The plain router, with every option off.
58 ///
59 /// Its local APDU receiver is a raw [`mpsc::Receiver`]. All ports share
60 /// one 256-item local queue. Full/Closed admission drops the arriving APDU
61 /// with its payload, metadata and reply sender without sending reply bytes
62 /// or a wire rejection, never evicting an older item. Admission never
63 /// waits for the consumer or stops forwarding/inline control handling.
64 /// This raw receiver has no per-source quota, admission snapshot or
65 /// depth/high-water tracking. Full/Closed drops are counted internally,
66 /// with Closed > Full precedence.
67 ///
68 /// Closing retains queued items; dropping discards them. Both leave
69 /// routing active, and each later local admission attempt counts as
70 /// Closed. [`BACnetRouter::stop`] also leaves queued items drainable. Use
71 /// [`Self::track_admission`] for snapshots and per-source fairness; see the
72 /// [receive-queue contract](crate::layer#receive-queue-admission) for the
73 /// raw/tracked matrix and ownership/lifecycle details.
74 pub fn new() -> Self {
75 Self {
76 policy: ControlPolicy::default(),
77 authorizer: None,
78 network_control: false,
79 receivers: PhantomData,
80 }
81 }
82}
83
84impl Default for RouterOptions {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90impl<A, C> RouterOptions<A, C> {
91 /// Return the local APDU receiver as an [`AdmissionReceiver`], which
92 /// exposes queue-admission snapshots and enforces a per-source quota.
93 ///
94 /// Routing and the router's lifecycle are unchanged. All ports share one
95 /// 256-item queue and one accounting state; [`AdmissionReceiver::counters`]
96 /// returns cloneable count-only handles for depth/high-water and
97 /// admission-drop totals, readable after receiver or router drop. Failed
98 /// admission drops the arriving APDU, never an older one, and releases its
99 /// payload, metadata and reply sender without reply bytes or wire
100 /// rejections. Forwarding and inline control handling remain independent.
101 /// Closing the receiver retains queued items for draining and counts each
102 /// subsequent local arrival as a closed drop while forwarding continues.
103 /// [`BACnetRouter::stop`] also leaves queued items drainable. Dropping the
104 /// receiver discards queued items and releases their reply channels
105 /// without sending reply bytes; discarding queued items is not an
106 /// admission drop.
107 ///
108 /// Each ([ingress port network number](ReceivedApdu::ingress_network),
109 /// complete [`source_mac`](ReceivedApdu::source_mac) byte value) may hold
110 /// at most 16 queued local APDUs, never keyed by routed NPDU SNET/SADR.
111 /// Identical MAC bytes on different ports have separate quotas.
112 /// Dequeuing releases one slot, even if the consumer retains the APDU.
113 /// **Closed > fairness > Full** selects one drop reason; over-quota
114 /// arrivals increment
115 /// [`fairness_drops`](crate::layer::QueueAdmissionSnapshot::fairness_drops)
116 /// even if the queue is also globally full, unless admission is closed.
117 /// The raw receiver from [`RouterOptions::new`] does not enforce a quota.
118 /// See the [receive-queue contract](crate::layer#receive-queue-admission)
119 /// for exact keys, the raw/tracked matrix and complete ownership/lifecycle
120 /// rules.
121 pub fn track_admission(self) -> RouterOptions<AdmissionReceiver<ReceivedApdu>, C> {
122 self.receivers()
123 }
124
125 /// RB-09: the wire-control policy for state-changing routing controls.
126 ///
127 /// [`ControlPolicy::Permissive`], the default, admits them.
128 /// [`ControlPolicy::Hardened`] denies a protected control unless an
129 /// authorizer approves it, as a silent drop. Read the totals with
130 /// [`BACnetRouter::control_snapshot`].
131 pub fn control_policy(mut self, policy: ControlPolicy) -> Self {
132 self.policy = policy;
133 self
134 }
135
136 /// RB-09: a callback that decides every protected wire control, whatever
137 /// the [policy](Self::control_policy). See [`ControlAuthorizer`] for what
138 /// it may and may not do.
139 pub fn control_authorizer(mut self, authorizer: ControlAuthorizer) -> Self {
140 self.authorizer = Some(authorizer);
141 self
142 }
143
144 /// Also return the router's own network-control receiver (#1175), in
145 /// [`StartedRouter::network_control`].
146 ///
147 /// The receiver gets each Reject-Message-To-Network addressed to the
148 /// router itself: one with no DNET, or whose DADR is the router's MAC on
149 /// the port attached to that DNET. Such a reject still updates the routing
150 /// table and is never relayed. Without this receiver, the table update is
151 /// all that happens. Every other network message is still handled inline.
152 ///
153 /// Records match what a non-router
154 /// [`NetworkLayer::enable_network_control_receiver`](crate::layer::NetworkLayer::enable_network_control_receiver)
155 /// delivers, numbered from [`BACnetRouter::network_control_ingress_sequence`].
156 /// The queue holds 256 controls, apart from the APDU queue. Admission never
157 /// waits: a full or closed receiver drops the arriving control, and
158 /// routing carries on. This receiver is a raw [`mpsc::Receiver`], which
159 /// counts its drops internally only; use
160 /// [`Self::network_control_receiver_with_admission`] to read them. The two
161 /// are alternatives, and the later call decides the receiver type.
162 pub fn network_control_receiver(
163 mut self,
164 ) -> RouterOptions<A, mpsc::Receiver<ReceivedNetworkControl>> {
165 self.network_control = true;
166 self.receivers()
167 }
168
169 /// [`Self::network_control_receiver`], with the receiver as an
170 /// [`AdmissionReceiver`] that reports on its queue (#1242), as
171 /// [`NetworkLayer::enable_network_control_receiver_with_admission`](crate::layer::NetworkLayer::enable_network_control_receiver_with_admission)
172 /// does for a non-router.
173 ///
174 /// [`AdmissionReceiver::counters`] reads the queue's depth, its high-water
175 /// mark and its full and closed drop totals. Controls have no per-source
176 /// quota, so the fairness total stays zero. The records, the 256-control
177 /// queue and the drop-arriving admission are those of the raw receiver;
178 /// see the [receive-queue contract](crate::layer#receive-queue-admission)
179 /// for what closing, dropping and stopping do to the counts.
180 pub fn network_control_receiver_with_admission(
181 mut self,
182 ) -> RouterOptions<A, AdmissionReceiver<ReceivedNetworkControl>> {
183 self.network_control = true;
184 self.receivers()
185 }
186
187 /// The wire-control gate these options select.
188 pub(super) fn control_gate(&self) -> ControlGate {
189 ControlGate::new(self.policy, self.authorizer.clone())
190 }
191
192 /// Whether a network-control receiver was asked for.
193 pub(super) fn wants_network_control(&self) -> bool {
194 self.network_control
195 }
196
197 /// These options, returning other receiver types.
198 fn receivers<A2, C2>(self) -> RouterOptions<A2, C2> {
199 RouterOptions {
200 policy: self.policy,
201 authorizer: self.authorizer,
202 network_control: self.network_control,
203 receivers: PhantomData,
204 }
205 }
206}
207
208impl<A, C> fmt::Debug for RouterOptions<A, C> {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 f.debug_struct("RouterOptions")
211 .field("policy", &self.policy)
212 .field(
213 "authorizer",
214 &self.authorizer.as_ref().map(|_| "<callback>"),
215 )
216 .field("network_control", &self.network_control)
217 .field("apdus", &std::any::type_name::<A>())
218 .field("controls", &std::any::type_name::<C>())
219 .finish()
220 }
221}
222
223/// A running router and the receivers its [`RouterOptions`] asked for.
224pub struct StartedRouter<
225 A = mpsc::Receiver<ReceivedApdu>,
226 C = mpsc::Receiver<ReceivedNetworkControl>,
227> {
228 /// The router.
229 pub router: BACnetRouter,
230 /// APDUs for local applications: those without a remote destination, or
231 /// for which this router is the final hop.
232 pub apdus: A,
233 /// The router's own network-control receiver, when
234 /// [`RouterOptions::network_control_receiver`] or
235 /// [`RouterOptions::network_control_receiver_with_admission`] asked for it.
236 pub network_control: Option<C>,
237}
238
239/// A local APDU receiver type [`BACnetRouter::start`] can return: the raw
240/// [`mpsc::Receiver`] or, after [`RouterOptions::track_admission`], an
241/// [`AdmissionReceiver`]. Sealed.
242pub trait LocalApduReceiver: sealed::Sealed<ReceivedApdu> {}
243
244impl LocalApduReceiver for mpsc::Receiver<ReceivedApdu> {}
245impl LocalApduReceiver for AdmissionReceiver<ReceivedApdu> {}
246
247/// A network-control receiver type [`BACnetRouter::start`] can return: the
248/// raw [`mpsc::Receiver`] from [`RouterOptions::network_control_receiver`] or
249/// the [`AdmissionReceiver`] from
250/// [`RouterOptions::network_control_receiver_with_admission`]. Sealed.
251pub trait NetworkControlReceiver: sealed::Sealed<ReceivedNetworkControl> {}
252
253impl NetworkControlReceiver for mpsc::Receiver<ReceivedNetworkControl> {}
254impl NetworkControlReceiver for AdmissionReceiver<ReceivedNetworkControl> {}
255
256pub(super) mod sealed {
257 use super::*;
258
259 /// How the start builds each receiver type from its queue of `T`.
260 pub trait Sealed<T>: Sized {
261 /// Whether the queue keeps depth accounting (and, for APDUs,
262 /// per-source accounting).
263 const TRACKED: bool;
264
265 /// Wrap the queue's receiving end.
266 fn from_queue(rx: mpsc::Receiver<T>, counters: QueueAdmissionCounters) -> Self;
267 }
268
269 impl<T> Sealed<T> for mpsc::Receiver<T> {
270 const TRACKED: bool = false;
271
272 fn from_queue(rx: mpsc::Receiver<T>, _: QueueAdmissionCounters) -> Self {
273 rx
274 }
275 }
276
277 impl Sealed<ReceivedApdu> for AdmissionReceiver<ReceivedApdu> {
278 const TRACKED: bool = true;
279
280 fn from_queue(rx: mpsc::Receiver<ReceivedApdu>, counters: QueueAdmissionCounters) -> Self {
281 AdmissionReceiver::from_apdu_parts(rx, counters)
282 }
283 }
284
285 impl Sealed<ReceivedNetworkControl> for AdmissionReceiver<ReceivedNetworkControl> {
286 const TRACKED: bool = true;
287
288 fn from_queue(
289 rx: mpsc::Receiver<ReceivedNetworkControl>,
290 counters: QueueAdmissionCounters,
291 ) -> Self {
292 AdmissionReceiver::from_parts(rx, counters)
293 }
294 }
295}
296
297#[cfg(test)]
298#[path = "options_tests.rs"]
299mod tests;