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