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