Skip to main content

bacnet_bip_core/node/
config.rs

1//! A node's fixed configuration: its own address, its subnet, its mode and
2//! the largest datagram it sends, checked once before the engine runs.
3
4use core::fmt;
5use core::net::SocketAddrV4;
6
7use bacnet_npdu::MAX_NPDU;
8use bacnet_time::Duration;
9
10use crate::LocalSubnet;
11
12/// The largest datagram B/IP sends a [`MAX_NPDU`]-octet NPDU in, the
13/// largest a B/IP data link carries (Table 6-1): a
14/// Forwarded-NPDU, whose 4-octet header and 6-octet originating address
15/// come before the NPDU. It is the default send budget, and a node's
16/// receive buffer should hold at least this much.
17pub const MAX_DATAGRAM: usize = 4 + 6 + MAX_NPDU;
18
19/// The shortest interval between automatic registration attempts: a
20/// shorter configured renewal is raised to it, so a node tries at most ten
21/// times a second.
22pub const MIN_RENEWAL: Duration = Duration::from_millis(100);
23
24/// The longest response timeout [`Foreign::new`] picks.
25pub const DEFAULT_RESPONSE_TIMEOUT: Duration = Duration::from_secs(3);
26
27/// The smallest datagram budget [`Link::validate`] accepts, in octets.
28pub const MIN_BUDGET: u16 = 64;
29
30/// How a B/IP node takes part in its network. It is fixed for the life of
31/// a [`Node`](super::Node); changing it means building a new one. A foreign
32/// device can still move to another BBMD or time-to-live
33/// ([`Node::retarget`](super::Node::retarget)).
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum Mode {
36    /// A device on one of the network's subnets: local broadcasts go to the
37    /// subnet's broadcast address (Annex J.4.1).
38    Normal,
39    /// A foreign device, registered with a BBMD that distributes its
40    /// broadcasts and forwards it the network's (Annex J.5).
41    Foreign(Foreign),
42    /// A BBMD: the receive decisions hand the BBMD's share of each message
43    /// to the caller, which runs it through [`bbmd::Tables`](crate::bbmd::Tables).
44    /// With a multicast group as its subnet ([`LocalSubnet::is_group`]) it
45    /// serves that group (Annex J.8.3), and its BDT must say so
46    /// ([`Bdt::set_serves_group`](crate::bbmd::Bdt::set_serves_group)).
47    Bbmd,
48}
49
50/// When the next automatic registration attempt goes.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52#[non_exhaustive]
53pub enum Retry {
54    /// One renewal interval after the attempt before started, whatever
55    /// came of it. A Distribute-Broadcast-To-Network NAK from the BBMD
56    /// changes nothing.
57    AtRenewal,
58    /// After a failure, sooner: one response timeout after it, doubling
59    /// with each failure in a row, and never later than
60    /// [`Retry::AtRenewal`] would. After a success, one renewal interval
61    /// into the lease it proved, which is at once when that lease started
62    /// with an earlier, unanswered attempt. A Distribute-Broadcast-To-Network
63    /// NAK from the BBMD that answers nothing pulls the next attempt forward
64    /// ([`Node::resolve`](super::Node::resolve)).
65    Backoff,
66}
67
68/// Which Forwarded-NPDUs a foreign device takes.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70#[non_exhaustive]
71pub enum ForwardedTrust {
72    /// Any sender's, by unicast or broadcast.
73    AnySender,
74    /// Only those its own BBMD sent it by unicast, as a BBMD sends a foreign
75    /// device the network's broadcasts (Annex J.4.5). No one else has a
76    /// reason to send it one.
77    ConfiguredBbmd,
78}
79
80/// A foreign device's registration with its BBMD (Annex J.5.2).
81///
82/// It is `#[non_exhaustive]`, so a new setting isn't a breaking change:
83/// outside this crate, start from [`Foreign::new`] and set the fields to
84/// change.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86#[non_exhaustive]
87pub struct Foreign {
88    /// The BBMD, at its B/IP address. Replies are matched against this
89    /// address and port.
90    pub bbmd: SocketAddrV4,
91    /// The time-to-live sent with each registration, in seconds; above 0.
92    pub ttl: u16,
93    /// How often to register again: above zero and below the time-to-live.
94    /// A shorter one than [`MIN_RENEWAL`] is raised to it. Registering more
95    /// often than the time-to-live needs keeps a NAT mapping open (Annex
96    /// J.7.5).
97    pub renewal: Duration,
98    /// How long an attempt waits for its result: above zero. One longer
99    /// than the renewal interval is cut to it. It also sets the answer
100    /// window the lease counts on ([`Lease`](super::Lease)).
101    pub response_timeout: Duration,
102    /// When a failed attempt is tried again.
103    pub retry: Retry,
104    /// How many broadcasts the caller can keep while the first attempt is
105    /// outstanding. [`Node::admit_broadcast`](super::Node::admit_broadcast)
106    /// asks it to hold them until that attempt is answered or times out.
107    /// With 0 they are sent at once.
108    pub hold: u16,
109    /// Which Forwarded-NPDUs are taken.
110    pub trust: ForwardedTrust,
111}
112
113impl Foreign {
114    /// Registration with `bbmd` for `ttl` seconds: renewed every half
115    /// time-to-live (at least [`MIN_RENEWAL`]), a response timeout of that
116    /// or [`DEFAULT_RESPONSE_TIMEOUT`] if shorter, [`Retry::Backoff`], no
117    /// broadcasts held, and Forwarded-NPDUs taken only from `bbmd`.
118    #[must_use]
119    pub const fn new(bbmd: SocketAddrV4, ttl: u16) -> Self {
120        // Half the time-to-live, exactly: a whole number of milliseconds.
121        let renewal = Duration::from_millis(ttl as u64 * 500).max(MIN_RENEWAL);
122        Self {
123            bbmd,
124            ttl,
125            renewal,
126            response_timeout: renewal.min(DEFAULT_RESPONSE_TIMEOUT),
127            retry: Retry::Backoff,
128            hold: 0,
129            trust: ForwardedTrust::ConfiguredBbmd,
130        }
131    }
132
133    /// The time-to-live as a duration.
134    #[must_use]
135    pub const fn ttl_duration(&self) -> Duration {
136        Duration::from_secs(self.ttl as u64)
137    }
138
139    /// The renewal interval in use: [`renewal`](Self::renewal), at least
140    /// [`MIN_RENEWAL`].
141    #[must_use]
142    pub const fn effective_renewal(&self) -> Duration {
143        self.renewal.max(MIN_RENEWAL)
144    }
145
146    /// The response timeout in use: [`response_timeout`](Self::response_timeout),
147    /// at most the [effective renewal](Self::effective_renewal).
148    #[must_use]
149    pub const fn effective_response_timeout(&self) -> Duration {
150        self.response_timeout.min(self.effective_renewal())
151    }
152}
153
154/// A B/IP node's link: everything its receive and send decisions read.
155/// They are pure, so a host can share one copy among threads and decide
156/// without a lock.
157///
158/// It is `#[non_exhaustive]`, so a new setting isn't a breaking change:
159/// outside this crate, start from [`Link::new`] and set the fields to
160/// change. [`Node::new`](super::Node::new) checks the result with
161/// [`Link::validate`].
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163#[non_exhaustive]
164pub struct Link {
165    /// This node's own B/IP address, the MAC address its peers know it by.
166    pub address: SocketAddrV4,
167    /// Where its local broadcasts go (the subnet's broadcast address at the
168    /// network's port), and the subnet mask a BBMD uses.
169    pub subnet: LocalSubnet,
170    /// How it takes part in the network.
171    pub mode: Mode,
172    /// The longest datagram it sends, in octets, or `None` to send any NPDU
173    /// the codec can carry. See [`Link::max_npdu`].
174    pub budget: Option<u16>,
175}
176
177impl Link {
178    /// A link in `mode`, with the default budget of [`MAX_DATAGRAM`]
179    /// octets, checked by [`Link::validate`].
180    pub const fn new(
181        address: SocketAddrV4,
182        subnet: LocalSubnet,
183        mode: Mode,
184    ) -> Result<Self, ConfigError> {
185        let link = Self {
186            address,
187            subnet,
188            mode,
189            budget: Some(MAX_DATAGRAM as u16),
190        };
191        match link.validate() {
192            Ok(()) => Ok(link),
193            Err(error) => Err(error),
194        }
195    }
196
197    /// Check the configuration: a nonzero own port, a usable foreign-device
198    /// registration, a budget of at least [`MIN_BUDGET`] octets, and no
199    /// multicast group (a class D [`LocalSubnet::broadcast`]) in
200    /// [`Mode::Foreign`]. A normal node with a group is a B/IP-M member, and
201    /// a BBMD with one serves the group (Annex J.8.3).
202    pub const fn validate(&self) -> Result<(), ConfigError> {
203        if self.address.port() == 0 {
204            return Err(ConfigError::OwnPort);
205        }
206        if self.subnet.is_group() && matches!(self.mode, Mode::Foreign(_)) {
207            return Err(ConfigError::MulticastMode);
208        }
209        if let Some(budget) = self.budget
210            && budget < MIN_BUDGET
211        {
212            return Err(ConfigError::Budget);
213        }
214        if let Mode::Foreign(foreign) = &self.mode {
215            let ttl = foreign.ttl_duration().as_nanos();
216            let renewal = foreign.renewal.as_nanos();
217            if foreign.ttl == 0 || renewal == 0 || renewal >= ttl {
218                return Err(ConfigError::Renewal);
219            }
220            if foreign.response_timeout.as_nanos() == 0 {
221                return Err(ConfigError::ResponseTimeout);
222            }
223            if foreign.bbmd.port() == 0 {
224                return Err(ConfigError::BbmdPort);
225            }
226            if foreign.bbmd.ip().is_unspecified() || self.groups().contains(foreign.bbmd) {
227                return Err(ConfigError::BbmdAddress);
228            }
229            let (bbmd, own) = (foreign.bbmd, self.address);
230            if bbmd.ip().to_bits() == own.ip().to_bits() && bbmd.port() == own.port() {
231                return Err(ConfigError::BbmdIsSelf);
232            }
233        }
234        Ok(())
235    }
236}
237
238/// Why a [`Link`] can't run.
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240#[non_exhaustive]
241pub enum ConfigError {
242    /// The node's own UDP port is 0.
243    OwnPort,
244    /// A foreign device's time-to-live is 0, or its renewal interval isn't
245    /// above zero and below the time-to-live.
246    Renewal,
247    /// A foreign device's response timeout is zero.
248    ResponseTimeout,
249    /// A foreign device's BBMD has UDP port 0.
250    BbmdPort,
251    /// A foreign device's BBMD is the unspecified address or a group
252    /// address ([`Groups::contains`](super::Groups::contains)), which no
253    /// BBMD answers from.
254    BbmdAddress,
255    /// A foreign device's BBMD is the node itself.
256    BbmdIsSelf,
257    /// The datagram budget is below [`MIN_BUDGET`] octets.
258    Budget,
259    /// [`Node::retarget`](super::Node::retarget) on a node that is not a
260    /// foreign device.
261    NotForeign,
262    /// A multicast group as the subnet's broadcast address (B/IP-M, Annex
263    /// J.8) in [`Mode::Foreign`]: a foreign device broadcasts through its
264    /// BBMD, never to a group.
265    MulticastMode,
266}
267
268impl fmt::Display for ConfigError {
269    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270        f.write_str(match self {
271            Self::OwnPort => "the node's own UDP port is 0",
272            Self::Renewal => {
273                "foreign-device mode needs TTL > 0 and a renewal interval > 0 and < TTL"
274            }
275            Self::ResponseTimeout => "foreign-device mode needs a response timeout > 0",
276            Self::BbmdPort => "the BBMD's UDP port is 0",
277            Self::BbmdAddress => "the BBMD's address is unspecified or a group address",
278            Self::BbmdIsSelf => "the BBMD is this node's own address",
279            Self::Budget => "the datagram budget is below 64 octets",
280            Self::NotForeign => "only a foreign device can change its BBMD",
281            Self::MulticastMode => "a foreign device can't use a multicast group",
282        })
283    }
284}
285
286impl core::error::Error for ConfigError {}