Skip to main content

bacnet_server/server/
device_bindings.rs

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
8/// Maximum number of configured and observed device bindings held by a server.
9pub(super) const MAX_DEVICE_BINDINGS: usize = 4096;
10
11/// Freshness window for passively observed I-Am bindings, measured on
12/// [`runtime_clock::now`] (#1556): the instants the table's callers pass come
13/// from there, and so does the tokio deadline a fresh binding resolves to.
14pub(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/// One explicitly configured unicast route to a Device object.
29///
30/// Constructed values are transport-neutral. A builder performs the remaining
31/// concrete data-link check before the transport is started: neither the
32/// peer nor the router may be a broadcast or other group address of the link
33/// ([`TransportPort::is_group_destination`], #1493).
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct DeviceBinding {
36    device: ObjectIdentifier,
37    target: DeviceBindingTarget,
38}
39
40impl DeviceBinding {
41    /// Create a binding to a peer on the server's local network.
42    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    /// Create a routed binding with a final network/address and local router.
55    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/// The server's device bindings, behind one `RwLock`.
163///
164/// Lock order: the database guard, when a caller holds one, comes first. A
165/// read of Device_Address_Binding takes this table's read guard under the
166/// database read guard, after the COV table's guard is gone, and nothing
167/// that holds this table's guard waits for the database or the COV table,
168/// or across a send or a wait.
169#[derive(Debug, Default)]
170pub(super) struct DeviceBindingTable {
171    entries: HashMap<ObjectIdentifier, BindingEntry>,
172    /// Targeted Who-Is requests out or held off, kept under the same guard
173    /// so an I-Am can wake the writes waiting on them (#1322).
174    pub(super) probes: BindingProbes,
175}
176
177impl DeviceBindingTable {
178    /// Snapshot original sender and correlation while holding only the binding
179    /// guard. `local_network` is this network's own number, which
180    /// [`Self::source_binding`] matches against.
181    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    /// Prefer a known, unambiguous Device identity in target Audit records.
201    /// This is address correlation, never authentication of a principal.
202    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    /// The Device whose binding names a request's source, when exactly one
216    /// does. A routed binding names a request relayed with its network and
217    /// final MAC as SNET and SADR. A local binding names a request from its
218    /// MAC with no SNET (Clause 6.2.2), and, once `local_network`, this
219    /// network's own number, is known, one relayed with that number and its
220    /// MAC as SNET and SADR: a router here passing a peer's request back onto
221    /// this network adds that pair (Clause 6.5.4), and network numbers are
222    /// unique, so both forms name the same node. A binding routed through
223    /// `local_network` is the local binding it is (#1404), as sends to it
224    /// take it ([`RecipientRoute::localize`], #1358).
225    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    /// Immutable configured routes, already validated against the concrete link.
280    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    /// The Device object's Device_Address_Binding at `now` (Clause 12.11.34,
408    /// #1369): one BACnetAddressBinding for each configured binding and each
409    /// I-Am observation younger than [`OBSERVED_BINDING_TTL`], devices found
410    /// by a targeted Who-Is included, in Device instance order. A binding on
411    /// this network reads network 0 and the peer's MAC; a routed one reads
412    /// its network and the device's MAC there, not the router's. A stale
413    /// observation drops out here, with no sweep, as it stops routing.
414    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                    // The device's identifier, then its BACnetAddress: the
446                    // network number and the MAC, each application tagged.
447                    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    /// Where a targeted Who-Is for `device` goes: the network its last I-Am
458    /// came from while that observation is held, or every network for a
459    /// device never heard from.
460    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    /// Drop `device`'s observation if it is stale at `now`: a Who-Is to
475    /// where it was last seen drew nothing, so its next one goes global. A
476    /// configured binding, or an observation an I-Am refreshed, stays.
477    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
534/// The MAC of `target` that is a group address of the link, the device's own
535/// or its router's, with which of the two it is (#1493).
536fn 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
550/// `mac` as colon-separated hex octets, the form the address parsers take.
551fn colon_hex(mac: &[u8]) -> String {
552    mac.iter()
553        .map(|octet| format!("{octet:02x}"))
554        .collect::<Vec<_>>()
555        .join(":")
556}
557
558/// Whether a binding names one device this link can reach. A binding never
559/// takes a group address, the link's broadcast or any other such as a
560/// multicast address (`is_group`, [`TransportPort::is_group_destination`]),
561/// for its peer or its next-hop router (#1493): one names no single device,
562/// and a confirmed request sent there would be a local broadcast.
563fn 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
579/// Snapshot only services that can submit a tracked command, before the
580/// database lock. `local_network` is this network's own number as read for
581/// the request.
582pub(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;