1use super::binding_probes::{BindingProbes, WhoIsScope};
2use super::event_recipient_route::RecipientRoute;
3use super::*;
4use bacnet_encoding::primitives::{
5 encode_app_object_id, encode_app_octet_string, encode_app_unsigned,
6};
7
8pub(super) const MAX_DEVICE_BINDINGS: usize = 4096;
10
11pub(super) const OBSERVED_BINDING_TTL: Duration = Duration::from_secs(10 * 60);
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15enum DeviceBindingTarget {
16 Local {
17 peer_mac: MacAddr,
18 },
19 Routed {
20 network: u16,
21 final_mac: MacAddr,
22 router_mac: MacAddr,
23 },
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct DeviceBinding {
34 device: ObjectIdentifier,
35 target: DeviceBindingTarget,
36}
37
38impl DeviceBinding {
39 pub fn local(device: ObjectIdentifier, peer_mac: impl AsRef<[u8]>) -> Result<Self, Error> {
41 validate_device_identifier(device)?;
42 let peer_mac = MacAddr::from_slice(peer_mac.as_ref());
43 if peer_mac.is_empty() {
44 return Err(binding_error("local peer MAC must be non-empty"));
45 }
46 Ok(Self {
47 device,
48 target: DeviceBindingTarget::Local { peer_mac },
49 })
50 }
51
52 pub fn routed(
54 device: ObjectIdentifier,
55 network: u16,
56 final_mac: impl AsRef<[u8]>,
57 router_mac: impl AsRef<[u8]>,
58 ) -> Result<Self, Error> {
59 validate_device_identifier(device)?;
60 if !(1..=0xFFFE).contains(&network) {
61 return Err(binding_error("routed network must be in 1..=65534"));
62 }
63 let final_mac = MacAddr::from_slice(final_mac.as_ref());
64 if final_mac.is_empty() {
65 return Err(binding_error("routed final MAC must be non-empty"));
66 }
67 let router_mac = MacAddr::from_slice(router_mac.as_ref());
68 if router_mac.is_empty() {
69 return Err(binding_error("routed router MAC must be non-empty"));
70 }
71 Ok(Self {
72 device,
73 target: DeviceBindingTarget::Routed {
74 network,
75 final_mac,
76 router_mac,
77 },
78 })
79 }
80}
81
82fn binding_error(message: &str) -> Error {
83 Error::Encoding(format!("invalid device binding: {message}"))
84}
85
86fn validate_device_identifier(device: ObjectIdentifier) -> Result<(), Error> {
87 if device.object_type() != ObjectType::DEVICE {
88 return Err(binding_error("identifier must name a Device object"));
89 }
90 Ok(())
91}
92
93pub(super) fn register_configured_binding(
94 bindings: &mut Vec<DeviceBinding>,
95 binding: DeviceBinding,
96) -> Result<(), Error> {
97 if bindings.len() >= MAX_DEVICE_BINDINGS {
98 return Err(binding_error("configured binding capacity exceeded"));
99 }
100 if bindings
101 .iter()
102 .any(|configured| configured.device == binding.device)
103 {
104 return Err(binding_error("duplicate configured Device identifier"));
105 }
106 bindings.push(binding);
107 Ok(())
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub(super) enum DeviceResolution {
112 ResolvedLocal {
113 peer_mac: MacAddr,
114 freshness: BindingFreshness,
115 },
116 ResolvedRouted {
117 network: u16,
118 final_mac: MacAddr,
119 router_mac: MacAddr,
120 freshness: BindingFreshness,
121 },
122 Unknown,
123 Stale,
124 Invalid,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub(super) enum BindingFreshness {
129 Configured,
130 ObservedUntil(tokio::time::Instant),
131}
132
133impl BindingFreshness {
134 pub(super) fn permits_attempt_at(self, now: tokio::time::Instant) -> bool {
135 match self {
136 Self::Configured => true,
137 Self::ObservedUntil(deadline) => now < deadline,
138 }
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub(super) enum ObservationOutcome {
144 Inserted,
145 Refreshed,
146 ConfiguredPreserved,
147 RejectedInvalid,
148 RejectedCapacity,
149}
150
151#[derive(Debug, Clone)]
152enum BindingEntry {
153 Configured(DeviceBindingTarget),
154 Observed {
155 target: DeviceBindingTarget,
156 observed_at: Instant,
157 },
158}
159
160#[derive(Debug, Default)]
168pub(super) struct DeviceBindingTable {
169 entries: HashMap<ObjectIdentifier, BindingEntry>,
170 pub(super) probes: BindingProbes,
173}
174
175impl DeviceBindingTable {
176 pub(super) fn command_origin(
180 &self,
181 immediate: &[u8],
182 routed: Option<&NpduAddress>,
183 local_network: Option<u16>,
184 is_group: impl Fn(&[u8]) -> bool,
185 ) -> bacnet_objects::command_source::CommandOrigin {
186 bacnet_objects::command_source::CommandOrigin::Remote {
187 actual_address: bacnet_types::constructed::BACnetAddress {
188 network_number: routed.map_or(0, |source| source.network),
189 mac_address: routed.map_or_else(
190 || MacAddr::from_slice(immediate),
191 |source| source.mac_address.clone(),
192 ),
193 },
194 binding: self.source_binding(immediate, routed, local_network, is_group),
195 }
196 }
197
198 pub(super) fn source_device(
201 &self,
202 immediate: &[u8],
203 routed: Option<&NpduAddress>,
204 local_network: Option<u16>,
205 is_group: impl Fn(&[u8]) -> bool,
206 ) -> Option<ObjectIdentifier> {
207 match self.source_binding(immediate, routed, local_network, is_group) {
208 bacnet_objects::command_source::CommandDeviceBinding::Unique(device) => Some(device),
209 _ => None,
210 }
211 }
212
213 pub(super) fn source_binding(
224 &self,
225 immediate: &[u8],
226 routed: Option<&NpduAddress>,
227 local_network: Option<u16>,
228 is_group: impl Fn(&[u8]) -> bool,
229 ) -> bacnet_objects::command_source::CommandDeviceBinding {
230 use bacnet_objects::command_source::CommandDeviceBinding;
231 let now = Instant::now();
232 let mut matched = None;
233 for device in self.entries.keys() {
234 let resolution = self.resolve_at(device, now, &is_group);
235 let route = RecipientRoute::from_device_resolution(resolution).localize(
236 local_network,
237 &is_group,
238 &is_group,
239 );
240 let matches = match (route, routed) {
241 (RecipientRoute::BoundLocalUnicast { mac, .. }, None) => {
242 mac.as_slice() == immediate
243 }
244 (RecipientRoute::BoundLocalUnicast { mac, .. }, Some(source)) => {
245 Some(source.network) == local_network && mac == source.mac_address
246 }
247 (RecipientRoute::BoundRoutedUnicast { network, mac, .. }, Some(source)) => {
248 network == source.network && mac == source.mac_address
249 }
250 _ => false,
251 };
252 if matches {
253 if matched.is_some() {
254 return CommandDeviceBinding::Ambiguous;
255 }
256 matched = Some(*device);
257 }
258 }
259 matched.map_or(CommandDeviceBinding::Unknown, CommandDeviceBinding::Unique)
260 }
261
262 pub(super) fn new() -> Self {
263 Self::default()
264 }
265
266 pub(super) fn from_configured(
267 bindings: Vec<DeviceBinding>,
268 is_group: impl Fn(&[u8]) -> bool,
269 ) -> Result<Self, Error> {
270 let mut table = Self::new();
271 for binding in bindings {
272 table.insert_configured(binding, &is_group)?;
273 }
274 Ok(table)
275 }
276
277 pub(super) fn configured_resolutions(
279 &self,
280 ) -> impl Iterator<Item = (ObjectIdentifier, DeviceResolution)> + '_ {
281 self.entries
282 .iter()
283 .filter(|(_, entry)| matches!(entry, BindingEntry::Configured(_)))
284 .map(|(device, _)| (*device, self.resolve_at(device, Instant::now(), |_| false)))
285 }
286
287 #[cfg(test)]
288 pub(super) fn len(&self) -> usize {
289 self.entries.len()
290 }
291
292 pub(super) fn insert_configured(
293 &mut self,
294 binding: DeviceBinding,
295 is_group: impl Fn(&[u8]) -> bool,
296 ) -> Result<(), Error> {
297 validate_device_identifier(binding.device)?;
298 if let Some((role, mac)) = group_hop(&binding.target, &is_group) {
299 return Err(binding_error(&format!(
300 "Device {} is bound at {} as {role}, a broadcast or group address of \
301 this link; bind the device's unicast address",
302 binding.device.instance_number(),
303 colon_hex(mac)
304 )));
305 }
306 if !target_is_usable(&binding.target, &is_group) {
307 return Err(binding_error("the binding names no reachable device"));
308 }
309 if self.entries.contains_key(&binding.device) {
310 return Err(binding_error("duplicate configured Device identifier"));
311 }
312 if self.entries.len() >= MAX_DEVICE_BINDINGS {
313 return Err(binding_error("configured binding capacity exceeded"));
314 }
315 self.entries
316 .insert(binding.device, BindingEntry::Configured(binding.target));
317 Ok(())
318 }
319
320 pub(super) fn observe_i_am_at(
321 &mut self,
322 device: ObjectIdentifier,
323 source_mac: &[u8],
324 source_network: Option<&NpduAddress>,
325 now: Instant,
326 is_group: impl Fn(&[u8]) -> bool,
327 ) -> ObservationOutcome {
328 if validate_device_identifier(device).is_err() {
329 return ObservationOutcome::RejectedInvalid;
330 }
331 let target = match source_network {
332 None => DeviceBindingTarget::Local {
333 peer_mac: MacAddr::from_slice(source_mac),
334 },
335 Some(source)
336 if (1..=0xFFFE).contains(&source.network) && !source.mac_address.is_empty() =>
337 {
338 DeviceBindingTarget::Routed {
339 network: source.network,
340 final_mac: source.mac_address.clone(),
341 router_mac: MacAddr::from_slice(source_mac),
342 }
343 }
344 Some(_) => return ObservationOutcome::RejectedInvalid,
345 };
346 if !target_is_usable(&target, &is_group) {
347 return ObservationOutcome::RejectedInvalid;
348 }
349
350 let outcome = self.record_observation(device, target, now);
351 if matches!(
352 outcome,
353 ObservationOutcome::Inserted | ObservationOutcome::Refreshed
354 ) {
355 self.probes.answer(&device);
356 }
357 outcome
358 }
359
360 fn record_observation(
361 &mut self,
362 device: ObjectIdentifier,
363 target: DeviceBindingTarget,
364 now: Instant,
365 ) -> ObservationOutcome {
366 match self.entries.get_mut(&device) {
367 Some(BindingEntry::Configured(_)) => ObservationOutcome::ConfiguredPreserved,
368 Some(BindingEntry::Observed {
369 target: observed,
370 observed_at,
371 }) => {
372 *observed = target;
373 *observed_at = now;
374 ObservationOutcome::Refreshed
375 }
376 None => {
377 if self.entries.len() >= MAX_DEVICE_BINDINGS {
378 self.entries.retain(|_, entry| match entry {
379 BindingEntry::Configured(_) => true,
380 BindingEntry::Observed { observed_at, .. } => {
381 now.saturating_duration_since(*observed_at) < OBSERVED_BINDING_TTL
382 }
383 });
384 }
385 if self.entries.len() >= MAX_DEVICE_BINDINGS {
386 return ObservationOutcome::RejectedCapacity;
387 }
388 self.entries.insert(
389 device,
390 BindingEntry::Observed {
391 target,
392 observed_at: now,
393 },
394 );
395 ObservationOutcome::Inserted
396 }
397 }
398 }
399
400 pub(crate) fn address_binding_list(&self, now: Instant) -> PropertyValue {
408 let mut bindings: Vec<_> = self
409 .entries
410 .iter()
411 .filter_map(|(device, entry)| {
412 let target = match entry {
413 BindingEntry::Configured(target) => target,
414 BindingEntry::Observed {
415 target,
416 observed_at,
417 } => {
418 if now.saturating_duration_since(*observed_at) >= OBSERVED_BINDING_TTL {
419 return None;
420 }
421 target
422 }
423 };
424 Some((*device, target))
425 })
426 .collect();
427 bindings.sort_by_key(|(device, _)| device.instance_number());
428 PropertyValue::List(
429 bindings
430 .into_iter()
431 .map(|(device, target)| {
432 let (network, mac) = match target {
433 DeviceBindingTarget::Local { peer_mac } => (0, peer_mac),
434 DeviceBindingTarget::Routed {
435 network, final_mac, ..
436 } => (*network, final_mac),
437 };
438 let mut encoded = BytesMut::new();
441 encode_app_object_id(&mut encoded, &device);
442 encode_app_unsigned(&mut encoded, u64::from(network));
443 encode_app_octet_string(&mut encoded, mac);
444 PropertyValue::ApplicationData(encoded.to_vec())
445 })
446 .collect(),
447 )
448 }
449
450 pub(super) fn who_is_scope(&self, device: &ObjectIdentifier) -> WhoIsScope {
454 match self.entries.get(device) {
455 Some(BindingEntry::Observed {
456 target: DeviceBindingTarget::Local { .. },
457 ..
458 }) => WhoIsScope::Local,
459 Some(BindingEntry::Observed {
460 target: DeviceBindingTarget::Routed { network, .. },
461 ..
462 }) => WhoIsScope::Remote(*network),
463 Some(BindingEntry::Configured(_)) | None => WhoIsScope::Global,
464 }
465 }
466
467 pub(super) fn forget_stale(&mut self, device: &ObjectIdentifier, now: Instant) {
471 if let Some(BindingEntry::Observed { observed_at, .. }) = self.entries.get(device) {
472 if now.saturating_duration_since(*observed_at) >= OBSERVED_BINDING_TTL {
473 self.entries.remove(device);
474 }
475 }
476 }
477
478 pub(super) fn resolve_at(
479 &self,
480 device: &ObjectIdentifier,
481 now: Instant,
482 is_group: impl Fn(&[u8]) -> bool,
483 ) -> DeviceResolution {
484 if device.object_type() != ObjectType::DEVICE {
485 return DeviceResolution::Invalid;
486 }
487 let (target, freshness) = match self.entries.get(device) {
488 Some(BindingEntry::Configured(target)) => (target, BindingFreshness::Configured),
489 Some(BindingEntry::Observed {
490 target,
491 observed_at,
492 }) => {
493 if now.saturating_duration_since(*observed_at) >= OBSERVED_BINDING_TTL {
494 return DeviceResolution::Stale;
495 }
496 (
497 target,
498 BindingFreshness::ObservedUntil(tokio::time::Instant::from_std(
499 *observed_at + OBSERVED_BINDING_TTL,
500 )),
501 )
502 }
503 None => return DeviceResolution::Unknown,
504 };
505 if !target_is_usable(target, is_group) {
506 return DeviceResolution::Invalid;
507 }
508 match target {
509 DeviceBindingTarget::Local { peer_mac } => DeviceResolution::ResolvedLocal {
510 peer_mac: peer_mac.clone(),
511 freshness,
512 },
513 DeviceBindingTarget::Routed {
514 network,
515 final_mac,
516 router_mac,
517 } => DeviceResolution::ResolvedRouted {
518 network: *network,
519 final_mac: final_mac.clone(),
520 router_mac: router_mac.clone(),
521 freshness,
522 },
523 }
524 }
525}
526
527fn group_hop(
530 target: &DeviceBindingTarget,
531 is_group: impl Fn(&[u8]) -> bool,
532) -> Option<(&'static str, &MacAddr)> {
533 match target {
534 DeviceBindingTarget::Local { peer_mac } => {
535 is_group(peer_mac).then_some(("its own MAC", peer_mac))
536 }
537 DeviceBindingTarget::Routed { router_mac, .. } => {
538 is_group(router_mac).then_some(("its router's MAC", router_mac))
539 }
540 }
541}
542
543fn colon_hex(mac: &[u8]) -> String {
545 mac.iter()
546 .map(|octet| format!("{octet:02x}"))
547 .collect::<Vec<_>>()
548 .join(":")
549}
550
551fn target_is_usable(target: &DeviceBindingTarget, is_group: impl Fn(&[u8]) -> bool) -> bool {
557 match target {
558 DeviceBindingTarget::Local { peer_mac } => !peer_mac.is_empty() && !is_group(peer_mac),
559 DeviceBindingTarget::Routed {
560 network,
561 final_mac,
562 router_mac,
563 } => {
564 (1..=0xFFFE).contains(network)
565 && !final_mac.is_empty()
566 && !router_mac.is_empty()
567 && !is_group(router_mac)
568 }
569 }
570}
571
572pub(super) async fn snapshot_command_origin(
576 service: ConfirmedServiceChoice,
577 immediate: &[u8],
578 routed: Option<&NpduAddress>,
579 local_network: Option<u16>,
580 bindings: &Arc<RwLock<DeviceBindingTable>>,
581 transactions: &Arc<NotificationTransactions>,
582) -> Option<bacnet_objects::command_source::CommandOrigin> {
583 if !matches!(
584 service,
585 ConfirmedServiceChoice::WRITE_PROPERTY
586 | ConfirmedServiceChoice::WRITE_PROPERTY_MULTIPLE
587 | ConfirmedServiceChoice::CREATE_OBJECT
588 ) {
589 return None;
590 }
591 Some(
592 bindings
593 .read()
594 .await
595 .command_origin(immediate, routed, local_network, |mac| {
596 transactions
597 .audit_routes
598 .get()
599 .is_some_and(|routes| routes.is_group(mac))
600 }),
601 )
602}
603
604#[cfg(test)]
605#[path = "group_binding_tests.rs"]
606mod group_binding_tests;