Skip to main content

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;