Skip to main content

bacnet_server/server/
device_bindings.rs

1use super::*;
2
3/// Maximum number of configured and observed device bindings held by a server.
4pub(super) const MAX_DEVICE_BINDINGS: usize = 4096;
5
6/// Freshness window for passively observed I-Am bindings.
7pub(super) const OBSERVED_BINDING_TTL: Duration = Duration::from_secs(10 * 60);
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10enum DeviceBindingTarget {
11    Local {
12        peer_mac: MacAddr,
13    },
14    Routed {
15        network: u16,
16        final_mac: MacAddr,
17        router_mac: MacAddr,
18    },
19}
20
21/// One explicitly configured unicast route to a Device object.
22///
23/// Constructed values are transport-neutral. A builder performs the remaining
24/// concrete data-link broadcast check before the transport is started.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct DeviceBinding {
27    device: ObjectIdentifier,
28    target: DeviceBindingTarget,
29}
30
31impl DeviceBinding {
32    /// Create a binding to a peer on the server's local network.
33    pub fn local(device: ObjectIdentifier, peer_mac: impl AsRef<[u8]>) -> Result<Self, Error> {
34        validate_device_identifier(device)?;
35        let peer_mac = MacAddr::from_slice(peer_mac.as_ref());
36        if peer_mac.is_empty() {
37            return Err(binding_error("local peer MAC must be non-empty"));
38        }
39        Ok(Self {
40            device,
41            target: DeviceBindingTarget::Local { peer_mac },
42        })
43    }
44
45    /// Create a routed binding with a final network/address and local router.
46    pub fn routed(
47        device: ObjectIdentifier,
48        network: u16,
49        final_mac: impl AsRef<[u8]>,
50        router_mac: impl AsRef<[u8]>,
51    ) -> Result<Self, Error> {
52        validate_device_identifier(device)?;
53        if !(1..=0xFFFE).contains(&network) {
54            return Err(binding_error("routed network must be in 1..=65534"));
55        }
56        let final_mac = MacAddr::from_slice(final_mac.as_ref());
57        if final_mac.is_empty() {
58            return Err(binding_error("routed final MAC must be non-empty"));
59        }
60        let router_mac = MacAddr::from_slice(router_mac.as_ref());
61        if router_mac.is_empty() {
62            return Err(binding_error("routed router MAC must be non-empty"));
63        }
64        Ok(Self {
65            device,
66            target: DeviceBindingTarget::Routed {
67                network,
68                final_mac,
69                router_mac,
70            },
71        })
72    }
73}
74
75fn binding_error(message: &str) -> Error {
76    Error::Encoding(format!("invalid device binding: {message}"))
77}
78
79fn validate_device_identifier(device: ObjectIdentifier) -> Result<(), Error> {
80    if device.object_type() != ObjectType::DEVICE {
81        return Err(binding_error("identifier must name a Device object"));
82    }
83    Ok(())
84}
85
86pub(super) fn register_configured_binding(
87    bindings: &mut Vec<DeviceBinding>,
88    binding: DeviceBinding,
89) -> Result<(), Error> {
90    if bindings.len() >= MAX_DEVICE_BINDINGS {
91        return Err(binding_error("configured binding capacity exceeded"));
92    }
93    if bindings
94        .iter()
95        .any(|configured| configured.device == binding.device)
96    {
97        return Err(binding_error("duplicate configured Device identifier"));
98    }
99    bindings.push(binding);
100    Ok(())
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub(super) enum DeviceResolution {
105    ResolvedLocal {
106        peer_mac: MacAddr,
107        freshness: BindingFreshness,
108    },
109    ResolvedRouted {
110        network: u16,
111        final_mac: MacAddr,
112        router_mac: MacAddr,
113        freshness: BindingFreshness,
114    },
115    Unknown,
116    Stale,
117    Invalid,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub(super) enum BindingFreshness {
122    Configured,
123    ObservedUntil(tokio::time::Instant),
124}
125
126impl BindingFreshness {
127    pub(super) fn permits_attempt_at(self, now: tokio::time::Instant) -> bool {
128        match self {
129            Self::Configured => true,
130            Self::ObservedUntil(deadline) => now < deadline,
131        }
132    }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub(super) enum ObservationOutcome {
137    Inserted,
138    Refreshed,
139    ConfiguredPreserved,
140    RejectedInvalid,
141    RejectedCapacity,
142}
143
144#[derive(Debug, Clone)]
145enum BindingEntry {
146    Configured(DeviceBindingTarget),
147    Observed {
148        target: DeviceBindingTarget,
149        observed_at: Instant,
150    },
151}
152
153#[derive(Debug, Default)]
154pub(super) struct DeviceBindingTable {
155    entries: HashMap<ObjectIdentifier, BindingEntry>,
156}
157
158impl DeviceBindingTable {
159    pub(super) fn new() -> Self {
160        Self::default()
161    }
162
163    pub(super) fn from_configured(
164        bindings: Vec<DeviceBinding>,
165        is_broadcast: impl Fn(&[u8]) -> bool,
166    ) -> Result<Self, Error> {
167        let mut table = Self::new();
168        for binding in bindings {
169            table.insert_configured(binding, &is_broadcast)?;
170        }
171        Ok(table)
172    }
173
174    #[cfg(test)]
175    pub(super) fn len(&self) -> usize {
176        self.entries.len()
177    }
178
179    pub(super) fn insert_configured(
180        &mut self,
181        binding: DeviceBinding,
182        is_broadcast: impl Fn(&[u8]) -> bool,
183    ) -> Result<(), Error> {
184        validate_device_identifier(binding.device)?;
185        if !target_is_usable(&binding.target, &is_broadcast) {
186            return Err(binding_error(
187                "local peer or next-hop router is a broadcast address",
188            ));
189        }
190        if self.entries.contains_key(&binding.device) {
191            return Err(binding_error("duplicate configured Device identifier"));
192        }
193        if self.entries.len() >= MAX_DEVICE_BINDINGS {
194            return Err(binding_error("configured binding capacity exceeded"));
195        }
196        self.entries
197            .insert(binding.device, BindingEntry::Configured(binding.target));
198        Ok(())
199    }
200
201    pub(super) fn observe_i_am_at(
202        &mut self,
203        device: ObjectIdentifier,
204        source_mac: &[u8],
205        source_network: Option<&NpduAddress>,
206        now: Instant,
207        is_broadcast: impl Fn(&[u8]) -> bool,
208    ) -> ObservationOutcome {
209        if validate_device_identifier(device).is_err() {
210            return ObservationOutcome::RejectedInvalid;
211        }
212        let target = match source_network {
213            None => DeviceBindingTarget::Local {
214                peer_mac: MacAddr::from_slice(source_mac),
215            },
216            Some(source)
217                if (1..=0xFFFE).contains(&source.network) && !source.mac_address.is_empty() =>
218            {
219                DeviceBindingTarget::Routed {
220                    network: source.network,
221                    final_mac: source.mac_address.clone(),
222                    router_mac: MacAddr::from_slice(source_mac),
223                }
224            }
225            Some(_) => return ObservationOutcome::RejectedInvalid,
226        };
227        if !target_is_usable(&target, &is_broadcast) {
228            return ObservationOutcome::RejectedInvalid;
229        }
230
231        match self.entries.get_mut(&device) {
232            Some(BindingEntry::Configured(_)) => ObservationOutcome::ConfiguredPreserved,
233            Some(BindingEntry::Observed {
234                target: observed,
235                observed_at,
236            }) => {
237                *observed = target;
238                *observed_at = now;
239                ObservationOutcome::Refreshed
240            }
241            None => {
242                if self.entries.len() >= MAX_DEVICE_BINDINGS {
243                    self.entries.retain(|_, entry| match entry {
244                        BindingEntry::Configured(_) => true,
245                        BindingEntry::Observed { observed_at, .. } => {
246                            now.saturating_duration_since(*observed_at) < OBSERVED_BINDING_TTL
247                        }
248                    });
249                }
250                if self.entries.len() >= MAX_DEVICE_BINDINGS {
251                    return ObservationOutcome::RejectedCapacity;
252                }
253                self.entries.insert(
254                    device,
255                    BindingEntry::Observed {
256                        target,
257                        observed_at: now,
258                    },
259                );
260                ObservationOutcome::Inserted
261            }
262        }
263    }
264
265    pub(super) fn resolve_at(
266        &self,
267        device: &ObjectIdentifier,
268        now: Instant,
269        is_broadcast: impl Fn(&[u8]) -> bool,
270    ) -> DeviceResolution {
271        if device.object_type() != ObjectType::DEVICE {
272            return DeviceResolution::Invalid;
273        }
274        let (target, freshness) = match self.entries.get(device) {
275            Some(BindingEntry::Configured(target)) => (target, BindingFreshness::Configured),
276            Some(BindingEntry::Observed {
277                target,
278                observed_at,
279            }) => {
280                if now.saturating_duration_since(*observed_at) >= OBSERVED_BINDING_TTL {
281                    return DeviceResolution::Stale;
282                }
283                (
284                    target,
285                    BindingFreshness::ObservedUntil(tokio::time::Instant::from_std(
286                        *observed_at + OBSERVED_BINDING_TTL,
287                    )),
288                )
289            }
290            None => return DeviceResolution::Unknown,
291        };
292        if !target_is_usable(target, is_broadcast) {
293            return DeviceResolution::Invalid;
294        }
295        match target {
296            DeviceBindingTarget::Local { peer_mac } => DeviceResolution::ResolvedLocal {
297                peer_mac: peer_mac.clone(),
298                freshness,
299            },
300            DeviceBindingTarget::Routed {
301                network,
302                final_mac,
303                router_mac,
304            } => DeviceResolution::ResolvedRouted {
305                network: *network,
306                final_mac: final_mac.clone(),
307                router_mac: router_mac.clone(),
308                freshness,
309            },
310        }
311    }
312}
313
314fn target_is_usable(target: &DeviceBindingTarget, is_broadcast: impl Fn(&[u8]) -> bool) -> bool {
315    match target {
316        DeviceBindingTarget::Local { peer_mac } => !peer_mac.is_empty() && !is_broadcast(peer_mac),
317        DeviceBindingTarget::Routed {
318            network,
319            final_mac,
320            router_mac,
321        } => {
322            (1..=0xFFFE).contains(network)
323                && !final_mac.is_empty()
324                && !router_mac.is_empty()
325                && !is_broadcast(router_mac)
326        }
327    }
328}