1use std::collections::HashMap;
2use std::sync::{Arc, Mutex as StdMutex, MutexGuard as StdMutexGuard};
3use std::time::{Duration, Instant as StdInstant};
4
5use bacnet_network::layer::ReceivedNetworkControl;
6use bacnet_npdu::{NetworkMessageType, RejectMessageReason, RejectMessageToNetwork};
7use tokio::sync::{Mutex as AsyncMutex, OwnedMutexGuard};
8use tokio::time::Instant as TokioInstant;
9
10use super::*;
11use crate::tsm::CompletionOutcome;
12
13const CONSERVATIVE_ROUTED_PATH_MAX_NPDU: u16 = 228;
15pub(super) const MAX_ROUTED_PATH_ENTRIES: usize = 256;
17
18fn routed_mac_fits(length: usize) -> bool {
24 (1..=bacnet_npdu::MAX_MAC_LEN).contains(&length)
25}
26
27#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
28struct RoutedPathKey {
29 immediate_router_mac: MacAddr,
30 dnet: u16,
31}
32
33impl RoutedPathKey {
34 fn new(immediate_router_mac: &[u8], dnet: u16) -> Self {
35 Self {
36 immediate_router_mac: MacAddr::from_slice(immediate_router_mac),
37 dnet,
38 }
39 }
40}
41
42#[derive(Clone, Copy, Debug)]
43enum ConfiguredLimitProvenance {
44 ExplicitApi,
45}
46
47struct ConfiguredPathLimit {
48 max_npdu: u16,
49 provenance: ConfiguredLimitProvenance,
50 observed_at: StdInstant,
51}
52
53struct LearnedPathLimit {
54 exclusive_max_npdu: u16,
55 observed_at: StdInstant,
56}
57
58struct ActiveRoutedSend {
59 path: RoutedPathKey,
60 tsm_mac: MacAddr,
61 invoke_id: u8,
62 owner: TransactionOwner,
63 forwarded_npci_len: u16,
64 outgoing_apdu_len: Option<u16>,
65 ingress_floor: u64,
66}
67
68struct RoutedPathEntry {
69 gate: Arc<AsyncMutex<()>>,
70 configured: Option<ConfiguredPathLimit>,
71 learned: Option<LearnedPathLimit>,
72 active: Option<ActiveRoutedSend>,
73 last_used: u64,
74 quarantine_started: Option<TokioInstant>,
75 generation_attempts: u32,
76 generation_terminal_observed: bool,
77}
78
79impl RoutedPathEntry {
80 fn new(last_used: u64) -> Self {
81 Self {
82 gate: Arc::new(AsyncMutex::new(())),
83 configured: None,
84 learned: None,
85 active: None,
86 last_used,
87 quarantine_started: None,
88 generation_attempts: 0,
89 generation_terminal_observed: false,
90 }
91 }
92}
93
94struct RoutedPathState {
95 entries: HashMap<RoutedPathKey, RoutedPathEntry>,
96 next_use: u64,
97 capacity: usize,
98}
99
100pub(super) struct RoutedPathLimits {
101 state: StdMutex<RoutedPathState>,
102 quarantine_horizon: Duration,
103}
104
105impl RoutedPathLimits {
106 pub(super) fn new(quarantine_horizon: Duration) -> Self {
107 Self::with_capacity(MAX_ROUTED_PATH_ENTRIES, quarantine_horizon)
108 }
109
110 fn with_capacity(capacity: usize, quarantine_horizon: Duration) -> Self {
111 assert!(capacity > 0, "routed path capacity must be nonzero");
112 Self {
113 state: StdMutex::new(RoutedPathState {
114 entries: HashMap::new(),
115 next_use: 0,
116 capacity,
117 }),
118 quarantine_horizon,
119 }
120 }
121
122 fn state(&self) -> StdMutexGuard<'_, RoutedPathState> {
123 self.state
124 .lock()
125 .unwrap_or_else(std::sync::PoisonError::into_inner)
126 }
127
128 #[cfg(test)]
130 pub(super) fn entry_count(&self) -> usize {
131 self.state().entries.len()
132 }
133
134 pub(super) async fn acquire(
137 self: &Arc<Self>,
138 router_mac: &[u8],
139 dnet: u16,
140 ) -> Result<RoutedPathLease, Error> {
141 if !routed_mac_fits(router_mac.len()) {
142 return Err(Error::Encoding(format!(
143 "immediate router MAC address must contain 1..={} octets",
144 bacnet_npdu::MAX_MAC_LEN
145 )));
146 }
147 let key = RoutedPathKey::new(router_mac, dnet);
148 let gate = self.reserve_gate(&key)?;
149 let gate = gate.lock_owned().await;
150 self.wait_for_quarantine(&key).await;
151 Ok(RoutedPathLease {
152 limits: Arc::clone(self),
153 key,
154 _gate: gate,
155 })
156 }
157
158 fn reserve_gate(&self, key: &RoutedPathKey) -> Result<Arc<AsyncMutex<()>>, Error> {
159 let mut state = self.state();
160 let last_used = bump_use(&mut state);
161 if let Some(entry) = state.entries.get_mut(key) {
162 entry.last_used = last_used;
163 return Ok(Arc::clone(&entry.gate));
164 }
165
166 if state.entries.len() >= state.capacity {
167 let reclaim = state
168 .entries
169 .iter()
170 .filter(|(_, entry)| self.is_safely_reclaimable(entry))
171 .min_by(|(key_a, entry_a), (key_b, entry_b)| {
172 entry_a
173 .last_used
174 .cmp(&entry_b.last_used)
175 .then_with(|| key_a.cmp(key_b))
176 })
177 .map(|(key, _)| key.clone());
178 if let Some(reclaim) = reclaim {
179 state.entries.remove(&reclaim);
180 }
181 }
182 if state.entries.len() >= state.capacity {
183 return Err(Error::RoutedPathCapacityExceeded {
184 capacity: state.capacity,
185 });
186 }
187
188 let entry = RoutedPathEntry::new(last_used);
189 let gate = Arc::clone(&entry.gate);
190 state.entries.insert(key.clone(), entry);
191 Ok(gate)
192 }
193
194 fn is_safely_reclaimable(&self, entry: &RoutedPathEntry) -> bool {
195 entry.configured.is_none()
196 && entry.learned.is_none()
197 && entry.active.is_none()
198 && Arc::strong_count(&entry.gate) == 1
199 && entry
200 .quarantine_started
201 .is_none_or(|started| started.elapsed() >= self.quarantine_horizon)
202 }
203
204 async fn wait_for_quarantine(&self, key: &RoutedPathKey) {
205 loop {
206 let remaining = {
207 let mut state = self.state();
208 let last_used = bump_use(&mut state);
209 let entry = state
210 .entries
211 .get_mut(key)
212 .expect("a held or waiting gate keeps its path entry resident");
213 entry.last_used = last_used;
214 match entry.quarantine_started {
215 Some(started) => {
216 let remaining = self.quarantine_horizon.saturating_sub(started.elapsed());
217 if remaining.is_zero() {
218 entry.quarantine_started = None;
219 entry.generation_attempts = 0;
220 entry.generation_terminal_observed = false;
221 None
222 } else {
223 Some(remaining)
224 }
225 }
226 None => {
227 entry.generation_attempts = 0;
228 entry.generation_terminal_observed = false;
229 None
230 }
231 }
232 };
233 let Some(remaining) = remaining else {
234 return;
235 };
236 tokio::time::sleep(remaining).await;
237 }
238 }
239
240 pub(super) fn install_active(
241 &self,
242 target: ConfirmedTarget<'_>,
243 tsm_mac: MacAddr,
244 invoke_id: u8,
245 owner: TransactionOwner,
246 forwarded_npci_len: u16,
247 ingress_floor: u64,
248 ) {
249 let ConfirmedTarget::Routed {
250 router_mac,
251 dest_network,
252 ..
253 } = target
254 else {
255 return;
256 };
257 let key = RoutedPathKey::new(router_mac, dest_network);
258 let mut state = self.state();
259 let entry = state
260 .entries
261 .get_mut(&key)
262 .expect("the routed path lease keeps its entry resident");
263 debug_assert!(
264 entry.active.is_none(),
265 "the per-path gate permits only one active routed request"
266 );
267 entry.active = Some(ActiveRoutedSend {
268 path: key,
269 tsm_mac,
270 invoke_id,
271 owner,
272 forwarded_npci_len,
273 outgoing_apdu_len: None,
274 ingress_floor,
275 });
276 }
277
278 pub(super) fn authorize_attempt(
279 &self,
280 target: ConfirmedTarget<'_>,
281 tsm_mac: &MacAddr,
282 invoke_id: u8,
283 owner: &TransactionOwner,
284 outgoing_apdu_len: usize,
285 ) -> bool {
286 let ConfirmedTarget::Routed {
287 router_mac,
288 dest_network,
289 ..
290 } = target
291 else {
292 return true;
293 };
294 let Ok(outgoing_apdu_len) = u16::try_from(outgoing_apdu_len) else {
295 return false;
296 };
297 let key = RoutedPathKey::new(router_mac, dest_network);
298 let mut state = self.state();
299 let Some(entry) = state.entries.get_mut(&key) else {
300 return false;
301 };
302 let Some(active) = entry.active.as_mut() else {
303 return false;
304 };
305 if active.tsm_mac != *tsm_mac
306 || active.invoke_id != invoke_id
307 || !active.owner.same_as(owner)
308 {
309 return false;
310 }
311 active.outgoing_apdu_len = Some(outgoing_apdu_len);
312 entry.generation_attempts = entry.generation_attempts.saturating_add(1);
313 true
314 }
315
316 pub(super) async fn handle_network_control(
317 &self,
318 tsm: &Arc<Mutex<Tsm>>,
319 control: ReceivedNetworkControl,
320 ) {
321 if control.npdu.message_type != Some(NetworkMessageType::REJECT_MESSAGE_TO_NETWORK) {
322 return;
323 }
324 let Ok(reject) = RejectMessageToNetwork::decode(&control.npdu.payload) else {
325 return;
326 };
327 if reject.reason != RejectMessageReason::MESSAGE_TOO_LONG {
328 return;
329 }
330
331 let Some(active) = self.claim_active(
332 &control.source_mac,
333 reject.network,
334 control.ingress_sequence,
335 ) else {
336 return;
337 };
338 let outcome = tsm.lock().await.complete_network_path_too_long_for_owner(
339 &active.tsm_mac,
340 active.invoke_id,
341 &active.owner,
342 reject.network,
343 );
344 if outcome == CompletionOutcome::Delivered {
345 self.commit_learned_limit(active);
346 }
347 }
348
349 fn claim_active(
350 &self,
351 router_mac: &[u8],
352 dnet: u16,
353 ingress_sequence: u64,
354 ) -> Option<ActiveRoutedSend> {
355 let key = RoutedPathKey::new(router_mac, dnet);
356 let mut state = self.state();
357 let entry = state.entries.get_mut(&key)?;
358 if entry.active.as_ref().is_none_or(|active| {
359 active.outgoing_apdu_len.is_none()
360 || active.path != key
361 || ingress_sequence <= active.ingress_floor
362 }) {
363 return None;
364 }
365 entry.active.take()
366 }
367
368 fn commit_learned_limit(&self, active: ActiveRoutedSend) {
369 let Some(attempted_npdu) = active
370 .outgoing_apdu_len
371 .and_then(|apdu| active.forwarded_npci_len.checked_add(apdu))
372 else {
373 return;
374 };
375 let mut state = self.state();
376 let entry = state
377 .entries
378 .get_mut(&active.path)
379 .expect("the reason-4 claimant still holds its routed path gate");
380 let exclusive_max_npdu = entry.learned.as_ref().map_or(attempted_npdu, |learned| {
381 learned.exclusive_max_npdu.min(attempted_npdu)
382 });
383 entry.learned = Some(LearnedPathLimit {
384 exclusive_max_npdu,
385 observed_at: StdInstant::now(),
386 });
387 }
388}
389
390pub(super) struct RoutedPathLease {
391 limits: Arc<RoutedPathLimits>,
392 key: RoutedPathKey,
393 _gate: OwnedMutexGuard<()>,
394}
395
396impl RoutedPathLease {
397 pub(super) fn max_apdu(&self, forwarded_npci_len: u16) -> u16 {
400 let state = self.limits.state();
401 let entry = state
402 .entries
403 .get(&self.key)
404 .expect("path entry remains present while its gate is held");
405 let configured_or_conservative =
406 entry
407 .configured
408 .as_ref()
409 .map_or(CONSERVATIVE_ROUTED_PATH_MAX_NPDU, |configured| {
410 tracing::trace!(
411 max_npdu = configured.max_npdu,
412 provenance = ?configured.provenance,
413 age = ?configured.observed_at.elapsed(),
414 "Using configured routed-path NPDU limit"
415 );
416 configured.max_npdu
417 });
418 let effective_npdu = entry
419 .learned
420 .as_ref()
421 .map_or(configured_or_conservative, |learned| {
422 tracing::trace!(
423 exclusive_max_npdu = learned.exclusive_max_npdu,
424 age = ?learned.observed_at.elapsed(),
425 "Applying learned routed-path upper bound"
426 );
427 configured_or_conservative.min(learned.exclusive_max_npdu.saturating_sub(1))
428 });
429 effective_npdu.saturating_sub(forwarded_npci_len)
430 }
431
432 pub(super) fn mark_terminal_observed(&self) {
437 let mut state = self.limits.state();
438 let entry = state
439 .entries
440 .get_mut(&self.key)
441 .expect("path entry remains present while its gate is held");
442 entry.generation_terminal_observed = entry.generation_attempts == 1;
443 }
444
445 fn configure(&self, max_npdu: u16) {
446 let mut state = self.limits.state();
447 let entry = state
448 .entries
449 .get_mut(&self.key)
450 .expect("path entry remains present while its gate is held");
451 entry.configured = Some(ConfiguredPathLimit {
452 max_npdu,
453 provenance: ConfiguredLimitProvenance::ExplicitApi,
454 observed_at: StdInstant::now(),
455 });
456 entry.learned = None;
457 }
458
459 fn clear(&self) {
460 let mut state = self.limits.state();
461 let entry = state
462 .entries
463 .get_mut(&self.key)
464 .expect("path entry remains present while its gate is held");
465 entry.configured = None;
466 entry.learned = None;
467 }
468}
469
470impl Drop for RoutedPathLease {
471 fn drop(&mut self) {
472 let mut state = self.limits.state();
473 if let Some(entry) = state.entries.get_mut(&self.key) {
474 entry.active = None;
475 if entry.generation_attempts > 0 && !entry.generation_terminal_observed {
476 entry.quarantine_started = Some(TokioInstant::now());
477 }
478 entry.generation_attempts = 0;
479 entry.generation_terminal_observed = false;
480 }
481 }
482}
483
484fn bump_use(state: &mut RoutedPathState) -> u64 {
485 state.next_use = state.next_use.saturating_add(1);
486 state.next_use
487}
488
489pub(super) fn routed_path_quarantine_horizon(config: &ClientConfig) -> Duration {
490 let attempts = u64::from(config.apdu_retries).saturating_add(1);
495 Duration::from_millis(config.apdu_timeout_ms.saturating_mul(attempts).max(1))
496}
497
498pub(super) fn forwarded_npci_len(
503 dadr_len: usize,
504 local_source_mac_len: usize,
505) -> Result<u16, Error> {
506 check_routed_dadr(dadr_len)?;
507 if !routed_mac_fits(local_source_mac_len) {
508 return Err(Error::Encoding(format!(
509 "local source MAC address for routed forwarding must contain 1..={} octets",
510 bacnet_npdu::MAX_MAC_LEN
511 )));
512 }
513 Ok(u16::try_from(9 + dadr_len + local_source_mac_len)
514 .expect("two addresses within bacnet_npdu::MAX_MAC_LEN fit a u16"))
515}
516
517impl<T: TransportPort + 'static> BACnetClient<T> {
518 pub async fn configure_routed_path_max_npdu(
526 &self,
527 router_mac: &[u8],
528 dnet: u16,
529 max_npdu: u16,
530 ) -> Result<(), Error> {
531 check_remote_dnet(dnet)?;
532 let lease = self.routed_path_limits.acquire(router_mac, dnet).await?;
533 lease.configure(max_npdu);
534 Ok(())
535 }
536
537 pub async fn clear_routed_path_limit(&self, router_mac: &[u8], dnet: u16) -> Result<(), Error> {
545 check_remote_dnet(dnet)?;
546 let lease = self.routed_path_limits.acquire(router_mac, dnet).await?;
547 lease.clear();
548 Ok(())
549 }
550}
551
552pub(super) fn check_remote_dnet(dnet: u16) -> Result<(), Error> {
558 if dnet == 0 || dnet == u16::MAX {
559 return Err(Error::Encoding("routed DNET must be in 1..=65534".into()));
560 }
561 Ok(())
562}
563
564fn check_routed_dadr(dadr_len: usize) -> Result<(), Error> {
568 if !routed_mac_fits(dadr_len) {
569 return Err(Error::Encoding(format!(
570 "routed destination MAC address must contain 1..={} octets",
571 bacnet_npdu::MAX_MAC_LEN
572 )));
573 }
574 Ok(())
575}
576
577pub(crate) fn check_routed_unicast(dnet: u16, dadr_len: usize) -> Result<(), Error> {
584 check_remote_dnet(dnet)?;
585 check_routed_dadr(dadr_len)
586}
587
588#[cfg(test)]
589#[path = "routed_path_limits_tests.rs"]
590mod tests;