bacnet_network/layer.rs
1//! NetworkLayer for local BACnet packet assembly and dispatch.
2//!
3//! The network layer wraps a transport and provides APDU-level send/receive
4//! by handling NPDU encoding/decoding. This is a non-router implementation:
5//! it does not forward messages between networks, but it can address remote
6//! devices through local routers via NPDU destination fields (DNET/DADR).
7//!
8//! # Receive-queue admission
9//!
10//! [`NetworkLayer`] and [`BACnetRouter`](crate::router::BACnetRouter) use the
11//! same drop-arriving policy for application delivery. Each receive queue holds
12//! **256 items**. Admission never waits for a consumer and never evicts an older
13//! queued item. NetworkLayer APDU and opted-in control queues have independent
14//! capacities, so saturation of either does not stall admission to the other.
15//! All router ports share one local APDU queue; its saturation does not stop
16//! forwarding or inline network-message handling.
17//!
18//! ## Raw and tracked receivers
19//!
20//! | Receiver kind | Entry points | Capacity | Per-source quota | Accounting |
21//! | --- | --- | --- | --- | --- |
22//! | Raw APDU | [`NetworkLayer::start`], [`BACnetRouter::start`](crate::router::BACnetRouter::start) with [`RouterOptions::new`](crate::router::RouterOptions::new) | 256 | None | Full/Closed counted internally; no admission snapshot or depth/high-water tracking |
23//! | Tracked APDU | [`NetworkLayer::start_with_admission`], [`RouterOptions::track_admission`](crate::router::RouterOptions::track_admission) | 256 | 16 queued APDUs per key, defined below | Exact depth/high-water and Full/fairness/Closed totals via [`AdmissionReceiver::counters`] |
24//! | Raw control | [`NetworkLayer::enable_network_control_receiver`], [`RouterOptions::network_control_receiver`](crate::router::RouterOptions::network_control_receiver) | 256, separate from APDUs | None | Full/Closed counted internally; no admission snapshot or depth/high-water tracking |
25//! | Tracked control | [`NetworkLayer::enable_network_control_receiver_with_admission`], [`RouterOptions::network_control_receiver_with_admission`](crate::router::RouterOptions::network_control_receiver_with_admission) | 256, separate from APDUs | None | Exact depth/high-water and Full/Closed totals; fairness always zero |
26//!
27//! Raw receivers remain `tokio::sync::mpsc::Receiver`; tracked receivers are the
28//! additive [`AdmissionReceiver`] alternatives. APDU and control opt-ins are
29//! independent. Routers process network messages inline; their control receiver
30//! gets only the Reject-Message-To-Network messages addressed to the router
31//! itself, and every closed admission attempt counts as Closed without
32//! disabling the stream. [`priority_channel`](crate::priority_channel) is not
33//! used by these ingress paths.
34//!
35//! ## Source keys and drop precedence
36//!
37//! Tracked APDU queues cap each key at **16 queued, unconsumed APDUs**:
38//! - NetworkLayer: the complete transport-native [`ReceivedApdu::source_mac`]
39//! byte value. Its single transport needs no separate port key.
40//! - Router local delivery: ([ingress port network number](ReceivedApdu::ingress_network),
41//! [`source_mac`](ReceivedApdu::source_mac)). Identical MAC bytes on different
42//! ports have separate quotas. Neither key uses routed NPDU SNET/SADR
43//! ([`source_network`](ReceivedApdu::source_network)) or authenticates a peer.
44//!
45//! Each successful [`recv`](AdmissionReceiver::recv) or
46//! [`try_recv`](AdmissionReceiver::try_recv) releases a queue slot and, for
47//! tracked APDUs, that key's quota slot before returning the item. Retaining or
48//! processing the returned item does not occupy either slot. Sixteen keys at
49//! their quota fill the 256-item queue; a key below quota can still encounter
50//! global Full. This is a queued-item cap, not a byte/rate limit or a delivery
51//! guarantee. Raw receivers and control queues have no per-source quota.
52//!
53//! Drop attribution is **Closed > fairness > Full**: closed admission wins;
54//! otherwise a tracked APDU whose key already holds 16 items is a fairness drop,
55//! even if the queue also holds 256 items; otherwise global saturation is Full.
56//! Only the winning reason is counted. Every admission drop releases the
57//! arriving item's owned payload, metadata and any reply sender without sending
58//! reply bytes or generating a wire rejection. This does not suppress ordinary
59//! router forwarding/reject behavior or change transport no-reply handling.
60//!
61//! ## Close, drop, stop, and observation
62//!
63//! Closing either receiver kind prevents further admission but retains queued
64//! items for draining. Dropping a receiver discards them and releases their
65//! payloads, metadata and reply senders without sending reply bytes. For tracked
66//! receivers, close preserves depth until dequeued; drop sets depth to zero.
67//! Neither operation resets high-water/drop totals, and discarding queued items
68//! is not an admission drop.
69//!
70//! NetworkLayer ends its dispatcher on the first APDU admission attempt after
71//! that receiver closes/drops, also ending control delivery. A closed/dropped
72//! control receiver instead disables only that stream on the first subsequent
73//! control admission attempt. Later discarded controls (like controls received
74//! without either opt-in) are not admission attempts. Router forwarding and
75//! inline control handling continue after local delivery closes/drops; each
76//! subsequent local admission attempt is counted as Closed.
77//!
78//! [`NetworkLayer::stop`] after start and [`BACnetRouter::stop`](crate::router::BACnetRouter::stop)
79//! end dispatch without waiting for application queues to drain; queued items
80//! remain drainable. Stopping is not an admission drop. An independently owned
81//! [`QueueAdmissionCounters`] handle remains readable after the receiver, layer,
82//! or router is dropped; it keeps accounting alive, not the channel or tasks.
83//! [`snapshot`](QueueAdmissionCounters::snapshot) is consistent for one queue,
84//! not atomic across queues, and exposes counts only, not payloads, per-source
85//! identities or per-source totals. Its three drop totals saturate at `u64::MAX`.
86
87use crate::network_number::LocalNetworkNumber;
88use bacnet_encoding::npdu::{encode_npdu, Npdu, NpduAddress};
89use bacnet_transport::port::{DataAttribute, TransportPort, TransportProvenance};
90use bacnet_types::enums::{NetworkPriority, PduType};
91use bacnet_types::error::Error;
92use bacnet_types::MacAddr;
93use bytes::{Bytes, BytesMut};
94use std::sync::atomic::{AtomicU64, Ordering};
95use std::sync::Arc;
96use tokio::sync::oneshot;
97use tokio::task::JoinHandle;
98
99#[path = "layer_issuance.rs"]
100mod issuance;
101pub use issuance::IssuedApdu;
102
103#[path = "layer_admission.rs"]
104mod admission;
105pub(crate) use admission::{
106 broadcast_carries_unconfirmed, destination_is_coherent, link_source_fits, AdmissionSender,
107};
108pub use admission::{AdmissionReceiver, QueueAdmissionCounters, QueueAdmissionSnapshot};
109
110/// A received APDU with source addressing information.
111pub struct ReceivedApdu {
112 /// Raw APDU bytes.
113 pub apdu: Bytes,
114 /// Source MAC address in transport-native format (claimed identity; see
115 /// [`TransportProvenance`]. On SC this is the source VMAC, not a
116 /// certificate principal).
117 pub source_mac: MacAddr,
118 /// BACnet network number of the router ingress port at admission time.
119 ///
120 /// Router deliveries set this to `Some`, independently of the routed source
121 /// address. Non-router [`NetworkLayer`] deliveries use `None` because the
122 /// layer owns one transport without an assigned ingress network number.
123 /// Both raw and tracked deliveries carry this metadata. For
124 /// [`RouterOptions::track_admission`](crate::router::RouterOptions::track_admission),
125 /// it scopes [`source_mac`](Self::source_mac) to the ingress port for the
126 /// 16-queued-APDU quota; [`source_network`](Self::source_network) is not part
127 /// of that key. See the [receive-queue contract](self#receive-queue-admission).
128 pub ingress_network: Option<u16>,
129 /// Source network address if the APDU was routed (NPDU had source field).
130 /// Claimed SNET/SADR, never a credential (RB-07 compat mode: decisions
131 /// unchanged; RB-09 consumes provenance later).
132 pub source_network: Option<NpduAddress>,
133 /// Whether the NPDU arrived through a data-link multicast or broadcast.
134 ///
135 /// This preserves raw data-link provenance independently of [`Self::is_group`].
136 pub link_layer_group: bool,
137 /// Whether the APDU's effective BACnet destination was multicast or broadcast.
138 ///
139 /// A specific DNET/DADR remains a unicast even when a router used a
140 /// group data-link destination to reach that remote device.
141 pub is_group: bool,
142 /// Whether the NPDU named the global broadcast network (DNET 65535).
143 ///
144 /// A global broadcast is also a group delivery, but [`Self::is_group`]
145 /// alone cannot tell it from a local or one-network broadcast once a
146 /// router has passed it on. A Notification Forwarder ignores event
147 /// notifications that arrive this way (Clause 12.51).
148 pub global_broadcast: bool,
149 /// Data-link attributes associated with the NPDU, if the transport supplied any.
150 pub data_attributes: Vec<DataAttribute>,
151 /// Honest transport + origin provenance, immutable by value (RB-07).
152 /// Threaded from [`bacnet_transport::port::ReceivedNpdu`]; cloning
153 /// preserves the meaning and never duplicates reply authority. Compat
154 /// mode: forwarding/learning/admission decisions ignore it.
155 pub provenance: TransportProvenance,
156 /// Sealed route back to this envelope's original verified direct socket.
157 pub direct_response: Option<bacnet_transport::port::DirectResponse>,
158 /// Optional reply channel for MS/TP DataExpectingReply flows.
159 /// The application layer can send NPDU-wrapped reply bytes through this channel.
160 /// An APDU dropped at queue admission releases this sender without sending
161 /// bytes, just as when the application receiver is closed. The transport
162 /// observes channel closure and retains its existing no-reply handling.
163 pub reply_tx: Option<oneshot::Sender<Bytes>>,
164}
165
166/// A decoded network-layer control message received from a transport peer.
167///
168/// Non-router users opt in to this stream before [`NetworkLayer::start`].
169/// Without that opt-in, network messages retain their historical discard/log
170/// behavior. A router opts in with
171/// [`RouterOptions::network_control_receiver`](crate::router::RouterOptions::network_control_receiver),
172/// and its stream carries only the rejects addressed to the router itself.
173#[derive(Clone)]
174pub struct ReceivedNetworkControl {
175 /// Decoded NPDU, including network-message type and typed-address fields.
176 pub npdu: Npdu,
177 /// Immediate transport peer that sent the message (claimed identity; see
178 /// [`TransportProvenance`]).
179 pub source_mac: MacAddr,
180 /// Whether the data-link delivery was multicast or broadcast.
181 pub link_layer_group: bool,
182 /// Data-link attributes supplied by the transport.
183 pub data_attributes: Vec<DataAttribute>,
184 /// Honest transport + origin provenance, immutable by value (RB-07).
185 /// Compat mode: decisions unchanged.
186 pub provenance: TransportProvenance,
187 /// Monotonic decoded-ingress sequence assigned before channel delivery.
188 ///
189 /// A consumer can compare this with [`NetworkLayer::network_control_ingress_sequence`]
190 /// when activating state so controls already queued at that point cannot
191 /// be mistaken for feedback about the new state.
192 pub ingress_sequence: u64,
193}
194
195impl std::fmt::Debug for ReceivedNetworkControl {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 // Redacted like ReceivedApdu: lengths only, no key material.
198 f.debug_struct("ReceivedNetworkControl")
199 .field("npdu", &self.npdu)
200 .field("source_mac_len", &self.source_mac.len())
201 .field("link_layer_group", &self.link_layer_group)
202 .field("data_attributes", &self.data_attributes)
203 .field("provenance", &self.provenance)
204 .field("ingress_sequence", &self.ingress_sequence)
205 .finish_non_exhaustive()
206 }
207}
208
209impl Clone for ReceivedApdu {
210 fn clone(&self) -> Self {
211 Self {
212 apdu: self.apdu.clone(),
213 source_mac: self.source_mac.clone(),
214 ingress_network: self.ingress_network,
215 source_network: self.source_network.clone(),
216 link_layer_group: self.link_layer_group,
217 is_group: self.is_group,
218 global_broadcast: self.global_broadcast,
219 data_attributes: self.data_attributes.clone(),
220 provenance: self.provenance,
221 direct_response: self.direct_response.clone(),
222 reply_tx: None,
223 }
224 }
225}
226
227impl std::fmt::Debug for ReceivedApdu {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 // Redacted: MAC bytes, routed source bytes, and payload lengths only;
230 // no key material. Follows the TimeSyncSourceRestriction
231 // finish_non_exhaustive pattern.
232 let source_network_len = self
233 .source_network
234 .as_ref()
235 .map(|source| (source.network, source.mac_address.len()));
236 f.debug_struct("ReceivedApdu")
237 .field("apdu_len", &self.apdu.len())
238 .field("source_mac_len", &self.source_mac.len())
239 .field("ingress_network", &self.ingress_network)
240 .field("source_network", &source_network_len)
241 .field("link_layer_group", &self.link_layer_group)
242 .field("is_group", &self.is_group)
243 .field("global_broadcast", &self.global_broadcast)
244 .field("data_attributes", &self.data_attributes)
245 .field("provenance", &self.provenance)
246 .field("reply_tx", &self.reply_tx.as_ref().map(|_| "Some(...)"))
247 .finish_non_exhaustive()
248 }
249}
250
251/// Whether an NPDU destination names the global broadcast network. Ingress
252/// has already dropped one that also carries a DADR (#1379), so on a
253/// delivered NPDU this is a plain global broadcast.
254pub(crate) fn is_global_broadcast(destination: Option<&NpduAddress>) -> bool {
255 destination.is_some_and(|destination| destination.network == 0xFFFF)
256}
257
258/// Refuse a caller-supplied DNET that a send naming one network cannot take.
259/// Network numbers start at 1 (Clause 6.2.2.1); the local network is reached
260/// with no DNET at all, and `local_form` tells the caller how to do that.
261/// DNET 0xFFFF selects every device on every network (Clauses 6.1 and
262/// 6.3.2), and its sender puts it on its own network with the broadcast MAC
263/// so that each router there can pass it on. Only
264/// [`NetworkLayer::broadcast_global_apdu`] sends it that way, so every send
265/// that checks here refuses DNET 0xFFFF, with or without a DADR (#1340,
266/// #1380), and points to that method.
267pub(crate) fn check_destination(network: u16, local_form: &str) -> Result<(), Error> {
268 match network {
269 0 => Err(Error::Encoding(format!(
270 "dest_network 0 is not a network number; {local_form}"
271 ))),
272 0xFFFF => Err(Error::Encoding(
273 "dest_network 0xFFFF is the global broadcast; \
274 use broadcast_global_apdu for a global broadcast"
275 .into(),
276 )),
277 _ => Ok(()),
278 }
279}
280
281/// Refuse an APDU that a broadcast may not carry, before anything is encoded
282/// or sent. Clause 6.3 lets only an Unconfirmed-Request PDU go to a broadcast
283/// network address, whether local, remote or global: any other PDU names one
284/// peer's transaction, and every device reached would have to answer it or
285/// act on it (#1479). `what` names the broadcast for the error, which also
286/// names the PDU type the APDU's first octet gives (Clause 20.1).
287pub(crate) fn check_broadcast_apdu(apdu: &[u8], what: &str) -> Result<(), Error> {
288 let pdu_type = match apdu.first() {
289 Some(&first) if PduType::from_raw(first >> 4) == PduType::UNCONFIRMED_REQUEST => {
290 return Ok(())
291 }
292 Some(&first) => format!("PDU type {}", PduType::from_raw(first >> 4)),
293 None => "an empty APDU".to_string(),
294 };
295 Err(Error::Encoding(format!(
296 "{what} carries only an UNCONFIRMED_REQUEST APDU (Clause 6.3), not {pdu_type}; \
297 send anything else to one device"
298 )))
299}
300
301pub(crate) fn is_group_delivery(link_layer_group: bool, destination: Option<&NpduAddress>) -> bool {
302 match destination {
303 None => link_layer_group,
304 Some(destination) => destination.network == 0xFFFF || destination.mac_address.is_empty(),
305 }
306}
307
308/// A remote device reached through a local router.
309#[derive(Debug, Clone, Copy)]
310pub struct RoutedTarget<'a> {
311 /// Destination network number written to the NPDU DNET.
312 pub network: u16,
313 /// Destination MAC on that network (NPDU DADR).
314 pub mac: &'a [u8],
315 /// Next-hop router MAC on the local network (the link-layer destination).
316 pub router_mac: &'a [u8],
317}
318
319/// Non-router BACnet network layer.
320///
321/// Wraps a [`TransportPort`] and provides APDU-level send/receive by handling
322/// NPDU framing. This layer does not act as a router (it does not forward
323/// messages between networks), but it can send to remote devices through
324/// local routers using NPDU destination addressing.
325/// See the [receive-queue contract](self#receive-queue-admission) for raw/tracked
326/// receiver choices, admission limits, drop attribution and lifecycle.
327/// An inbound frame from a link-layer source MAC longer than
328/// [`NpduAddress::MAX_MAC_LEN`], or whose DLEN or SLEN is past it, is dropped
329/// before admission and counted by [`Self::address_length_drops`]; one whose
330/// DNET 0xFFFF carries a DADR is dropped and counted by
331/// [`Self::global_broadcast_dadr_drops`], and a broadcast whose APDU isn't an
332/// Unconfirmed-Request by [`Self::broadcast_pdu_type_drops`].
333pub struct NetworkLayer<T: TransportPort> {
334 transport: T,
335 response_scope: bacnet_transport::port::DirectResponseScope,
336 dispatch_task: Option<JoinHandle<()>>,
337 network_control_tx: Option<AdmissionSender<ReceivedNetworkControl>>,
338 network_control_ingress_sequence: Arc<AtomicU64>,
339 address_length_drops: Arc<AtomicU64>,
340 global_broadcast_dadr_drops: Arc<AtomicU64>,
341 broadcast_pdu_type_drops: Arc<AtomicU64>,
342 local_network_number: LocalNetworkNumber,
343}
344
345impl<T: TransportPort + 'static> NetworkLayer<T> {
346 /// Create a new network layer wrapping the given transport.
347 pub fn new(transport: T) -> Self {
348 Self {
349 transport,
350 response_scope: Default::default(),
351 dispatch_task: None,
352 network_control_tx: None,
353 network_control_ingress_sequence: Arc::new(AtomicU64::new(0)),
354 address_length_drops: Arc::new(AtomicU64::new(0)),
355 global_broadcast_dadr_drops: Arc::new(AtomicU64::new(0)),
356 broadcast_pdu_type_drops: Arc::new(AtomicU64::new(0)),
357 local_network_number: LocalNetworkNumber::default(),
358 }
359 }
360
361 /// Send an APDU to a specific local destination by MAC address.
362 ///
363 /// With no DNET, the link's broadcast MAC or any other group address
364 /// ([`TransportPort::is_group_destination`]) makes this a local
365 /// broadcast, which may carry only an Unconfirmed-Request APDU (Clause
366 /// 6.3). This layer doesn't ask the transport on every unicast, so that
367 /// is the caller's to keep: `BACnetClient`, the endpoint's egress and its
368 /// requester, which take caller-chosen MACs, refuse anything else to it
369 /// (#1479). The server refuses one for the confirmed requests it starts
370 /// itself, to event recipients, Channel and Command targets, audit
371 /// recipients and bound devices (#1493); its replies and COV
372 /// notifications go to the source a request came from.
373 pub async fn send_apdu(
374 &self,
375 apdu: &[u8],
376 destination_mac: &[u8],
377 expecting_reply: bool,
378 priority: NetworkPriority,
379 ) -> Result<(), Error> {
380 self.send_apdu_with_data_attributes(apdu, destination_mac, expecting_reply, priority, &[])
381 .await
382 }
383
384 /// Send an APDU with data attributes to a specific local destination.
385 pub async fn send_apdu_with_data_attributes(
386 &self,
387 apdu: &[u8],
388 destination_mac: &[u8],
389 expecting_reply: bool,
390 priority: NetworkPriority,
391 data_attributes: &[DataAttribute],
392 ) -> Result<(), Error> {
393 let npdu = Npdu {
394 is_network_message: false,
395 expecting_reply,
396 priority,
397 destination: None,
398 source: None,
399 payload: Bytes::copy_from_slice(apdu),
400 ..Npdu::default()
401 };
402
403 let mut buf = BytesMut::with_capacity(2 + apdu.len());
404 encode_npdu(&mut buf, &npdu)?;
405
406 self.transport
407 .send_unicast_with_data_attributes(&buf, destination_mac, data_attributes)
408 .await
409 }
410
411 /// Broadcast an APDU on the local network. Anything but an
412 /// Unconfirmed-Request APDU is refused before it is sent (Clause 6.3,
413 /// #1479).
414 pub async fn broadcast_apdu(
415 &self,
416 apdu: &[u8],
417 expecting_reply: bool,
418 priority: NetworkPriority,
419 ) -> Result<(), Error> {
420 self.broadcast_apdu_with_data_attributes(apdu, expecting_reply, priority, &[])
421 .await
422 }
423
424 /// Broadcast an APDU with data attributes on the local network.
425 pub async fn broadcast_apdu_with_data_attributes(
426 &self,
427 apdu: &[u8],
428 expecting_reply: bool,
429 priority: NetworkPriority,
430 data_attributes: &[DataAttribute],
431 ) -> Result<(), Error> {
432 check_broadcast_apdu(apdu, "a local broadcast")?;
433 let npdu = Npdu {
434 is_network_message: false,
435 expecting_reply,
436 priority,
437 destination: None,
438 source: None,
439 payload: Bytes::copy_from_slice(apdu),
440 ..Npdu::default()
441 };
442
443 let mut buf = BytesMut::with_capacity(2 + apdu.len());
444 encode_npdu(&mut buf, &npdu)?;
445
446 self.transport
447 .send_broadcast_with_data_attributes(&buf, data_attributes)
448 .await
449 }
450
451 /// Broadcast an APDU globally (DNET=0xFFFF, hop_count=255).
452 ///
453 /// Unlike `broadcast_apdu()` which only reaches the local subnet, this
454 /// sets DNET=0xFFFF so routers will forward to all reachable networks.
455 /// Anything but an Unconfirmed-Request APDU is refused before it is sent
456 /// (Clause 6.3, #1479).
457 pub async fn broadcast_global_apdu(
458 &self,
459 apdu: &[u8],
460 expecting_reply: bool,
461 priority: NetworkPriority,
462 ) -> Result<(), Error> {
463 self.broadcast_global_apdu_with_data_attributes(apdu, expecting_reply, priority, &[])
464 .await
465 }
466
467 /// Broadcast an APDU globally with data attributes (DNET=0xFFFF, hop_count=255).
468 pub async fn broadcast_global_apdu_with_data_attributes(
469 &self,
470 apdu: &[u8],
471 expecting_reply: bool,
472 priority: NetworkPriority,
473 data_attributes: &[DataAttribute],
474 ) -> Result<(), Error> {
475 check_broadcast_apdu(apdu, "a global broadcast")?;
476 let npdu = Npdu {
477 is_network_message: false,
478 expecting_reply,
479 priority,
480 destination: Some(NpduAddress {
481 network: 0xFFFF,
482 mac_address: MacAddr::new(),
483 }),
484 source: None,
485 hop_count: 255,
486 payload: Bytes::copy_from_slice(apdu),
487 ..Npdu::default()
488 };
489
490 let mut buf = BytesMut::with_capacity(8 + apdu.len());
491 encode_npdu(&mut buf, &npdu)?;
492 self.transport
493 .send_broadcast_with_data_attributes(&buf, data_attributes)
494 .await
495 }
496
497 /// Broadcast an APDU to a specific remote network via routers.
498 ///
499 /// Like `broadcast_global_apdu()` but targets a single network number
500 /// instead of all networks (DNET=0xFFFF). Refuses `dest_network` 0 and
501 /// 0xFFFF, and anything but an Unconfirmed-Request APDU (Clause 6.3,
502 /// #1479), without sending anything. This is the one send that puts a
503 /// remote network's broadcast on the link's broadcast MAC;
504 /// [`Self::send_apdu_routed`] with an empty `dest_mac` sends it to a
505 /// known router instead.
506 pub async fn broadcast_to_network(
507 &self,
508 apdu: &[u8],
509 dest_network: u16,
510 expecting_reply: bool,
511 priority: NetworkPriority,
512 ) -> Result<(), Error> {
513 self.broadcast_to_network_with_data_attributes(
514 apdu,
515 dest_network,
516 expecting_reply,
517 priority,
518 &[],
519 )
520 .await
521 }
522
523 /// Broadcast an APDU with data attributes to a specific remote network via routers.
524 pub async fn broadcast_to_network_with_data_attributes(
525 &self,
526 apdu: &[u8],
527 dest_network: u16,
528 expecting_reply: bool,
529 priority: NetworkPriority,
530 data_attributes: &[DataAttribute],
531 ) -> Result<(), Error> {
532 check_destination(dest_network, "use broadcast_apdu for the local network")?;
533 check_broadcast_apdu(apdu, "a broadcast to one network")?;
534 let npdu = Npdu {
535 is_network_message: false,
536 expecting_reply,
537 priority,
538 destination: Some(NpduAddress {
539 network: dest_network,
540 mac_address: MacAddr::new(),
541 }),
542 source: None,
543 hop_count: 255,
544 payload: Bytes::copy_from_slice(apdu),
545 ..Npdu::default()
546 };
547
548 let mut buf = BytesMut::with_capacity(8 + apdu.len());
549 encode_npdu(&mut buf, &npdu)?;
550 self.transport
551 .send_broadcast_with_data_attributes(&buf, data_attributes)
552 .await
553 }
554
555 /// Send an APDU to a remote device through a local router.
556 ///
557 /// The NPDU is sent via unicast to `router_mac` (the next-hop router on
558 /// the local network), but the NPDU header addresses the final destination
559 /// with `dest_network` / `dest_mac`. Refuses `dest_network` 0 and 0xFFFF
560 /// without sending anything; this form and
561 /// [`Self::send_apdu_routed_via_local_broadcast`] share that check. A
562 /// global broadcast unicast to one router would reach only that router's
563 /// networks, so it goes through [`Self::broadcast_global_apdu`] instead.
564 ///
565 /// An empty `dest_mac` (DLEN 0) asks the router to broadcast on
566 /// `dest_network`, a remote broadcast, so it carries only an
567 /// Unconfirmed-Request APDU (Clause 6.3); anything else is refused
568 /// without sending anything (#1479).
569 pub async fn send_apdu_routed(
570 &self,
571 apdu: &[u8],
572 dest_network: u16,
573 dest_mac: &[u8],
574 router_mac: &[u8],
575 expecting_reply: bool,
576 priority: NetworkPriority,
577 ) -> Result<(), Error> {
578 self.send_apdu_routed_with_data_attributes(
579 apdu,
580 RoutedTarget {
581 network: dest_network,
582 mac: dest_mac,
583 router_mac,
584 },
585 expecting_reply,
586 priority,
587 &[],
588 )
589 .await
590 }
591
592 /// Send an APDU with data attributes to a remote device through a local router.
593 pub async fn send_apdu_routed_with_data_attributes(
594 &self,
595 apdu: &[u8],
596 target: RoutedTarget<'_>,
597 expecting_reply: bool,
598 priority: NetworkPriority,
599 data_attributes: &[DataAttribute],
600 ) -> Result<(), Error> {
601 check_destination(target.network, "use send_apdu for a local device")?;
602 if target.mac.is_empty() {
603 check_broadcast_apdu(
604 apdu,
605 "a routed send with an empty dest_mac (a remote broadcast)",
606 )?;
607 }
608 let buf = Self::encode_routed_npdu_buf(
609 apdu,
610 target.network,
611 target.mac,
612 expecting_reply,
613 priority,
614 )?;
615 self.transport
616 .send_unicast_with_data_attributes(&buf, target.router_mac, data_attributes)
617 .await
618 }
619
620 /// Send a routed APDU with a broadcast link DA, for when the next-hop
621 /// router's MAC is unknown.
622 ///
623 /// Clause 6.5.3 normally addresses the frame to the MAC of the router
624 /// that serves DNET, and falls back to a link broadcast while that
625 /// router has not been located yet. DNET/DADR still select a single
626 /// device in the NPDU. Clause 6.3 reserves broadcast network addresses
627 /// for Unconfirmed-Request PDUs, but that limits the network-layer
628 /// address, not the link DA: a link broadcast carrying a one-device
629 /// DNET/DADR is fine for any PDU type, so this send form stays
630 /// conformant. DNET 0xFFFF would make it a global broadcast, which this
631 /// form doesn't mean, so it refuses that DNET as [`Self::send_apdu_routed`]
632 /// does. An empty `dest_mac` would make it a broadcast on `dest_network`,
633 /// which [`Self::broadcast_to_network`] already sends, so this form
634 /// refuses it too and points there, leaving one way to send a remote
635 /// broadcast with the link's broadcast MAC (#1479).
636 pub async fn send_apdu_routed_via_local_broadcast(
637 &self,
638 apdu: &[u8],
639 dest_network: u16,
640 dest_mac: &[u8],
641 expecting_reply: bool,
642 priority: NetworkPriority,
643 ) -> Result<(), Error> {
644 self.send_apdu_routed_via_local_broadcast_with_data_attributes(
645 apdu,
646 dest_network,
647 dest_mac,
648 expecting_reply,
649 priority,
650 &[],
651 )
652 .await
653 }
654
655 /// Send a routed APDU with data attributes and a broadcast link DA.
656 pub async fn send_apdu_routed_via_local_broadcast_with_data_attributes(
657 &self,
658 apdu: &[u8],
659 dest_network: u16,
660 dest_mac: &[u8],
661 expecting_reply: bool,
662 priority: NetworkPriority,
663 data_attributes: &[DataAttribute],
664 ) -> Result<(), Error> {
665 check_destination(dest_network, "use send_apdu for a local device")?;
666 if dest_mac.is_empty() {
667 return Err(Error::Encoding(format!(
668 "an empty dest_mac addresses the broadcast on network {dest_network}, \
669 not one device; use broadcast_to_network for a remote broadcast"
670 )));
671 }
672 let buf =
673 Self::encode_routed_npdu_buf(apdu, dest_network, dest_mac, expecting_reply, priority)?;
674 self.transport
675 .send_broadcast_with_data_attributes(&buf, data_attributes)
676 .await
677 }
678
679 /// Attach selected-object protection before starting the owned transport.
680 #[doc(hidden)]
681 pub fn retain_network_port_lease_internal(&mut self, lease: Arc<()>) -> Result<(), Error> {
682 self.transport.retain_network_port_lease_internal(lease)
683 }
684
685 /// Access the underlying transport.
686 ///
687 /// Useful for transport-specific operations like BBMD registration
688 /// after the network layer has been started.
689 pub fn transport(&self) -> &T {
690 &self.transport
691 }
692
693 /// Get the transport's local MAC address.
694 pub fn local_mac(&self) -> &[u8] {
695 self.transport.local_mac()
696 }
697
698 /// The number of the network this layer's one port is attached to, as
699 /// the owner of its Number controls last published it, and unknown until
700 /// then. The full server and the standalone client publish into their own
701 /// layers internally; a stack built directly on this layer publishes its
702 /// own, and can clone the handle to publish from another task.
703 pub fn local_network_number(&self) -> &LocalNetworkNumber {
704 &self.local_network_number
705 }
706
707 /// Sequence assigned to the most recent opted-in control admission attempt.
708 ///
709 /// The value advances before admission, including Full and Closed drops,
710 /// but not for controls discarded without an enabled stream or after its
711 /// first Closed drop. See the [receive-queue contract](self#receive-queue-admission).
712 /// At counter exhaustion it remains at `u64::MAX`, which makes sequence-based
713 /// consumers fail closed rather than accepting an alias.
714 pub fn network_control_ingress_sequence(&self) -> u64 {
715 self.network_control_ingress_sequence.load(Ordering::SeqCst)
716 }
717
718 /// Inbound NPDUs dropped since this layer was created because an address
719 /// in them was past [`NpduAddress::MAX_MAC_LEN`]: their DLEN or SLEN
720 /// (#1141), or the link-layer source MAC the transport reported (#1198).
721 /// Saturates at `u64::MAX`.
722 ///
723 /// Only routers answer an NPDU with Reject-Message-To-Network (Clause
724 /// 6.6.3.5), so this non-router layer discards the NPDU and counts it.
725 /// The NPDU reaches neither the APDU nor the control receiver, so it
726 /// never shows up in their admission counters. No built-in transport
727 /// reports a source MAC that long, so a nonzero count with well-formed
728 /// traffic points at a custom transport.
729 pub fn address_length_drops(&self) -> u64 {
730 self.address_length_drops.load(Ordering::Relaxed)
731 }
732
733 /// Inbound NPDUs dropped since this layer was created because their DNET
734 /// was 0xFFFF and they also carried a DADR (#1379). Saturates at
735 /// `u64::MAX`.
736 ///
737 /// DNET 0xFFFF already names every device on every network (Clauses
738 /// 6.2.2 and 6.3.2), so a DADR beside it contradicts it. Such an NPDU
739 /// reaches neither the APDU nor the control receiver, so it never shows
740 /// up in their admission counters. It is counted here rather than in
741 /// [`Self::address_length_drops`], whose addresses are too long to hold:
742 /// this one is well formed, only contradictory, and points at the peer
743 /// that sent it.
744 pub fn global_broadcast_dadr_drops(&self) -> u64 {
745 self.global_broadcast_dadr_drops.load(Ordering::Relaxed)
746 }
747
748 /// Inbound NPDUs dropped since this layer was created because they were
749 /// addressed to a broadcast, global (DNET 0xFFFF) or remote (a DNET with
750 /// DLEN 0), and carried an APDU other than an Unconfirmed-Request
751 /// (#1491). Saturates at `u64::MAX`.
752 ///
753 /// Only an Unconfirmed-Request may go to a broadcast network address
754 /// (Clause 6.3), so this layer hands no other APDU sent that way to the
755 /// application, and the NPDU never shows up in the receivers' admission
756 /// counters. A remote broadcast, which a non-router discards anyway, is
757 /// counted here too when its APDU is of another type. The PDU type comes
758 /// from the first APDU octet, with no decode; network messages, and an
759 /// APDU sent with no DNET, are not affected.
760 pub fn broadcast_pdu_type_drops(&self) -> u64 {
761 self.broadcast_pdu_type_drops.load(Ordering::Relaxed)
762 }
763
764 /// Encode an APDU into an NPDU whose destination is `dest_network` /
765 /// `dest_mac`, ready for whichever link send the caller chooses. Each
766 /// caller checks the DNET, and what an empty `dest_mac` may carry, first.
767 fn encode_routed_npdu_buf(
768 apdu: &[u8],
769 dest_network: u16,
770 dest_mac: &[u8],
771 expecting_reply: bool,
772 priority: NetworkPriority,
773 ) -> Result<BytesMut, Error> {
774 let npdu = Npdu {
775 is_network_message: false,
776 expecting_reply,
777 priority,
778 destination: Some(NpduAddress {
779 network: dest_network,
780 mac_address: MacAddr::from_slice(dest_mac),
781 }),
782 source: None,
783 hop_count: 255,
784 payload: Bytes::copy_from_slice(apdu),
785 ..Npdu::default()
786 };
787 let mut buf = BytesMut::with_capacity(8 + dest_mac.len() + apdu.len());
788 encode_npdu(&mut buf, &npdu)?;
789 Ok(buf)
790 }
791
792 /// Stop the network layer and underlying transport.
793 ///
794 /// After start, aborts and awaits dispatch before stopping the transport;
795 /// it does not wait for consumers, even with full APDU/control queues.
796 /// Queued items remain drainable, retaining their reply senders until
797 /// consumed or the receiver is dropped. Stop does not count as an admission
798 /// drop or clear depth/high-water/drop totals. See the
799 /// [receive-queue contract](self#receive-queue-admission).
800 pub async fn stop(&mut self) -> Result<(), Error> {
801 self.seal_responses();
802 if let Some(task) = self.abort_dispatch_task() {
803 let _ = task.await;
804 }
805 self.transport.stop().await
806 }
807}
808
809/// Advance a control ingress sequence, saturating at `u64::MAX`. Shared by
810/// [`NetworkLayer`] and the router's control receiver.
811pub(crate) fn next_ingress_sequence(sequence: &AtomicU64) -> u64 {
812 #[allow(deprecated, reason = "try_update needs Rust 1.95; the MSRV is 1.93")]
813 let previous = sequence
814 .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |value| {
815 value.checked_add(1)
816 })
817 .unwrap_or(u64::MAX);
818 previous.saturating_add(1)
819}
820
821/// Count one dropped NPDU in `counter`, saturating at `u64::MAX`. Shared by
822/// [`NetworkLayer`] and the router for each of their drop counters.
823pub(crate) fn count_drop(counter: &AtomicU64) {
824 #[allow(deprecated, reason = "try_update needs Rust 1.95; the MSRV is 1.93")]
825 let _ = counter.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| n.checked_add(1));
826}
827
828impl<T: TransportPort> NetworkLayer<T> {
829 fn abort_dispatch_task(&mut self) -> Option<JoinHandle<()>> {
830 let task = self.dispatch_task.take()?;
831 task.abort();
832 Some(task)
833 }
834}
835
836impl<T: TransportPort> Drop for NetworkLayer<T> {
837 fn drop(&mut self) {
838 self.response_scope.seal();
839 let _ = self.abort_dispatch_task();
840 self.transport.abort();
841 }
842}
843
844#[cfg(test)]
845#[path = "issuance_tests.rs"]
846mod issuance_tests;
847
848#[cfg(test)]
849mod tests {
850 use super::*;
851 use bacnet_encoding::npdu::decode_npdu;
852 use bacnet_transport::bip::BipTransport;
853 use bacnet_transport::sc::{LoopbackWebSocket, ScTransport, WebSocketPort};
854 use bacnet_transport::sc_frame::{
855 decode_sc_message, encode_sc_message, ScFunction, ScMessage, Vmac,
856 };
857 use std::net::Ipv4Addr;
858 use tokio::time::{timeout, Duration};
859
860 async fn sc_hub_accept(ws_hub: &LoopbackWebSocket, hub_vmac: Vmac) {
861 let data = ws_hub.recv().await.unwrap();
862 let req = decode_sc_message(&data).unwrap();
863 assert_eq!(req.function, ScFunction::ConnectRequest);
864
865 let mut accept_payload = Vec::with_capacity(26);
866 accept_payload.extend_from_slice(&hub_vmac);
867 accept_payload.extend_from_slice(&[0x33; 16]);
868 accept_payload.extend_from_slice(&1476u16.to_be_bytes());
869 accept_payload.extend_from_slice(&1476u16.to_be_bytes());
870
871 let accept = ScMessage {
872 function: ScFunction::ConnectAccept,
873 message_id: req.message_id,
874 originating_vmac: None,
875 destination_vmac: None,
876 dest_options: Vec::new(),
877 data_options: Vec::new(),
878 payload: Bytes::from(accept_payload),
879 };
880 let mut buf = BytesMut::new();
881 encode_sc_message(&mut buf, &accept);
882 ws_hub.send(&buf).await.unwrap();
883 }
884
885 async fn assert_sc_socket_closed_after_drop(ws_hub: &LoopbackWebSocket, context: &str) {
886 timeout(Duration::from_secs(1), async {
887 while let Ok(data) = ws_hub.recv().await {
888 let msg = decode_sc_message(&data).unwrap();
889 assert_ne!(
890 msg.function,
891 ScFunction::HeartbeatAck,
892 "{context} must not leave SC answering heartbeats"
893 );
894 }
895 })
896 .await
897 .unwrap_or_else(|_| panic!("{context} did not close the SC WebSocket"));
898
899 let heartbeat = ScMessage {
900 function: ScFunction::HeartbeatRequest,
901 message_id: 0x66,
902 originating_vmac: None,
903 destination_vmac: None,
904 dest_options: Vec::new(),
905 data_options: Vec::new(),
906 payload: Bytes::new(),
907 };
908 let mut buf = BytesMut::new();
909 encode_sc_message(&mut buf, &heartbeat);
910 assert!(
911 ws_hub.send(&buf).await.is_err(),
912 "{context} must reject post-drop Heartbeat-Request on the closed socket"
913 );
914 }
915
916 #[tokio::test]
917 async fn network_layer_drop_releases_sc_transport_socket() {
918 let (ws_client, ws_hub) = LoopbackWebSocket::pair();
919 let hub_vmac = [0x10; 6];
920 let mut net =
921 NetworkLayer::new(ScTransport::new(ws_client, [0x01; 6]).with_device_uuid([1; 16]));
922
923 let hub_accept_task = tokio::spawn(async move {
924 sc_hub_accept(&ws_hub, hub_vmac).await;
925 ws_hub
926 });
927
928 let _rx = net.start().await.unwrap();
929 let ws_hub = hub_accept_task.await.unwrap();
930
931 drop(net);
932
933 assert_sc_socket_closed_after_drop(&ws_hub, "dropped NetworkLayer").await;
934 }
935
936 #[tokio::test]
937 async fn send_apdu_data_attributes_reach_sc_data_options() {
938 let (ws_client, ws_hub) = LoopbackWebSocket::pair();
939 let hub_vmac = [0x10; 6];
940 let dest_vmac: Vmac = [0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
941 let mut net =
942 NetworkLayer::new(ScTransport::new(ws_client, [0x01; 6]).with_device_uuid([1; 16]));
943 let data_attributes = vec![
944 DataAttribute {
945 option_type: 1,
946 must_understand: true,
947 data: Vec::new(),
948 },
949 DataAttribute {
950 option_type: 31,
951 must_understand: false,
952 data: vec![0x12, 0x34, 0x56],
953 },
954 ];
955
956 let hub_accept_task = tokio::spawn(async move {
957 sc_hub_accept(&ws_hub, hub_vmac).await;
958 ws_hub
959 });
960
961 let _rx = net.start().await.unwrap();
962 let ws_hub = hub_accept_task.await.unwrap();
963
964 let apdu = Bytes::from_static(&[0x10, 0x08]);
965 net.send_apdu_with_data_attributes(
966 &apdu,
967 &dest_vmac,
968 false,
969 NetworkPriority::NORMAL,
970 &data_attributes,
971 )
972 .await
973 .unwrap();
974
975 let data = ws_hub.recv().await.unwrap();
976 let msg = decode_sc_message(&data).unwrap();
977 assert_eq!(msg.function, ScFunction::EncapsulatedNpdu);
978 assert_eq!(msg.destination_vmac, Some(dest_vmac));
979 assert_eq!(msg.data_options.len(), 2);
980 assert_eq!(msg.data_options[0].option_type, 1);
981 assert!(msg.data_options[0].must_understand);
982 assert_eq!(msg.data_options[1].option_type, 31);
983 assert!(!msg.data_options[1].must_understand);
984 assert_eq!(msg.data_options[1].data, vec![0x12, 0x34, 0x56]);
985
986 let npdu = decode_npdu(msg.payload).unwrap();
987 assert_eq!(npdu.payload, apdu);
988 assert!(!npdu.expecting_reply);
989 assert_eq!(npdu.priority, NetworkPriority::NORMAL);
990
991 net.stop().await.unwrap();
992 }
993
994 #[tokio::test]
995 async fn end_to_end_who_is() {
996 use bacnet_encoding::apdu::{decode_apdu, encode_apdu, Apdu, UnconfirmedRequest};
997 use bacnet_types::enums::UnconfirmedServiceChoice;
998
999 let transport_a = BipTransport::new(Ipv4Addr::LOCALHOST, 0, Ipv4Addr::BROADCAST);
1000 let transport_b = BipTransport::new(Ipv4Addr::LOCALHOST, 0, Ipv4Addr::BROADCAST);
1001
1002 let mut net_a = NetworkLayer::new(transport_a);
1003 let mut net_b = NetworkLayer::new(transport_b);
1004
1005 let _rx_a = net_a.start().await.unwrap();
1006 let mut rx_b = net_b.start().await.unwrap();
1007
1008 let who_is_apdu = Apdu::UnconfirmedRequest(UnconfirmedRequest {
1009 service_choice: UnconfirmedServiceChoice::WHO_IS,
1010 service_request: Bytes::new(),
1011 });
1012 let mut apdu_buf = BytesMut::new();
1013 encode_apdu(&mut apdu_buf, &who_is_apdu).expect("valid APDU encoding");
1014
1015 net_a
1016 .send_apdu(&apdu_buf, net_b.local_mac(), false, NetworkPriority::NORMAL)
1017 .await
1018 .unwrap();
1019
1020 let received = timeout(Duration::from_secs(2), rx_b.recv())
1021 .await
1022 .expect("Timed out waiting for APDU")
1023 .expect("Channel closed");
1024
1025 let decoded_apdu = decode_apdu(received.apdu.clone()).unwrap();
1026 match decoded_apdu {
1027 Apdu::UnconfirmedRequest(req) => {
1028 assert_eq!(req.service_choice, UnconfirmedServiceChoice::WHO_IS);
1029 assert!(req.service_request.is_empty());
1030 }
1031 other => panic!("Expected UnconfirmedRequest, got {:?}", other),
1032 }
1033
1034 net_a.stop().await.unwrap();
1035 net_b.stop().await.unwrap();
1036 }
1037
1038 #[test]
1039 fn transport_accessor() {
1040 let transport = BipTransport::new(Ipv4Addr::LOCALHOST, 0, Ipv4Addr::BROADCAST);
1041 let net = NetworkLayer::new(transport);
1042 let mac = net.transport().local_mac();
1043 assert_eq!(mac.len(), 6);
1044 }
1045}
1046
1047#[cfg(test)]
1048#[path = "layer_delivery_tests.rs"]
1049mod delivery_tests;
1050
1051#[cfg(test)]
1052#[path = "layer_admission_tests.rs"]
1053mod admission_tests;
1054
1055#[cfg(test)]
1056#[path = "layer_sc_data_options_tests.rs"]
1057mod sc_data_options_tests;
1058
1059#[cfg(test)]
1060#[path = "broadcast_pdu_tests.rs"]
1061mod broadcast_pdu_tests;