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 {}