bacnet_network/layer_admission.rs
1use super::*;
2use bacnet_encoding::npdu::{decode_npdu, NpduDecodeError};
3use std::collections::HashMap;
4use std::sync::Mutex;
5use std::task::Poll;
6use tokio::sync::mpsc;
7use tracing::{debug, warn};
8
9const QUEUE_CAPACITY: usize = 256;
10const APDU_SOURCE_CAPACITY: usize = 16;
11
12// NetworkLayer has one unnamed transport; router MACs are scoped to the
13// ingress port's network number, never the routed NPDU SNET/SADR.
14type AdmissionSource = (Option<u16>, MacAddr);
15
16fn apdu_source(apdu: &ReceivedApdu) -> AdmissionSource {
17 (apdu.ingress_network, apdu.source_mac.clone())
18}
19
20/// A consistent, count-only snapshot of one network-layer receive queue.
21///
22/// Counters belong to one receiver, not to a source MAC. A router's local queue
23/// is shared by all its ports. No payloads, source identities or per-source
24/// totals are exposed. The three drop totals saturate at `u64::MAX` and attribute
25/// each admission drop once, in **Closed > fairness > Full** order.
26/// Separate APDU/control snapshots are not an atomic snapshot of both queues.
27/// See the [receive-queue contract](crate::layer#receive-queue-admission) and
28/// [`QueueAdmissionCounters::snapshot`] for receiver kinds and lifetime.
29#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
30pub struct QueueAdmissionSnapshot {
31 /// Items currently queued (at most 256), excluding items already returned by
32 /// [`AdmissionReceiver::recv`] or [`AdmissionReceiver::try_recv`]. Close and
33 /// stop preserve depth until draining; dropping the receiver sets it to zero.
34 pub current_depth: usize,
35 /// Greatest queued depth since this receiver was created (at most 256).
36 /// Draining, closing, stopping or dropping the receiver does not reset it.
37 pub high_water: usize,
38 /// Arriving items dropped because the 256-item queue was full, after Closed
39 /// and any per-source fairness check; saturates at `u64::MAX`.
40 /// A fairness drop does not also increment this total.
41 pub full_drops: u64,
42 /// Arriving APDUs dropped by the tracked queue's quota of 16 per source:
43 /// complete source MAC bytes for NetworkLayer, (ingress port network number,
44 /// source MAC bytes) for routers, never routed NPDU SNET/SADR. See the
45 /// [source-key definitions](crate::layer#source-keys-and-drop-precedence).
46 /// Saturates at `u64::MAX`. Closed takes precedence; otherwise evaluated
47 /// before global Full, even when the queue has room. Always zero for control
48 /// queues, which have no per-source quota. This is a queue-wide total, not
49 /// a per-source counter.
50 pub fairness_drops: u64,
51 /// Arriving items dropped because the receiver was closed or dropped;
52 /// saturates at `u64::MAX`. Takes precedence over fairness and Full.
53 ///
54 /// In NetworkLayer, the first such APDU ends dispatch; the first such control
55 /// disables that stream. Later discarded controls are not admission attempts.
56 /// Routers keep forwarding and count each closed local-admission attempt.
57 /// Dropping already queued items with the receiver is not an admission drop.
58 pub closed_drops: u64,
59}
60
61/// Cloneable snapshot handle obtained from an [`AdmissionReceiver`].
62///
63/// This handle keeps only accounting alive, not payloads, the channel or dispatch task.
64/// It remains readable after the receiver, layer, or router is dropped. Dropping
65/// the receiver sets depth to zero; closing it preserves depth until drained.
66/// High-water/drop totals are retained, and later Closed admission attempts may
67/// still increase the Closed total. All clones observe the same queue; snapshots
68/// expose counts only, not source identities or per-source totals. See the
69/// [receive-queue contract](crate::layer#receive-queue-admission).
70#[derive(Debug, Clone, Default)]
71pub struct QueueAdmissionCounters(Arc<Mutex<QueueAdmissionState>>);
72
73#[derive(Debug, Default)]
74struct QueueAdmissionState {
75 snapshot: QueueAdmissionSnapshot,
76 // Used only by tracked APDU queues. Insert after successful
77 // admission, remove on the last dequeue: live entries <= depth <= 256.
78 queued_by_source: HashMap<AdmissionSource, usize>,
79}
80
81impl QueueAdmissionState {
82 fn dequeued(&mut self, source: Option<AdmissionSource>) {
83 self.snapshot.current_depth -= 1;
84 if let Some(ref source) = source {
85 let count = self
86 .queued_by_source
87 .get_mut(source)
88 .expect("queued APDU source must be accounted");
89 *count -= 1;
90 if *count == 0 {
91 self.queued_by_source.remove(source);
92 }
93 self.assert_source_depth();
94 }
95 }
96
97 fn assert_source_depth(&self) {
98 debug_assert!(self.queued_by_source.len() <= self.snapshot.current_depth);
99 debug_assert!(self.snapshot.current_depth <= QUEUE_CAPACITY);
100 debug_assert_eq!(
101 self.queued_by_source.values().sum::<usize>(),
102 self.snapshot.current_depth
103 );
104 debug_assert!(self
105 .queued_by_source
106 .values()
107 .all(|count| (1..=APDU_SOURCE_CAPACITY).contains(count)));
108 }
109}
110
111impl QueueAdmissionCounters {
112 /// Read this queue's depth, high-water mark, and admission-drop totals.
113 ///
114 /// Returns a consistent copy of all [`QueueAdmissionSnapshot`] fields under
115 /// one accounting lock, without resetting them. Separate calls for APDU and
116 /// control queues are not jointly atomic. Counts do not expose payloads,
117 /// source identities or per-source totals; drop precedence and saturation
118 /// follow the [receive-queue contract](crate::layer#receive-queue-admission).
119 /// The handle remains readable after the receiver, layer, or router is
120 /// dropped; a snapshot need not be final while dispatch can still attempt
121 /// admission. A default handle has zero counts and is not attached to a queue.
122 pub fn snapshot(&self) -> QueueAdmissionSnapshot {
123 self.0
124 .lock()
125 .expect("queue admission counters poisoned")
126 .snapshot
127 }
128}
129
130/// Opt-in, one-consumer receiver with exact queue-depth accounting.
131///
132/// Created by [`NetworkLayer::start_with_admission`] or
133/// [`NetworkLayer::enable_network_control_receiver_with_admission`], or by
134/// [`RouterOptions::track_admission`](crate::router::RouterOptions::track_admission) or
135/// [`RouterOptions::network_control_receiver_with_admission`](crate::router::RouterOptions::network_control_receiver_with_admission). The queue
136/// holds 256 items; tracked APDUs additionally have a quota of 16 queued items per
137/// source MAC (NetworkLayer) or (ingress port network number, source MAC) (router).
138/// Controls have no per-source quota. **Closed > fairness > Full** determines
139/// drop attribution; an admission drop releases the arriving payload, metadata
140/// and reply sender without reply bytes or a wire rejection, never evicting an
141/// older item. See the [receive-queue contract](crate::layer#receive-queue-admission)
142/// for exact keys, the raw/tracked matrix, queue independence and lifecycle.
143///
144/// [`Self::close`] retains queued items for draining; dropping this receiver
145/// discards them and sets depth to zero without counting admission drops.
146/// High-water/drop totals survive through an owned [`Self::counters`] handle.
147///
148/// This wrapper deliberately does not expose the underlying receiver: all
149/// dequeues must update the snapshot. Use [`NetworkLayer::start`], or a router
150/// started without [`RouterOptions::track_admission`](crate::router::RouterOptions::track_admission),
151/// when a plain `mpsc::Receiver` is required instead, or
152/// [`NetworkLayer::enable_network_control_receiver`] for raw controls.
153#[derive(Debug)]
154pub struct AdmissionReceiver<T> {
155 rx: mpsc::Receiver<T>,
156 counters: QueueAdmissionCounters,
157 // Only tracked APDU constructors install source accounting. The same
158 // extractor is used on admission and every dequeue; no public trait bound.
159 source_key: Option<fn(&T) -> AdmissionSource>,
160}
161
162impl<T> AdmissionReceiver<T> {
163 // Crate-internal construction keeps the generic sender and accounting in
164 // this module while letting the router hand out a raw receiver as well.
165 pub(crate) fn channel(
166 track_depth: bool,
167 ) -> (
168 AdmissionSender<T>,
169 mpsc::Receiver<T>,
170 QueueAdmissionCounters,
171 ) {
172 let (tx, rx) = AdmissionSender::channel(track_depth);
173 let counters = tx.counters.clone();
174 (tx, rx, counters)
175 }
176
177 pub(crate) fn from_parts(rx: mpsc::Receiver<T>, counters: QueueAdmissionCounters) -> Self {
178 Self {
179 rx,
180 counters,
181 source_key: None,
182 }
183 }
184
185 /// Obtain an independently owned snapshot handle for this queue.
186 ///
187 /// The clone shares accounting without retaining the receiver, payloads or
188 /// dispatch task, and remains readable after receiver/layer/router drop.
189 /// See [`QueueAdmissionCounters::snapshot`] and the
190 /// [receive-queue contract](crate::layer#receive-queue-admission).
191 pub fn counters(&self) -> QueueAdmissionCounters {
192 self.counters.clone()
193 }
194
195 /// Receive the next item, or `None` once closed and drained.
196 ///
197 /// Returning an item releases one depth slot and, for tracked APDUs, one
198 /// source-key slot before the consumer can retain or process it. `None`
199 /// changes no counts. The key and limits follow the
200 /// [receive-queue contract](crate::layer#receive-queue-admission).
201 /// Cancellation safe: dropping a pending receive does not remove an item
202 /// or alter its accounting. No counter lock is held while waiting.
203 pub async fn recv(&mut self) -> Option<T> {
204 std::future::poll_fn(|cx| {
205 let mut state = self
206 .counters
207 .0
208 .lock()
209 .expect("queue admission counters poisoned");
210 let result = self.rx.poll_recv(cx);
211 if let Poll::Ready(Some(item)) = &result {
212 state.dequeued(self.source_key.map(|source_key| source_key(item)));
213 }
214 result
215 })
216 .await
217 }
218
219 /// Receive immediately, releasing depth and source-key slots as in [`Self::recv`].
220 ///
221 /// A closed queue can still return queued items. Returns `Empty` when no item
222 /// is currently available but admission remains open, or `Disconnected` once
223 /// closed and drained. Errors do not change counts. See the
224 /// [receive-queue contract](crate::layer#receive-queue-admission).
225 pub fn try_recv(&mut self) -> Result<T, mpsc::error::TryRecvError> {
226 let mut state = self
227 .counters
228 .0
229 .lock()
230 .expect("queue admission counters poisoned");
231 let result = self.rx.try_recv();
232 if let Ok(item) = &result {
233 state.dequeued(self.source_key.map(|source_key| source_key(item)));
234 }
235 result
236 }
237
238 /// Stop admission while retaining already queued items for draining.
239 ///
240 /// Depth and source-key slots remain until [`Self::recv`] or [`Self::try_recv`]
241 /// dequeues an item, or the receiver is dropped. Close itself does not count
242 /// as a drop or reset high-water/drop totals. Later attempts count as Closed,
243 /// taking precedence over fairness and Full.
244 ///
245 /// In NetworkLayer, the next APDU admission attempt ends dispatch, or the next
246 /// control admission attempt disables only that stream. Routers count every
247 /// closed local admission attempt and keep forwarding/handling controls.
248 /// These match closing the corresponding raw receiver. See the
249 /// [receive-queue contract](crate::layer#close-drop-stop-and-observation).
250 pub fn close(&mut self) {
251 let _snapshot = self
252 .counters
253 .0
254 .lock()
255 .expect("queue admission counters poisoned");
256 self.rx.close();
257 }
258}
259
260impl AdmissionReceiver<ReceivedApdu> {
261 pub(crate) fn from_apdu_parts(
262 rx: mpsc::Receiver<ReceivedApdu>,
263 counters: QueueAdmissionCounters,
264 ) -> Self {
265 Self {
266 rx,
267 counters,
268 source_key: Some(apdu_source),
269 }
270 }
271}
272
273impl<T> Drop for AdmissionReceiver<T> {
274 fn drop(&mut self) {
275 let mut state = self
276 .counters
277 .0
278 .lock()
279 .expect("queue admission counters poisoned");
280 self.rx.close();
281 state.snapshot.current_depth = 0;
282 state.queued_by_source = HashMap::new();
283 // The receiver's queued items (including reply senders) drop after
284 // this guard is released. Closing first prevents concurrent admission.
285 }
286}
287
288/// Shared non-waiting admission for raw and tracked 256-item queues.
289///
290/// Both modes count Full/Closed internally; only tracked receivers expose
291/// snapshots and release depth/source slots on dequeue. Tracked APDU sends use
292/// the 16-per-key quota and Closed > fairness > Full precedence. Generic/control
293/// sends and raw APDUs have no quota. See the public
294/// [receive-queue contract](crate::layer#receive-queue-admission).
295pub(crate) struct AdmissionSender<T> {
296 tx: mpsc::Sender<T>,
297 counters: QueueAdmissionCounters,
298 // Legacy receivers dequeue outside this module, so only opt-in receivers
299 // expose depth/high-water accounting. Admission drops are counted in both.
300 track_depth: bool,
301}
302
303impl<T> Clone for AdmissionSender<T> {
304 fn clone(&self) -> Self {
305 Self {
306 tx: self.tx.clone(),
307 counters: self.counters.clone(),
308 track_depth: self.track_depth,
309 }
310 }
311}
312
313impl<T> AdmissionSender<T> {
314 fn channel(track_depth: bool) -> (Self, mpsc::Receiver<T>) {
315 let (tx, rx) = mpsc::channel(QUEUE_CAPACITY);
316 (
317 Self {
318 tx,
319 counters: QueueAdmissionCounters::default(),
320 track_depth,
321 },
322 rx,
323 )
324 }
325
326 pub(crate) fn try_send(&self, item: T) -> Result<(), mpsc::error::TrySendError<T>> {
327 self.try_send_from(item, None)
328 }
329
330 fn try_send_from(
331 &self,
332 item: T,
333 source: Option<AdmissionSource>,
334 ) -> Result<(), mpsc::error::TrySendError<T>> {
335 // Serialize the admission and dequeue accounting, not asynchronous
336 // waiting. Each queue has its own short critical section, and no lock
337 // survives a poll/try operation. Closed keeps its lifecycle semantics;
338 // otherwise a source over quota takes precedence over global Full.
339 let mut state = self
340 .counters
341 .0
342 .lock()
343 .expect("queue admission counters poisoned");
344 if !self.tx.is_closed()
345 && source.as_ref().is_some_and(|source| {
346 state.queued_by_source.get(source).copied().unwrap_or(0) >= APDU_SOURCE_CAPACITY
347 })
348 {
349 state.snapshot.fairness_drops = state.snapshot.fairness_drops.saturating_add(1);
350 // Reuse the caller's drop-arriving path, but not its Full counter.
351 return Err(mpsc::error::TrySendError::Full(item));
352 }
353 let result = self.tx.try_send(item);
354 match &result {
355 Ok(()) if self.track_depth => {
356 state.snapshot.current_depth += 1;
357 state.snapshot.high_water =
358 state.snapshot.high_water.max(state.snapshot.current_depth);
359 if let Some(source) = source {
360 *state.queued_by_source.entry(source).or_default() += 1;
361 state.assert_source_depth();
362 }
363 }
364 Ok(()) => {}
365 Err(mpsc::error::TrySendError::Full(_)) => {
366 state.snapshot.full_drops = state.snapshot.full_drops.saturating_add(1);
367 }
368 Err(mpsc::error::TrySendError::Closed(_)) => {
369 state.snapshot.closed_drops = state.snapshot.closed_drops.saturating_add(1);
370 }
371 }
372 result
373 }
374}
375
376impl AdmissionSender<ReceivedApdu> {
377 pub(crate) fn try_send_apdu(
378 &self,
379 apdu: ReceivedApdu,
380 ) -> Result<(), mpsc::error::TrySendError<()>> {
381 // Raw receivers cannot release source counts: legacy ingress neither
382 // clones keys nor enforces a quota.
383 let source = self.track_depth.then(|| apdu_source(&apdu));
384 // Both dispatchers drop arriving APDUs on failure. Release the owned
385 // item after the accounting lock is released; only Closed affects the
386 // NetworkLayer dispatch lifecycle, not the returned payload.
387 self.try_send_from(apdu, source)
388 .map_err(|error| match error {
389 mpsc::error::TrySendError::Full(_) => mpsc::error::TrySendError::Full(()),
390 mpsc::error::TrySendError::Closed(_) => mpsc::error::TrySendError::Closed(()),
391 })
392 }
393}
394
395impl<T: TransportPort + 'static> NetworkLayer<T> {
396 /// Enable the one-consumer decoded network-control stream with a raw receiver.
397 ///
398 /// Call before either [`Self::start`] or [`Self::start_with_admission`], at
399 /// most once across the two control-enable alternatives. Returns an encoding
400 /// error if dispatch has started or a control receiver is already enabled.
401 /// The separate 256-item control queue has no per-source quota: Closed takes
402 /// precedence over Full, with Full/Closed counted internally but no snapshot
403 /// or depth/high-water tracking. Failed admission drops the control and its
404 /// metadata without waiting for a consumer or generating reply bytes/wire
405 /// rejections; APDU admission remains independent.
406 ///
407 /// Closing retains queued controls; dropping discards them. The first later
408 /// control admission attempt disables only this stream, then controls resume
409 /// discard behavior. Use [`Self::enable_network_control_receiver_with_admission`]
410 /// for counters, and see the [receive-queue contract](crate::layer#receive-queue-admission)
411 /// for the raw/tracked matrix, sequencing and stop/drain semantics.
412 pub fn enable_network_control_receiver(
413 &mut self,
414 ) -> Result<mpsc::Receiver<ReceivedNetworkControl>, Error> {
415 self.enable_network_control(false).map(|(rx, _)| rx)
416 }
417
418 /// Enable the control stream with queue-admission snapshots.
419 ///
420 /// This has the same before-start, one-consumer contract and errors as
421 /// [`Self::enable_network_control_receiver`]; the two methods are alternatives.
422 /// The separate 256-item queue tracks depth/high-water and Full/Closed drops
423 /// through [`AdmissionReceiver::counters`]. It has no per-source quota, so
424 /// fairness drops are always zero and precedence is Closed > Full.
425 /// Drop-arriving, ownership release, no-reply/no-wire-rejection and
426 /// close/drop/stop behavior follow the shared
427 /// [receive-queue contract](crate::layer#receive-queue-admission).
428 ///
429 /// Controls discarded without opting into either stream are not admission
430 /// drops. [`Self::network_control_ingress_sequence`] advances before every
431 /// attempted control admission, including Full and the first Closed drop,
432 /// but not for later controls once that stream is disabled.
433 pub fn enable_network_control_receiver_with_admission(
434 &mut self,
435 ) -> Result<AdmissionReceiver<ReceivedNetworkControl>, Error> {
436 let (rx, counters) = self.enable_network_control(true)?;
437 Ok(AdmissionReceiver::from_parts(rx, counters))
438 }
439
440 fn enable_network_control(
441 &mut self,
442 track_depth: bool,
443 ) -> Result<
444 (
445 mpsc::Receiver<ReceivedNetworkControl>,
446 QueueAdmissionCounters,
447 ),
448 Error,
449 > {
450 if self.dispatch_task.is_some() {
451 return Err(Error::Encoding(
452 "network-control receiver must be enabled before NetworkLayer::start".into(),
453 ));
454 }
455 if self.network_control_tx.is_some() {
456 return Err(Error::Encoding(
457 "network-control receiver is already enabled".into(),
458 ));
459 }
460 let (tx, rx) = AdmissionSender::channel(track_depth);
461 let counters = tx.counters.clone();
462 self.network_control_tx = Some(tx);
463 Ok((rx, counters))
464 }
465
466 /// Start the network layer. Returns a receiver for incoming APDUs.
467 ///
468 /// This starts the underlying transport and spawns a dispatch task that
469 /// decodes incoming NPDUs and extracts APDUs. The queue holds 256 items;
470 /// failed admission drops the arriving APDU with its payload, metadata and reply
471 /// sender without waiting for the consumer, sending reply bytes or generating
472 /// a wire rejection. Control capacity is independent. This raw receiver has
473 /// no per-source quota, admission snapshot or depth/high-water tracking;
474 /// Full/Closed drops are counted internally, with Closed > Full precedence.
475 ///
476 /// Close retains queued APDUs; drop discards them. Either ends dispatch on
477 /// the next APDU admission attempt, also ending control delivery.
478 /// [`Self::stop`] after start leaves queued items drainable. Use
479 /// [`Self::start_with_admission`] for snapshots and per-source fairness; see
480 /// the [receive-queue contract](crate::layer#receive-queue-admission) for the
481 /// raw/tracked matrix and ownership/lifecycle details.
482 pub async fn start(&mut self) -> Result<mpsc::Receiver<ReceivedApdu>, Error> {
483 self.start_dispatch(false).await.map(|(rx, _)| rx)
484 }
485
486 /// Start with an APDU receiver that also exposes admission snapshots.
487 ///
488 /// This is an alternative to [`Self::start`], with the same transport and
489 /// dispatch lifecycle. [`Self::stop`] closes admission but leaves
490 /// queued items available to drain. Dropping the receiver discards those
491 /// items and releases their reply channels without sending reply bytes.
492 ///
493 /// Unlike the raw receiver, this tracks depth/high-water and drop totals via
494 /// [`AdmissionReceiver::counters`] and caps each complete source MAC byte
495 /// value at 16 queued, unconsumed APDUs in the 256-item queue. The single
496 /// transport uses no port key and ignores routed NPDU SNET/SADR for quotas.
497 /// Dequeuing releases one source slot even if the consumer retains the APDU.
498 /// **Closed > fairness > Full** selects one drop reason, releasing the
499 /// arriving payload, metadata and reply sender without sending reply bytes
500 /// or a wire rejection. Control queues have separate capacity and no quota.
501 /// For the router's port-scoped key, raw/tracked matrix, and full
502 /// close/drop/stop and counter-lifetime rules, see the
503 /// [receive-queue contract](crate::layer#receive-queue-admission).
504 ///
505 /// ```no_run
506 /// # async fn example() -> Result<(), bacnet_types::error::Error> {
507 /// use bacnet_network::layer::NetworkLayer;
508 /// use bacnet_transport::loopback::LoopbackTransport;
509 /// let (transport, _peer) = LoopbackTransport::pair(vec![1], vec![2]);
510 /// let mut network = NetworkLayer::new(transport);
511 /// let mut controls = network.enable_network_control_receiver_with_admission()?;
512 /// let mut apdus = network.start_with_admission().await?;
513 /// let apdu_counters = apdus.counters();
514 /// let control_counters = controls.counters();
515 /// tokio::select! {
516 /// _ = apdus.recv() => {},
517 /// _ = controls.recv() => {},
518 /// }
519 /// let _snapshots = (apdu_counters.snapshot(), control_counters.snapshot());
520 /// network.stop().await?;
521 /// # Ok(()) }
522 /// ```
523 pub async fn start_with_admission(&mut self) -> Result<AdmissionReceiver<ReceivedApdu>, Error> {
524 let (rx, counters) = self.start_dispatch(true).await?;
525 Ok(AdmissionReceiver::from_apdu_parts(rx, counters))
526 }
527
528 async fn start_dispatch(
529 &mut self,
530 track_depth: bool,
531 ) -> Result<(mpsc::Receiver<ReceivedApdu>, QueueAdmissionCounters), Error> {
532 let mut npdu_rx = self.transport.start().await?;
533 let mut network_control_tx = self.network_control_tx.take();
534 let network_control_ingress_sequence = Arc::clone(&self.network_control_ingress_sequence);
535 let address_length_drops = Arc::clone(&self.address_length_drops);
536 let global_broadcast_dadr_drops = Arc::clone(&self.global_broadcast_dadr_drops);
537 let broadcast_pdu_type_drops = Arc::clone(&self.broadcast_pdu_type_drops);
538 let (apdu_tx, apdu_rx) = AdmissionSender::channel(track_depth);
539 let counters = apdu_tx.counters.clone();
540
541 let dispatch_task = tokio::spawn(async move {
542 while let Some(received) = npdu_rx.recv().await {
543 if !link_source_fits(&received.source_mac, &address_length_drops) {
544 continue;
545 }
546 match decode_npdu(received.npdu.clone()) {
547 Ok(npdu) => {
548 if !destination_is_coherent(
549 npdu.destination.as_ref(),
550 &global_broadcast_dadr_drops,
551 ) || !broadcast_carries_unconfirmed(&npdu, &broadcast_pdu_type_drops)
552 {
553 continue;
554 }
555 if npdu.is_network_message {
556 if let Some(tx) = network_control_tx.as_ref() {
557 let ingress_sequence = next_ingress_sequence(
558 network_control_ingress_sequence.as_ref(),
559 );
560 let control = ReceivedNetworkControl {
561 npdu,
562 source_mac: received.source_mac,
563 link_layer_group: received.link_layer_group,
564 data_attributes: received.data_attributes,
565 provenance: received.provenance,
566 ingress_sequence,
567 };
568 match tx.try_send(control) {
569 Ok(()) | Err(mpsc::error::TrySendError::Full(_)) => {}
570 Err(mpsc::error::TrySendError::Closed(_)) => {
571 network_control_tx = None;
572 debug!("Network-control receiver closed; resuming discard behavior");
573 }
574 }
575 } else {
576 debug!(
577 message_type = npdu.message_type,
578 "Ignoring network layer message (non-router mode)"
579 );
580 }
581 continue;
582 }
583
584 // Non-routing node: discard messages with a specific DNET.
585 if let Some(ref dest) = npdu.destination {
586 if dest.network != 0xFFFF {
587 debug!(
588 dnet = dest.network,
589 "Discarding routed message (non-router)"
590 );
591 continue;
592 }
593 }
594
595 let source_network = npdu.source.clone();
596 let is_group =
597 is_group_delivery(received.link_layer_group, npdu.destination.as_ref());
598 let global_broadcast = is_global_broadcast(npdu.destination.as_ref());
599 let apdu = ReceivedApdu {
600 direct_response: received.direct_response.clone(),
601 apdu: npdu.payload,
602 source_mac: received.source_mac,
603 ingress_network: None,
604 source_network,
605 link_layer_group: received.link_layer_group,
606 is_group,
607 global_broadcast,
608 data_attributes: received.data_attributes,
609 provenance: received.provenance,
610 reply_tx: received.reply_tx,
611 };
612
613 match apdu_tx.try_send_apdu(apdu) {
614 Ok(()) | Err(mpsc::error::TrySendError::Full(_)) => {}
615 Err(mpsc::error::TrySendError::Closed(_)) => break,
616 }
617 }
618 Err(e @ NpduDecodeError::AddressTooLong { .. }) => {
619 // A non-router has no reject to send (Clause 6.6.3.5).
620 count_drop(&address_length_drops);
621 warn!(error = %e, "Dropping NPDU with an over-long address");
622 }
623 Err(e) => {
624 warn!(error = %e, "Failed to decode NPDU");
625 }
626 }
627 }
628 });
629
630 self.dispatch_task = Some(dispatch_task);
631 Ok((apdu_rx, counters))
632 }
633}
634
635/// Whether a frame's link-layer source MAC fits [`NpduAddress::MAX_MAC_LEN`]
636/// (#1198). [`NetworkLayer`] and the router ask before decoding anything else
637/// in the frame. A longer MAC is counted in `drops` and its frame goes no
638/// further, so neither an APDU consumer nor the routing table ever learns an
639/// address that a `BACnetAddress` cannot hold. No built-in transport makes
640/// one: B/IP, BACnet/SC and Ethernet MACs are 6 octets, MS/TP 1 and B/IPv6
641/// 18; only a custom [`TransportPort`] can.
642pub(crate) fn link_source_fits(source_mac: &[u8], drops: &AtomicU64) -> bool {
643 if source_mac.len() <= NpduAddress::MAX_MAC_LEN {
644 return true;
645 }
646 count_drop(drops);
647 warn!(
648 octets = source_mac.len(),
649 limit = NpduAddress::MAX_MAC_LEN,
650 "Dropping a frame whose link-layer source MAC is over the limit"
651 );
652 false
653}
654
655/// Whether a decoded NPDU's destination can be acted on: anything but DNET
656/// 0xFFFF with a DADR (#1379). That DNET already names every device on every
657/// network (Clauses 6.2.2 and 6.3.2), so a DADR beside it names one device as
658/// well and the NPDU contradicts itself. [`NetworkLayer`] and the router both
659/// ask right after decoding, before they look at what the NPDU carries, so
660/// this one check covers APDUs and network messages on either. A
661/// contradictory NPDU is counted in `drops` and goes no further: it is not
662/// delivered, forwarded, answered, rejected or learned from.
663pub(crate) fn destination_is_coherent(
664 destination: Option<&NpduAddress>,
665 drops: &AtomicU64,
666) -> bool {
667 let Some(destination) = destination else {
668 return true;
669 };
670 if destination.network != 0xFFFF || destination.mac_address.is_empty() {
671 return true;
672 }
673 count_drop(drops);
674 warn!(
675 dlen = destination.mac_address.len(),
676 "Dropping a global broadcast NPDU that also names a DADR"
677 );
678 false
679}
680
681/// Whether a decoded NPDU may go on: anything but an APDU sent to a broadcast
682/// network address, global (DNET 0xFFFF) or remote (a DNET with no DADR),
683/// whose PDU type isn't Unconfirmed-Request (#1491). Only that type may use
684/// a broadcast network address (Clause 6.3); any other belongs to one peer's
685/// transaction, so every device reached would get a request or an answer
686/// that names no one. The type is the high nibble of the APDU's first octet
687/// (Clause 20.1), so nothing is decoded, and an empty APDU names no type.
688/// Network messages pass. [`NetworkLayer`] and the router both ask right
689/// after [`destination_is_coherent`]: a refused NPDU is counted in `drops`
690/// and is not delivered, forwarded, answered or rejected.
691pub(crate) fn broadcast_carries_unconfirmed(npdu: &Npdu, drops: &AtomicU64) -> bool {
692 let Some(destination) = npdu.destination.as_ref() else {
693 return true;
694 };
695 let broadcast = destination.network == 0xFFFF || destination.mac_address.is_empty();
696 let unconfirmed = npdu
697 .payload
698 .first()
699 .is_some_and(|&first| PduType::from_raw(first >> 4) == PduType::UNCONFIRMED_REQUEST);
700 if npdu.is_network_message || !broadcast || unconfirmed {
701 return true;
702 }
703 count_drop(drops);
704 warn!(
705 dnet = destination.network,
706 "Dropping a broadcast NPDU whose APDU isn't an Unconfirmed-Request"
707 );
708 false
709}
710
711#[cfg(test)]
712#[path = "layer_fairness_tests.rs"]
713mod fairness_tests;