bacnet_bip_core/node/receive.rs
1//! What a node does with one datagram: the receive rules, in order, and the
2//! dispatch by BVLC function and mode.
3
4use core::net::SocketAddrV4;
5
6use bacnet_bip_codec::{BvlcFunction, BvlcResultCode, Bvll, BvllError, BvllFrame};
7use bacnet_time::Instant;
8
9use super::addressing::Addressed;
10use super::config::{ForwardedTrust, Link, Mode};
11use crate::Arrival;
12use crate::bbmd::Request;
13
14/// One received datagram, as the host's socket reported it.
15///
16/// It is `#[non_exhaustive]`, so a new field isn't a breaking change:
17/// outside this crate, build it with [`Datagram::new`] and set the fields
18/// that differ from its defaults.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[non_exhaustive]
21pub struct Datagram<'d> {
22 /// The UDP payload.
23 pub payload: &'d [u8],
24 /// The UDP source: the sender's B/IP address, at any port (Annex J.1.2).
25 pub source: SocketAddrV4,
26 /// What the destination address was, for this node, or `None` when the
27 /// host can't tell: its IP stack reports no destination per datagram
28 /// (#1678). Such a datagram is discarded
29 /// ([`Discard::UnknownDestination`]): whether it came by unicast or
30 /// broadcast decides what a node may do with it, so the node never
31 /// guesses.
32 pub to: Option<Addressed>,
33 /// Which of the host's sockets read it.
34 pub path: Path,
35 /// Whether the OS or the receive buffer cut the datagram short.
36 pub truncated: bool,
37}
38
39impl<'d> Datagram<'d> {
40 /// `payload` from `source`, sent to what `to` says, read whole by the
41 /// [`Primary`](Path::Primary) socket. A host with a broadcast listener
42 /// sets [`path`](Self::path), and one whose read was cut short sets
43 /// [`truncated`](Self::truncated).
44 #[must_use]
45 pub const fn new(payload: &'d [u8], source: SocketAddrV4, to: Option<Addressed>) -> Self {
46 Self {
47 payload,
48 source,
49 to,
50 path: Path::Primary,
51 truncated: false,
52 }
53 }
54}
55
56/// Which socket read a datagram. The host builds it, and the node matches
57/// it.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59#[non_exhaustive]
60pub enum Path {
61 /// The socket every send leaves from.
62 Primary,
63 /// A receive-only broadcast listener beside a socket bound to the
64 /// interface address, which keeps only this link's broadcasts.
65 Listener {
66 /// The host saw it arrive on another interface than the node's.
67 other_interface: bool,
68 },
69}
70
71/// The host's own admission rules, which [`Link::receive`] consults.
72/// [`Permissive`] admits everything.
73pub trait Policy {
74 /// Whether a Read-Broadcast-Distribution-Table, Read-Foreign-Device-Table,
75 /// Register-Foreign-Device or Delete-Foreign-Device-Table-Entry of
76 /// `function` from `source` goes on, before its payload is read: a rate
77 /// limit. One refused is dropped unanswered.
78 fn admit_management(
79 &mut self,
80 source: SocketAddrV4,
81 function: BvlcFunction,
82 now: Instant,
83 ) -> bool {
84 let _ = (source, function, now);
85 true
86 }
87
88 /// Whether a table ACK the codec decoded is acceptable, such as within
89 /// the host's size limit. One refused counts as malformed, and still
90 /// completes the exchange it matches, with the ACK as received.
91 fn accept_table(&mut self, ack: &Bvll<'_>) -> bool {
92 let _ = ack;
93 true
94 }
95}
96
97/// A [`Policy`] that admits everything: for firmware and tests.
98#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
99pub struct Permissive;
100
101impl Policy for Permissive {}
102
103/// What to do with a received datagram. Each borrows the datagram, not the
104/// link, so a host can act on it after releasing any lock.
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum Received<'d> {
107 /// Hand an NPDU up to the network layer.
108 Deliver(Delivery<'d>),
109 /// Send a BVLC-Result with `code` to `to`: the NAK for a BBMD-only
110 /// request this node doesn't serve (Annex J.2.1).
111 Nak {
112 /// Where the NAK goes: the request's source.
113 to: SocketAddrV4,
114 /// The request's own NAK.
115 code: BvlcResultCode,
116 /// The request's payload was malformed; count it so.
117 malformed: bool,
118 },
119 /// A BVLC-Result or table ACK, for [`Node::resolve`](super::Node::resolve)
120 /// to match against this node's own requests.
121 Response(Response<'d>),
122 /// The BBMD's share of a message: [`Mode::Bbmd`] only.
123 Bbmd(ForBbmd<'d>),
124 /// Dropped, for this reason.
125 Discarded(Discard),
126}
127
128/// An NPDU to hand up.
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130#[non_exhaustive]
131pub struct Delivery<'d> {
132 /// The NPDU.
133 pub npdu: &'d [u8],
134 /// Its source B/IP address: the sender, or a Forwarded-NPDU's origin
135 /// (Annex J.2.5).
136 pub source: SocketAddrV4,
137 /// Whether it reached more than this node (a broadcast), so the network
138 /// layer never answers it as a directed message.
139 pub group: bool,
140}
141
142/// A response to one of this node's management requests, before it is
143/// matched. [`Link::receive`] builds it, and
144/// [`Node::resolve`](super::Node::resolve) takes it back.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146#[non_exhaustive]
147pub struct Response<'d> {
148 /// Who sent it.
149 pub peer: SocketAddrV4,
150 /// Its function: BVLC-Result, Read-BDT-Ack or Read-FDT-Ack.
151 pub function: BvlcFunction,
152 /// The message, or why the codec refused its payload.
153 pub message: Result<Bvll<'d>, BvllError>,
154 /// Whether the codec decoded it and [`Policy::accept_table`] took it.
155 pub accepted: bool,
156}
157
158/// Why a datagram was dropped: each is counted once, under the first rule
159/// it fails, in this order.
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161#[non_exhaustive]
162pub enum Discard {
163 /// The OS or the buffer cut it short.
164 Truncated,
165 /// Its UDP source is a group address ([`Groups::contains_ip`](super::Groups::contains_ip)).
166 GroupSource,
167 /// The host couldn't say where it was sent ([`Datagram::to`] is
168 /// `None`), so nothing tells a unicast from a broadcast (#1678).
169 UnknownDestination,
170 /// A broadcast listener read it but doesn't keep it: not sent to this
171 /// link's broadcast address, or it arrived on another interface.
172 ListenerMismatch {
173 /// It arrived on another interface.
174 other_interface: bool,
175 },
176 /// Shorter than the 4-octet BVLL header.
177 TooShort,
178 /// The BVLC type isn't B/IP's.
179 NotBip,
180 /// The Length field differs from the datagram's length.
181 LengthMismatch,
182 /// Its function doesn't fit the address it was sent to.
183 DestinationMismatch,
184 /// A management request the [`Policy`] refused.
185 RateLimited,
186 /// A function Annex J doesn't define.
187 UnknownFunction,
188 /// The payload doesn't fit the function, and nothing answers it.
189 MalformedPayload,
190 /// It comes from this node: from its own address, or a Forwarded-NPDU
191 /// naming it as the origin.
192 OwnEcho,
193 /// A Forwarded-NPDU whose origin is a group address.
194 GroupOrigin,
195 /// A request whose NAK couldn't reach anyone: from the unspecified
196 /// address, port 0, a group address or this node.
197 UnanswerableSource,
198 /// A Forwarded-NPDU a foreign device doesn't take from this sender
199 /// ([`ForwardedTrust::ConfiguredBbmd`]).
200 ForwardedUntrusted,
201}
202
203impl Discard {
204 /// Why a datagram failed to decode.
205 pub(crate) const fn of_decode_error(error: &BvllError) -> Self {
206 match error {
207 BvllError::TooShort { .. } => Self::TooShort,
208 BvllError::NotBip { .. } => Self::NotBip,
209 BvllError::LengthMismatch { .. } => Self::LengthMismatch,
210 BvllError::UnknownFunction { .. } => Self::UnknownFunction,
211 _ => Self::MalformedPayload,
212 }
213 }
214}
215
216/// The share of a message a BBMD acts on through
217/// [`bbmd::Tables`](crate::bbmd::Tables), once every node rule has passed.
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub enum ForBbmd<'d> {
220 /// An Original-Broadcast-NPDU: deliver it, then forward it
221 /// ([`Tables::original_broadcast`](crate::bbmd::Tables::original_broadcast)).
222 OriginalBroadcast(Delivery<'d>),
223 /// A Forwarded-NPDU: [`Tables::forwarded_npdu`](crate::bbmd::Tables::forwarded_npdu)
224 /// decides whether it is delivered and where it goes.
225 Forwarded {
226 /// The BBMD that sent it.
227 sender: SocketAddrV4,
228 /// How it arrived.
229 arrival: Arrival,
230 /// Its originating address, the NPDU's source.
231 origin: SocketAddrV4,
232 /// The NPDU.
233 npdu: &'d [u8],
234 },
235 /// A Distribute-Broadcast-To-Network:
236 /// [`Tables::distribute_broadcast`](crate::bbmd::Tables::distribute_broadcast),
237 /// and its NAK on refusal.
238 Distribute {
239 /// The foreign device that sent it.
240 sender: SocketAddrV4,
241 /// The NPDU.
242 npdu: &'d [u8],
243 },
244 /// A request for the BBMD's tables (Register-Foreign-Device, the two
245 /// table reads, Delete-Foreign-Device-Table-Entry), which
246 /// [`Tables::serve`](crate::bbmd::Tables::serve) answers.
247 Management(Request),
248}
249
250impl<'d> ForBbmd<'d> {
251 /// The BBMD's share of a well-formed BBMD-only request from `sender`;
252 /// `None` for Write-Broadcast-Distribution-Table, which every device
253 /// refuses (Annex J.4.4.2), and any other message.
254 fn of_request(message: Bvll<'d>, sender: SocketAddrV4) -> Option<Self> {
255 let request = match message {
256 Bvll::DistributeBroadcastToNetwork(npdu) => {
257 return Some(Self::Distribute { sender, npdu });
258 }
259 Bvll::RegisterForeignDevice { ttl } => Request::Register { sender, ttl },
260 Bvll::ReadBroadcastDistributionTable => Request::ReadBdt { sender },
261 Bvll::ReadForeignDeviceTable => Request::ReadFdt { sender },
262 Bvll::DeleteForeignDeviceTableEntry(entry) => Request::DeleteFdtEntry { sender, entry },
263 _ => return None,
264 };
265 Some(Self::Management(request))
266 }
267}
268
269/// The management requests [`Policy::admit_management`] is asked about.
270fn rate_limited(function: BvlcFunction) -> bool {
271 matches!(
272 function,
273 BvlcFunction::READ_BROADCAST_DISTRIBUTION_TABLE
274 | BvlcFunction::READ_FOREIGN_DEVICE_TABLE
275 | BvlcFunction::REGISTER_FOREIGN_DEVICE
276 | BvlcFunction::DELETE_FOREIGN_DEVICE_TABLE_ENTRY
277 )
278}
279
280impl Link {
281 /// Decide what to do with one datagram.
282 ///
283 /// The rules run in this order, and a dropped datagram is
284 /// [`Discarded`](Received::Discarded) under the first it fails:
285 ///
286 /// 1. truncated by the OS or the buffer;
287 /// 2. a UDP source that is a group address;
288 /// 3. a destination the host couldn't report ([`Datagram::to`] is
289 /// `None`);
290 /// 4. on a broadcast listener: not sent to the broadcast address, or
291 /// arrived on another interface;
292 /// 5. the BVLL header: length, type, Length field (Annex J.2);
293 /// 6. the function against the destination ([`Addressed`]); for a
294 /// foreign device, a Forwarded-NPDU sent to its own address is
295 /// unicast, even where that address is its broadcast one too;
296 /// 7. on a broadcast listener, a function that arrived as unicast;
297 /// 8. [`Policy::admit_management`], for the four requests it covers;
298 /// 9. the payload against the function's layout.
299 ///
300 /// Then each function is dispatched by mode. Every one of the six
301 /// BBMD-only requests that arrived by unicast gets its own NAK
302 /// ([`BvlcFunction::nak`]), unless [`Mode::Bbmd`] claims it, from any
303 /// source a reply can reach ([`Link::is_answerable`]). Write-BDT is
304 /// refused in every mode, and so is a malformed BBMD-only request.
305 pub fn receive<'d>(
306 &self,
307 datagram: &Datagram<'d>,
308 now: Instant,
309 policy: &mut impl Policy,
310 ) -> Received<'d> {
311 let source = datagram.source;
312 if datagram.truncated {
313 return Received::Discarded(Discard::Truncated);
314 }
315 if self.groups().contains_ip(*source.ip()) {
316 return Received::Discarded(Discard::GroupSource);
317 }
318 let Some(to) = datagram.to else {
319 return Received::Discarded(Discard::UnknownDestination);
320 };
321 let listener = matches!(datagram.path, Path::Listener { .. });
322 if let Path::Listener { other_interface } = datagram.path
323 && let Some(discard) = listener_address_mismatch(to, other_interface)
324 {
325 return Received::Discarded(discard);
326 }
327 let frame = match BvllFrame::decode(datagram.payload) {
328 Ok(frame) => frame,
329 Err(error) => return Received::Discarded(Discard::of_decode_error(&error)),
330 };
331 let Some(arrival) = self.arrival(to, frame.function) else {
332 return Received::Discarded(Discard::DestinationMismatch);
333 };
334 // Where the broadcast address is also the node's own, as in a
335 // loopback test, a unicast function passes both checks above.
336 if listener && arrival != Arrival::Broadcast {
337 return listener_mismatch(false);
338 }
339 if rate_limited(frame.function) && !policy.admit_management(source, frame.function, now) {
340 return Received::Discarded(Discard::RateLimited);
341 }
342 match frame.message() {
343 Ok(message) => self.dispatch(message, source, arrival, policy),
344 Err(error) => self.malformed(frame.function, error, source),
345 }
346 }
347
348 fn dispatch<'d>(
349 &self,
350 message: Bvll<'d>,
351 source: SocketAddrV4,
352 arrival: Arrival,
353 policy: &mut impl Policy,
354 ) -> Received<'d> {
355 match message {
356 Bvll::OriginalUnicastNpdu(npdu) => {
357 if self.is_own(source) {
358 return Received::Discarded(Discard::OwnEcho);
359 }
360 // From the address, not the function: Original-Unicast is
361 // only ever admitted as unicast.
362 let group = arrival == Arrival::Broadcast;
363 Received::Deliver(Delivery {
364 npdu,
365 source,
366 group,
367 })
368 }
369 Bvll::OriginalBroadcastNpdu(npdu) => {
370 if self.is_own(source) {
371 return Received::Discarded(Discard::OwnEcho);
372 }
373 let delivery = Delivery {
374 npdu,
375 source,
376 group: true,
377 };
378 match self.mode {
379 Mode::Bbmd => Received::Bbmd(ForBbmd::OriginalBroadcast(delivery)),
380 Mode::Normal | Mode::Foreign(_) => Received::Deliver(delivery),
381 }
382 }
383 Bvll::ForwardedNpdu { origin, npdu } => self.forwarded(source, arrival, origin, npdu),
384 Bvll::BvlcResult(_) => Received::Response(Response {
385 peer: source,
386 function: message.function(),
387 message: Ok(message),
388 accepted: true,
389 }),
390 Bvll::ReadBroadcastDistributionTableAck(_) | Bvll::ReadForeignDeviceTableAck(_) => {
391 let accepted = policy.accept_table(&message);
392 Received::Response(Response {
393 peer: source,
394 function: message.function(),
395 message: Ok(message),
396 accepted,
397 })
398 }
399 _ => self.request(message, source),
400 }
401 }
402
403 /// A Forwarded-NPDU: its origin is its NPDU's source (Annex J.2.5).
404 fn forwarded<'d>(
405 &self,
406 sender: SocketAddrV4,
407 arrival: Arrival,
408 origin: SocketAddrV4,
409 npdu: &'d [u8],
410 ) -> Received<'d> {
411 if self.is_own(origin) {
412 return Received::Discarded(Discard::OwnEcho);
413 }
414 if self.groups().contains(origin) {
415 return Received::Discarded(Discard::GroupOrigin);
416 }
417 match self.mode {
418 Mode::Bbmd => {
419 // A local rebroadcast keeps its remote origin; this BBMD's
420 // own row in its BDT must not pass its echo as a peer's.
421 if self.is_own(sender) {
422 return Received::Discarded(Discard::OwnEcho);
423 }
424 Received::Bbmd(ForBbmd::Forwarded {
425 sender,
426 arrival,
427 origin,
428 npdu,
429 })
430 }
431 Mode::Foreign(foreign)
432 if foreign.trust == ForwardedTrust::ConfiguredBbmd
433 && (sender != foreign.bbmd || arrival != Arrival::Unicast) =>
434 {
435 Received::Discarded(Discard::ForwardedUntrusted)
436 }
437 Mode::Normal | Mode::Foreign(_) => Received::Deliver(Delivery {
438 npdu,
439 source: origin,
440 group: true,
441 }),
442 }
443 }
444
445 /// One of the six BBMD-only requests, well formed.
446 fn request<'d>(&self, message: Bvll<'d>, source: SocketAddrV4) -> Received<'d> {
447 let function = message.function();
448 let Some(code) = function.nak() else {
449 return Received::Discarded(Discard::UnknownFunction);
450 };
451 if function == BvlcFunction::DISTRIBUTE_BROADCAST_TO_NETWORK && self.is_own(source) {
452 return Received::Discarded(Discard::OwnEcho);
453 }
454 if self.mode == Mode::Bbmd
455 && let Some(share) = ForBbmd::of_request(message, source)
456 {
457 return Received::Bbmd(share);
458 }
459 if !self.is_answerable(source) {
460 return Received::Discarded(Discard::UnanswerableSource);
461 }
462 Received::Nak {
463 to: source,
464 code,
465 malformed: false,
466 }
467 }
468
469 /// A frame whose payload the codec refused, or whose function it doesn't
470 /// know. A BBMD-only request is still refused with its NAK in every
471 /// mode, since nothing else would answer the requester; a malformed
472 /// response goes on to be matched and counted.
473 fn malformed<'d>(
474 &self,
475 function: BvlcFunction,
476 error: BvllError,
477 source: SocketAddrV4,
478 ) -> Received<'d> {
479 if let Some(code) = function.nak() {
480 if self.is_answerable(source) {
481 return Received::Nak {
482 to: source,
483 code,
484 malformed: true,
485 };
486 }
487 return Received::Discarded(Discard::MalformedPayload);
488 }
489 match function {
490 BvlcFunction::BVLC_RESULT
491 | BvlcFunction::READ_BROADCAST_DISTRIBUTION_TABLE_ACK
492 | BvlcFunction::READ_FOREIGN_DEVICE_TABLE_ACK => Received::Response(Response {
493 peer: source,
494 function,
495 message: Err(error),
496 accepted: false,
497 }),
498 _ => Received::Discarded(Discard::of_decode_error(&error)),
499 }
500 }
501}
502
503impl Link {
504 /// Whether [`receive`](Self::receive) would get past its broadcast
505 /// listener rules (4 and 7) with `datagram`: `false` exactly when it
506 /// would drop it as [`Discard::ListenerMismatch`]. A datagram another
507 /// socket read, or one with no reported destination, passes. A host
508 /// that reports a listener's datagrams somewhere before the node acts
509 /// on them, as a frame tap does, asks this so that it reports only what
510 /// the node keeps (#1856). It reads the frame's header only; a frame
511 /// that doesn't decode passes, for `receive` to count as malformed.
512 #[must_use]
513 pub fn listener_keeps(&self, datagram: &Datagram<'_>) -> bool {
514 let (Some(to), Path::Listener { other_interface }) = (datagram.to, datagram.path) else {
515 return true;
516 };
517 if listener_address_mismatch(to, other_interface).is_some() {
518 return false;
519 }
520 match BvllFrame::decode(datagram.payload) {
521 Ok(frame) => self
522 .arrival(to, frame.function)
523 .is_none_or(|arrival| arrival == Arrival::Broadcast),
524 Err(_) => true,
525 }
526 }
527
528 /// How a frame of `function` sent to `to` arrived, or `None` when the
529 /// destination doesn't fit the function (rule 6).
530 fn arrival(&self, to: Addressed, function: BvlcFunction) -> Option<Arrival> {
531 let arrival = to.arrival(function)?;
532 // A foreign device's BBMD sends it Forwarded-NPDUs by unicast. Where
533 // its broadcast address is also its own, as in a loopback test, each
534 // of them is flagged as both, and counts as the unicast it was
535 // (#1851). The trust rule still asks that the BBMD sent it.
536 if to.node
537 && function == BvlcFunction::FORWARDED_NPDU
538 && matches!(self.mode, Mode::Foreign(_))
539 {
540 return Some(Arrival::Unicast);
541 }
542 Some(arrival)
543 }
544}
545
546/// A broadcast listener's address rule (rule 4): what it read must have been
547/// sent to the broadcast address, on the node's own interface.
548fn listener_address_mismatch(to: Addressed, other_interface: bool) -> Option<Discard> {
549 if !to.broadcast {
550 return Some(Discard::ListenerMismatch {
551 other_interface: false,
552 });
553 }
554 other_interface.then_some(Discard::ListenerMismatch {
555 other_interface: true,
556 })
557}
558
559fn listener_mismatch<'d>(other_interface: bool) -> Received<'d> {
560 Received::Discarded(Discard::ListenerMismatch { other_interface })
561}