use std::panic::AssertUnwindSafe;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use bacnet_transport::port::TransportProvenance;
use tracing::debug;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ControlPolicy {
#[default]
Permissive,
Hardened,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ControlTrust {
Unverified,
VerifiedChannel,
VerifiedRelay,
}
impl ControlTrust {
pub fn from_provenance(p: &TransportProvenance) -> Self {
if p.is_direct_peer() || p.is_hub_channel() {
Self::VerifiedChannel
} else if p.is_relayed_origin() {
Self::VerifiedRelay
} else {
Self::Unverified
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct ControlAuthContext {
pub port_idx: usize,
pub port_network: u16,
pub source_mac: bacnet_types::MacAddr,
pub provenance: TransportProvenance,
pub trust: ControlTrust,
pub message_type: bacnet_npdu::NetworkMessageType,
pub vendor_id: Option<u16>,
pub source_net: Option<u16>,
pub dest_net: Option<u16>,
pub target_nets: Vec<u16>,
pub targets_omitted: bool,
}
impl ControlAuthContext {
pub fn sending_peer_principal(&self) -> Option<bacnet_transport::port::ScPrincipal> {
self.provenance.principal()
}
}
impl std::fmt::Debug for ControlAuthContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ControlAuthContext")
.field("port_idx", &self.port_idx)
.field("port_network", &self.port_network)
.field("source_mac_len", &self.source_mac.len())
.field("provenance", &self.provenance)
.field(
"sending_peer_principal",
&self.sending_peer_principal().is_some(),
)
.field("trust", &self.trust)
.field("message_type", &self.message_type)
.field("vendor_id", &self.vendor_id)
.field("source_net", &self.source_net)
.field("dest_net", &self.dest_net)
.field("target_nets", &self.target_nets)
.field("targets_omitted", &self.targets_omitted)
.finish_non_exhaustive()
}
}
pub type ControlAuthorizer = Arc<dyn Fn(&ControlAuthContext) -> bool + Send + Sync>;
pub use bacnet_router_core::router::ControlClass;
fn index(class: ControlClass) -> Option<usize> {
match class {
ControlClass::IAm => Some(0),
ControlClass::InitAck => Some(1),
ControlClass::InitMgmt => Some(2),
ControlClass::Busy => Some(3),
ControlClass::Available => Some(4),
ControlClass::Reject => Some(5),
ControlClass::ICouldBe => Some(6),
_ => None,
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ControlServiceCounters {
pub allow_total: u64,
pub deny_total: u64,
pub policy_deny_total: u64,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ControlDecisionCounters {
pub i_am: ControlServiceCounters,
pub init_ack: ControlServiceCounters,
pub init_mgmt: ControlServiceCounters,
pub busy: ControlServiceCounters,
pub available: ControlServiceCounters,
pub reject: ControlServiceCounters,
pub i_could_be: ControlServiceCounters,
}
#[derive(Clone, Copy)]
enum Decision {
Allow,
Deny,
PolicyDeny,
}
#[derive(Default)]
pub struct ControlDecisions([[AtomicU64; 3]; 7]);
impl std::fmt::Debug for ControlDecisions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ControlDecisions")
.field("snapshot", &self.snapshot())
.finish_non_exhaustive()
}
}
impl ControlDecisions {
pub fn snapshot(&self) -> ControlDecisionCounters {
let rows = self.0.each_ref().map(|r| {
let [a, d, p] = r.each_ref().map(|n| n.load(Ordering::Relaxed));
ControlServiceCounters {
allow_total: a,
deny_total: d,
policy_deny_total: p,
}
});
let [
i_am,
init_ack,
init_mgmt,
busy,
available,
reject,
i_could_be,
] = rows;
ControlDecisionCounters {
i_am,
init_ack,
init_mgmt,
busy,
available,
reject,
i_could_be,
}
}
fn record(&self, class: ControlClass, decision: Decision) {
let Some(row) = index(class) else {
debug!(
?class,
"No decision counters for this control class; not counted"
);
return;
};
let inc = |col: usize| {
let _ = self.0[row][col].try_update(Ordering::Relaxed, Ordering::Relaxed, |n| {
Some(n.saturating_add(1))
});
};
match decision {
Decision::Allow => inc(0),
Decision::Deny => inc(1),
Decision::PolicyDeny => {
inc(1);
inc(2);
}
}
}
}
pub struct ControlGate {
policy: ControlPolicy,
authorizer: Option<ControlAuthorizer>,
decisions: ControlDecisions,
}
impl std::fmt::Debug for ControlGate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ControlGate")
.field("policy", &self.policy)
.field(
"authorizer",
&self.authorizer.as_ref().map(|_| "<callback>"),
)
.field("decisions", &self.decisions)
.finish_non_exhaustive()
}
}
impl Default for ControlGate {
fn default() -> Self {
Self::permissive()
}
}
impl ControlGate {
pub fn permissive() -> Self {
Self {
policy: ControlPolicy::Permissive,
authorizer: None,
decisions: ControlDecisions::default(),
}
}
pub fn hardened() -> Self {
Self {
policy: ControlPolicy::Hardened,
authorizer: None,
decisions: ControlDecisions::default(),
}
}
pub fn new(policy: ControlPolicy, authorizer: Option<ControlAuthorizer>) -> Self {
Self {
policy,
authorizer,
decisions: ControlDecisions::default(),
}
}
pub fn policy(&self) -> ControlPolicy {
self.policy
}
pub fn snapshot(&self) -> ControlDecisionCounters {
self.decisions.snapshot()
}
pub(super) fn decide(&self, ctx: &ControlAuthContext, class: ControlClass) -> bool {
match (&self.policy, &self.authorizer) {
(ControlPolicy::Permissive, None) => {
self.decisions.record(class, Decision::Allow);
true
}
(ControlPolicy::Hardened, None) => {
self.decisions.record(class, Decision::PolicyDeny);
false
}
(_, Some(auth)) => {
let allowed =
std::panic::catch_unwind(AssertUnwindSafe(|| auth(ctx))).unwrap_or(false);
self.decisions.record(
class,
if allowed {
Decision::Allow
} else {
Decision::Deny
},
);
allowed
}
}
}
}