1use std::panic::AssertUnwindSafe;
17use std::sync::atomic::{AtomicU64, Ordering};
18use std::sync::Arc;
19
20use bacnet_transport::port::TransportProvenance;
21
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
24pub enum ControlPolicy {
25 #[default]
27 Permissive,
28 Hardened,
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum ControlTrust {
38 Unverified,
40 VerifiedChannel,
42 VerifiedRelay,
44}
45
46impl ControlTrust {
47 pub fn from_provenance(p: TransportProvenance) -> Self {
50 if p.is_direct_peer() || p.is_hub_channel() {
51 Self::VerifiedChannel
52 } else if p.is_relayed_origin() {
53 Self::VerifiedRelay
54 } else {
55 Self::Unverified
56 }
57 }
58}
59
60#[derive(Clone, PartialEq, Eq)]
62pub struct ControlAuthContext {
63 pub port_idx: usize,
65 pub port_network: u16,
67 pub source_mac: bacnet_types::MacAddr,
69 pub provenance: TransportProvenance,
71 pub trust: ControlTrust,
74 pub message_type: u8,
76 pub vendor_id: Option<u16>,
78 pub source_net: Option<u16>,
80 pub dest_net: Option<u16>,
82 pub target_nets: Vec<u16>,
84 pub targets_omitted: bool,
86}
87
88impl std::fmt::Debug for ControlAuthContext {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 f.debug_struct("ControlAuthContext")
91 .field("port_idx", &self.port_idx)
92 .field("port_network", &self.port_network)
93 .field("source_mac_len", &self.source_mac.len())
94 .field("provenance", &self.provenance)
95 .field("trust", &self.trust)
96 .field("message_type", &self.message_type)
97 .field("vendor_id", &self.vendor_id)
98 .field("source_net", &self.source_net)
99 .field("dest_net", &self.dest_net)
100 .field("target_nets", &self.target_nets)
101 .field("targets_omitted", &self.targets_omitted)
102 .finish_non_exhaustive()
103 }
104}
105
106pub type ControlAuthorizer = Arc<dyn Fn(&ControlAuthContext) -> bool + Send + Sync>;
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
116pub enum ControlClass {
117 IAm,
119 InitAck,
121 InitMgmt,
123 Busy,
125 Available,
127 Reject,
129 ICouldBe,
131}
132
133impl ControlClass {
134 fn index(self) -> usize {
135 match self {
136 Self::IAm => 0,
137 Self::InitAck => 1,
138 Self::InitMgmt => 2,
139 Self::Busy => 3,
140 Self::Available => 4,
141 Self::Reject => 5,
142 Self::ICouldBe => 6,
143 }
144 }
145}
146
147#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
149pub struct ControlServiceCounters {
150 pub allow_total: u64,
152 pub deny_total: u64,
154 pub policy_deny_total: u64,
156}
157
158#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
160pub struct ControlDecisionCounters {
161 pub i_am: ControlServiceCounters,
163 pub init_ack: ControlServiceCounters,
165 pub init_mgmt: ControlServiceCounters,
167 pub busy: ControlServiceCounters,
169 pub available: ControlServiceCounters,
171 pub reject: ControlServiceCounters,
173 pub i_could_be: ControlServiceCounters,
175}
176
177#[derive(Clone, Copy)]
178enum Decision {
179 Allow,
180 Deny,
181 PolicyDeny,
182}
183
184#[derive(Default)]
186pub struct ControlDecisions([[AtomicU64; 3]; 7]);
187
188impl std::fmt::Debug for ControlDecisions {
189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190 f.debug_struct("ControlDecisions")
191 .field("snapshot", &self.snapshot())
192 .finish_non_exhaustive()
193 }
194}
195
196impl ControlDecisions {
197 pub fn snapshot(&self) -> ControlDecisionCounters {
199 let rows = self.0.each_ref().map(|r| {
200 let [a, d, p] = r.each_ref().map(|n| n.load(Ordering::Relaxed));
201 ControlServiceCounters {
202 allow_total: a,
203 deny_total: d,
204 policy_deny_total: p,
205 }
206 });
207 let [i_am, init_ack, init_mgmt, busy, available, reject, i_could_be] = rows;
208 ControlDecisionCounters {
209 i_am,
210 init_ack,
211 init_mgmt,
212 busy,
213 available,
214 reject,
215 i_could_be,
216 }
217 }
218
219 fn record(&self, class: ControlClass, decision: Decision) {
220 let inc = |col: usize| {
221 #[allow(deprecated, reason = "try_update needs Rust 1.95; the MSRV is 1.93")]
222 let _ = self.0[class.index()][col].fetch_update(
223 Ordering::Relaxed,
224 Ordering::Relaxed,
225 |n| Some(n.saturating_add(1)),
226 );
227 };
228 match decision {
229 Decision::Allow => inc(0),
230 Decision::Deny => inc(1),
231 Decision::PolicyDeny => {
232 inc(1);
233 inc(2);
234 }
235 }
236 }
237}
238
239pub struct ControlGate {
241 policy: ControlPolicy,
242 authorizer: Option<ControlAuthorizer>,
243 decisions: ControlDecisions,
244}
245
246impl std::fmt::Debug for ControlGate {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 f.debug_struct("ControlGate")
249 .field("policy", &self.policy)
250 .field(
251 "authorizer",
252 &self.authorizer.as_ref().map(|_| "<callback>"),
253 )
254 .field("decisions", &self.decisions)
255 .finish_non_exhaustive()
256 }
257}
258
259impl Default for ControlGate {
260 fn default() -> Self {
261 Self::permissive()
262 }
263}
264
265impl ControlGate {
266 pub fn permissive() -> Self {
268 Self {
269 policy: ControlPolicy::Permissive,
270 authorizer: None,
271 decisions: ControlDecisions::default(),
272 }
273 }
274
275 pub fn hardened() -> Self {
277 Self {
278 policy: ControlPolicy::Hardened,
279 authorizer: None,
280 decisions: ControlDecisions::default(),
281 }
282 }
283
284 pub fn new(policy: ControlPolicy, authorizer: Option<ControlAuthorizer>) -> Self {
287 Self {
288 policy,
289 authorizer,
290 decisions: ControlDecisions::default(),
291 }
292 }
293
294 pub fn policy(&self) -> ControlPolicy {
296 self.policy
297 }
298
299 pub fn snapshot(&self) -> ControlDecisionCounters {
301 self.decisions.snapshot()
302 }
303
304 fn decide(&self, ctx: &ControlAuthContext, class: ControlClass) -> bool {
305 match (&self.policy, &self.authorizer) {
306 (ControlPolicy::Permissive, None) => {
307 self.decisions.record(class, Decision::Allow);
308 true
309 }
310 (ControlPolicy::Hardened, None) => {
311 self.decisions.record(class, Decision::PolicyDeny);
312 false
313 }
314 (_, Some(auth)) => {
315 let allowed =
316 std::panic::catch_unwind(AssertUnwindSafe(|| auth(ctx))).unwrap_or(false);
317 self.decisions.record(
318 class,
319 if allowed {
320 Decision::Allow
321 } else {
322 Decision::Deny
323 },
324 );
325 allowed
326 }
327 }
328 }
329
330 pub(super) fn authorize(
334 &self,
335 ingress: &super::IngressContext,
336 class: ControlClass,
337 targets: &[u16],
338 targets_omitted: bool,
339 ) -> bool {
340 let ctx = ControlAuthContext {
341 port_idx: ingress.port_idx,
342 port_network: ingress.port_network,
343 source_mac: ingress.source_mac.clone(),
344 provenance: ingress.provenance,
345 trust: ControlTrust::from_provenance(ingress.provenance),
346 message_type: ingress.npdu.message_type.unwrap_or(0xFF),
347 vendor_id: ingress.npdu.vendor_id,
348 source_net: ingress.npdu.source.as_ref().map(|s| s.network),
349 dest_net: ingress.npdu.destination.as_ref().map(|d| d.network),
350 target_nets: targets.to_vec(),
351 targets_omitted,
352 };
353 self.decide(&ctx, class)
354 }
355}