Skip to main content

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}