bacnet_server/server/
dcc_outcomes.rs1use super::*;
2use std::fmt;
3use std::sync::atomic::AtomicU64;
4#[cfg(test)]
5#[path = "dcc_trace_tests.rs"]
6pub(crate) mod trace_tests;
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub struct DccOutcomeCounters {
13 pub accepted_total: u64,
15 pub policy_denied_total: u64,
17 pub password_failure_total: u64,
19 pub deprecated_denied_total: u64,
21 pub malformed_total: u64,
23}
24
25#[derive(Clone, Copy)]
26pub(crate) enum DccOutcome {
27 Accepted,
28 PolicyDenied,
29 PasswordFailure,
30 DeprecatedDenied,
31 Malformed,
32}
33
34impl DccOutcome {
35 fn label(self) -> &'static str {
36 match self {
37 Self::Accepted => "accepted",
38 Self::PolicyDenied => "policy_denied",
39 Self::PasswordFailure => "password_failure",
40 Self::DeprecatedDenied => "deprecated_denied",
41 Self::Malformed => "malformed",
42 }
43 }
44}
45
46#[derive(Clone, Copy, Default)]
48pub(crate) struct DccMetadata {
49 pub mode: Option<u32>,
50 pub duration: Option<u16>,
51}
52
53#[derive(Default)]
54pub(super) struct DccOutcomes([AtomicU64; 5]);
55
56impl DccOutcomes {
57 pub(super) fn snapshot(&self) -> DccOutcomeCounters {
58 let [accepted_total, policy_denied_total, password_failure_total, deprecated_denied_total, malformed_total] =
59 self.0.each_ref().map(|v| v.load(Ordering::Relaxed));
60 DccOutcomeCounters {
61 accepted_total,
62 policy_denied_total,
63 password_failure_total,
64 deprecated_denied_total,
65 malformed_total,
66 }
67 }
68
69 pub(super) fn record(
70 &self,
71 outcome: DccOutcome,
72 metadata: DccMetadata,
73 invoke_id: u8,
74 source_mac: &[u8],
75 source: Option<&NpduAddress>,
76 ) {
77 let index = match outcome {
80 DccOutcome::Accepted => 0,
81 DccOutcome::PolicyDenied => 1,
82 DccOutcome::PasswordFailure => 2,
83 DccOutcome::DeprecatedDenied => 3,
84 DccOutcome::Malformed => 4,
85 };
86 #[allow(deprecated, reason = "try_update needs Rust 1.95; the MSRV is 1.93")]
87 let _ = self.0[index].fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| {
88 Some(n.saturating_add(1))
89 });
90 let routed = source.map(|s| s.mac_address.as_slice());
93 tracing::debug!(target: "bacnet_server::dcc_outcome",
94 outcome = outcome.label(), invoke_id, service = 17u8,
95 decoded_mode = metadata.mode, duration_minutes = metadata.duration,
96 source_kind = if source.is_some() { "claimed_routed" } else { "claimed_direct" },
97 claimed_source_mac = %BoundedHex(source_mac),
98 source_mac_truncated = source_mac.len() > 32,
99 claimed_snet = source.map(|s| s.network),
100 claimed_sadr = %BoundedHex(routed.unwrap_or_default()),
101 sadr_truncated = routed.is_some_and(|s| s.len() > 32),
102 );
103 }
104}
105
106struct BoundedHex<'a>(&'a [u8]);
107#[test]
108fn dcc_outcomes_saturation_without_subscriber() {
109 let counters = DccOutcomes::default();
110 for counter in &counters.0 {
111 counter.store(u64::MAX - 1, Ordering::Relaxed);
112 }
113 tracing::subscriber::with_default(tracing::subscriber::NoSubscriber::default(), || {
114 for _ in 0..3 {
115 for outcome in [
116 DccOutcome::Accepted,
117 DccOutcome::PolicyDenied,
118 DccOutcome::PasswordFailure,
119 DccOutcome::DeprecatedDenied,
120 DccOutcome::Malformed,
121 ] {
122 counters.record(outcome, DccMetadata::default(), 0, &[], None);
123 }
124 }
125 });
126 assert!(counters
127 .0
128 .iter()
129 .all(|counter| counter.load(Ordering::Relaxed) == u64::MAX));
130}
131impl fmt::Display for BoundedHex<'_> {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 for byte in self.0.iter().take(32) {
134 write!(f, "{byte:02x}")?;
135 }
136 Ok(())
137 }
138}
139
140impl<T: TransportPort + 'static> BACnetServer<T> {
141 pub fn dcc_outcome_counters(&self) -> DccOutcomeCounters {
145 self.dcc_outcomes.snapshot()
146 }
147}