Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/bacnet-objects/src/device/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pub const EXECUTED_SERVICES: &[ServiceSupported] = &[
ServiceSupported::GET_EVENT_INFORMATION,
ServiceSupported::SUBSCRIBE_COV_PROPERTY_MULTIPLE,
ServiceSupported::CONFIRMED_AUDIT_NOTIFICATION,
ServiceSupported::UNCONFIRMED_AUDIT_NOTIFICATION,
ServiceSupported::AUDIT_LOG_QUERY,
];

Expand Down
1 change: 1 addition & 0 deletions crates/bacnet-objects/src/device/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ fn read_protocol_services_supported() {
assert!(ss.contains(ServiceSupported::WHO_IS));
assert!(ss.contains(ServiceSupported::READ_RANGE));
assert!(ss.contains(ServiceSupported::SUBSCRIBE_COV_PROPERTY_MULTIPLE));
assert!(ss.contains(ServiceSupported::UNCONFIRMED_AUDIT_NOTIFICATION));
assert!(!ss.contains(ServiceSupported::WRITE_GROUP));
// …and initiate-only services are not declared as executed.
assert!(!ss.contains(ServiceSupported::I_AM));
Expand Down
29 changes: 27 additions & 2 deletions crates/bacnet-server/src/audit_notification.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Receiver policy and bounded duplicate detection for ConfirmedAuditNotification.
//! Receiver policy for confirmed and unconfirmed AuditNotification services.

use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
Expand All @@ -10,7 +10,7 @@ use bacnet_types::primitives::ObjectIdentifier;
use bacnet_types::MacAddr;
use bytes::Bytes;

/// Local maximum accepted ConfirmedAuditNotification service payload.
/// Local maximum accepted AuditNotification service payload.
pub const MAX_AUDIT_NOTIFICATION_BYTES: usize = 64 * 1024;
/// Local maximum number of notifications in one accepted request.
pub const MAX_AUDIT_NOTIFICATIONS: usize = 256;
Expand Down Expand Up @@ -42,6 +42,31 @@ pub struct AuditNotificationAuthorizationContext {
pub type AuditNotificationAuthorizer =
Arc<dyn Fn(&AuditNotificationAuthorizationContext) -> bool + Send + Sync>;

/// Transport provenance and decoded content supplied to the unconfirmed Audit
/// authorizer.
///
/// The source/target identities inside `request` are peer-reported audit data,
/// not authenticated transport provenance. Policy should use `source_network`
/// for a usable routed origin and `source_mac` for the immediate data-link peer.
#[derive(Debug, Clone, PartialEq)]
pub struct UnconfirmedAuditNotificationAuthorizationContext {
/// Immediate data-link peer (normally a router for routed traffic).
pub source_mac: MacAddr,
/// Originating NPDU source when one was present.
pub source_network: Option<NpduAddress>,
/// Explicitly configured local Audit Log sink.
pub audit_log_sink: ObjectIdentifier,
/// Decoded peer-reported notification list, preserved verbatim.
pub request: AuditNotificationRequest,
}

/// Fast, nonblocking authorization callback for UnconfirmedAuditNotification.
///
/// Absence, `false`, or a panic silently denies the request before storage
/// mutation.
pub type UnconfirmedAuditNotificationAuthorizer =
Arc<dyn Fn(&UnconfirmedAuditNotificationAuthorizationContext) -> bool + Send + Sync>;

#[derive(Clone, Debug, PartialEq, Eq)]
enum CanonicalPeer {
Direct(MacAddr),
Expand Down
14 changes: 13 additions & 1 deletion crates/bacnet-server/src/handlers/audit_notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
/// Persistence is synchronous under the database writer in this bounded
/// receiver foundation. That limits availability to the configured backend's
/// commit latency, but keeps the durable commit and memory apply atomic.
pub fn handle_confirmed_audit_notification(
pub fn handle_audit_notification(
db: &mut ObjectDatabase,
sink: ObjectIdentifier,
request: &AuditNotificationRequest,
Expand All @@ -36,6 +36,18 @@ pub fn handle_confirmed_audit_notification(
storage.store_notifications(&request.notifications, apdu_timeout_ms)
}

/// Store one decoded and authorized confirmed notification batch.
///
/// Retained as a compatibility alias for the original confirmed-only receiver
/// API; both inbound AuditNotification forms use the same atomic storage owner.
pub fn handle_confirmed_audit_notification(
db: &mut ObjectDatabase,
sink: ObjectIdentifier,
request: &AuditNotificationRequest,
) -> Result<(), Error> {
handle_audit_notification(db, sink, request)
}

fn configured_apdu_timeout(db: &ObjectDatabase) -> Result<u32, Error> {
let devices: Vec<_> = db
.list_objects()
Expand Down
5 changes: 3 additions & 2 deletions crates/bacnet-server/src/handlers/tests/read_rpm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ fn read_property_serves_derived_services_supported() {
// End-to-end pin for #192: the wire-level BitString a client receives for
// Protocol_Services_Supported, through the real ReadProperty handler path.
// Expected bytes derive from device::EXECUTED_SERVICES bits
// {0,3-12,14-17,19,20,31-39,41,42,44} packed MSB-first over the full
// {0,3-12,14-17,19,20,31-39,41,44-46} packed MSB-first over the full
// production range (49 defined bits, 7 octets, 7 unused).
let db = make_db_with_device_and_ai();
let oid = ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap();
Expand All @@ -605,7 +605,7 @@ fn read_property_serves_derived_services_supported() {
val,
bacnet_types::primitives::PropertyValue::BitString {
unused_bits: 7,
data: vec![0x9F, 0xFB, 0xD8, 0x01, 0xFF, 0x4C, 0x00],
data: vec![0x9F, 0xFB, 0xD8, 0x01, 0xFF, 0x4E, 0x00],
}
);

Expand All @@ -616,6 +616,7 @@ fn read_property_serves_derived_services_supported() {
let ss = bacnet_types::bitstring::ServicesSupported::from_bacnet(&data);
assert!(ss.contains(bacnet_types::enums::ServiceSupported::WHO_IS));
assert!(ss.contains(bacnet_types::enums::ServiceSupported::CONFIRMED_AUDIT_NOTIFICATION));
assert!(ss.contains(bacnet_types::enums::ServiceSupported::UNCONFIRMED_AUDIT_NOTIFICATION));
assert!(ss.contains(bacnet_types::enums::ServiceSupported::AUDIT_LOG_QUERY));
assert!(!ss.contains(bacnet_types::enums::ServiceSupported::I_AM));
}
45 changes: 30 additions & 15 deletions crates/bacnet-server/src/server/audit_notification_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ use bacnet_types::primitives::{BACnetTimeStamp, Date, ObjectIdentifier, Property
use super::*;

#[derive(Default)]
struct MemoryPersistence {
snapshot: StdMutex<Option<AuditLogSnapshot>>,
fail: AtomicBool,
pub(super) struct MemoryPersistence {
pub(super) snapshot: StdMutex<Option<AuditLogSnapshot>>,
pub(super) fail: AtomicBool,
}

impl AuditLogPersistence for MemoryPersistence {
Expand Down Expand Up @@ -58,11 +58,11 @@ impl ClockReader for FixedClock {
}
}

fn oid(object_type: ObjectType, instance: u32) -> ObjectIdentifier {
pub(super) fn oid(object_type: ObjectType, instance: u32) -> ObjectIdentifier {
ObjectIdentifier::new(object_type, instance).unwrap()
}

fn notification(operation: AuditOperation) -> BACnetAuditNotification {
pub(super) fn notification(operation: AuditOperation) -> BACnetAuditNotification {
BACnetAuditNotification {
source_timestamp: Some(BACnetTimeStamp::Time(Time {
hour: 12,
Expand Down Expand Up @@ -91,22 +91,22 @@ fn notification(operation: AuditOperation) -> BACnetAuditNotification {
}
}

fn request_bytes(notifications: Vec<BACnetAuditNotification>) -> Bytes {
pub(super) fn request_bytes(notifications: Vec<BACnetAuditNotification>) -> Bytes {
let mut bytes = BytesMut::new();
AuditNotificationRequest { notifications }
.try_encode(&mut bytes)
.unwrap();
bytes.freeze()
}

fn database(
pub(super) fn database(
persistence: Arc<MemoryPersistence>,
sink_instance: u32,
) -> Arc<RwLock<ObjectDatabase>> {
database_with_device(persistence, sink_instance, Some(DeviceConfig::default()))
}

fn database_with_device(
pub(super) fn database_with_device(
persistence: Arc<MemoryPersistence>,
sink_instance: u32,
device_config: Option<DeviceConfig>,
Expand Down Expand Up @@ -182,7 +182,7 @@ async fn dispatch(
.map(|bytes| decode_apdu(decode_npdu(bytes).unwrap().payload).unwrap())
}

async fn count(db: &Arc<RwLock<ObjectDatabase>>, sink: ObjectIdentifier) -> (u64, u64) {
pub(super) async fn count(db: &Arc<RwLock<ObjectDatabase>>, sink: ObjectIdentifier) -> (u64, u64) {
let db = db.read().await;
let object = db.get(&sink).unwrap();
let PropertyValue::Unsigned(records) = object
Expand Down Expand Up @@ -497,14 +497,14 @@ async fn missing_or_invalid_device_apdu_timeout_is_operational_problem_without_m
}

#[test]
fn executed_service_truth_is_confirmed_only() {
fn executed_service_truth_includes_confirmed_and_unconfirmed_receipt() {
assert!(EXECUTED_CONFIRMED.contains(&ConfirmedServiceChoice::CONFIRMED_AUDIT_NOTIFICATION));
assert!(
!EXECUTED_UNCONFIRMED.contains(&UnconfirmedServiceChoice::UNCONFIRMED_AUDIT_NOTIFICATION)
EXECUTED_UNCONFIRMED.contains(&UnconfirmedServiceChoice::UNCONFIRMED_AUDIT_NOTIFICATION)
);
assert!(bacnet_objects::device::EXECUTED_SERVICES
.contains(&ServiceSupported::CONFIRMED_AUDIT_NOTIFICATION));
assert!(!bacnet_objects::device::EXECUTED_SERVICES
assert!(bacnet_objects::device::EXECUTED_SERVICES
.contains(&ServiceSupported::UNCONFIRMED_AUDIT_NOTIFICATION));
}

Expand All @@ -513,15 +513,25 @@ fn generic_and_bip_builders_store_the_same_explicit_receiver_policy() {
let sink = oid(ObjectType::AUDIT_LOG, 7);
let generic = BACnetServer::<BipTransport>::generic_builder()
.audit_notification_sink(sink)
.audit_notification_authorizer(|_| true);
.audit_notification_authorizer(|_| true)
.unconfirmed_audit_notification_authorizer(|_| true);
assert_eq!(generic.config.audit_notification_sink, Some(sink));
assert!(generic.config.audit_notification_authorizer.is_some());
assert!(generic
.config
.unconfirmed_audit_notification_authorizer
.is_some());

let bip = BACnetServer::<BipTransport>::bip_builder()
.audit_notification_sink(sink)
.audit_notification_authorizer(|_| true);
.audit_notification_authorizer(|_| true)
.unconfirmed_audit_notification_authorizer(|_| true);
assert_eq!(bip.config.audit_notification_sink, Some(sink));
assert!(bip.config.audit_notification_authorizer.is_some());
assert!(bip
.config
.unconfirmed_audit_notification_authorizer
.is_some());
}

#[cfg(feature = "sc-tls")]
Expand All @@ -532,7 +542,12 @@ fn sc_builder_exposes_the_same_explicit_receiver_policy() {
bacnet_transport::sc::ScTransport<bacnet_transport::sc_tls::TlsWebSocket>,
>::sc_builder()
.audit_notification_sink(sink)
.audit_notification_authorizer(|_| true);
.audit_notification_authorizer(|_| true)
.unconfirmed_audit_notification_authorizer(|_| true);
assert_eq!(sc.config.audit_notification_sink, Some(sink));
assert!(sc.config.audit_notification_authorizer.is_some());
assert!(sc
.config
.unconfirmed_audit_notification_authorizer
.is_some());
}
33 changes: 33 additions & 0 deletions crates/bacnet-server/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ use bacnet_types::MacAddr;

use crate::audit_notification::{
AuditNotificationAuthorizationContext, AuditNotificationAuthorizer, DuplicateAdmission,
UnconfirmedAuditNotificationAuthorizationContext, UnconfirmedAuditNotificationAuthorizer,
MAX_AUDIT_NOTIFICATIONS, MAX_AUDIT_NOTIFICATION_BYTES,
};
use crate::cov::{CovNotificationKind, CovSubscription, CovSubscriptionTable};
Expand Down Expand Up @@ -306,6 +307,10 @@ pub struct ServerConfig {
///
/// Absence, `false`, or a panic denies the request before mutation.
pub audit_notification_authorizer: Option<AuditNotificationAuthorizer>,
/// Optional fast, nonblocking UnconfirmedAuditNotification authorizer.
///
/// Absence, `false`, or a panic silently denies the request before mutation.
pub unconfirmed_audit_notification_authorizer: Option<UnconfirmedAuditNotificationAuthorizer>,
/// Optional password required for DeviceCommunicationControl.
pub dcc_password: Option<String>,
/// Optional password required for ReinitializeDevice.
Expand Down Expand Up @@ -385,6 +390,13 @@ impl std::fmt::Debug for ServerConfig {
.as_ref()
.map(|_| "<callback>"),
)
.field(
"unconfirmed_audit_notification_authorizer",
&self
.unconfirmed_audit_notification_authorizer
.as_ref()
.map(|_| "<callback>"),
)
.field("dcc_password", &self.dcc_password.as_ref().map(|_| "***"))
.field(
"reinit_password",
Expand Down Expand Up @@ -414,6 +426,7 @@ impl Default for ServerConfig {
life_safety_operation_authorizer: None,
audit_notification_sink: None,
audit_notification_authorizer: None,
unconfirmed_audit_notification_authorizer: None,
dcc_password: None,
reinit_password: None,
enable_fault_detection: false,
Expand Down Expand Up @@ -486,6 +499,15 @@ impl<T: TransportPort + 'static> ServerBuilder<T> {
self
}

/// Set the fail-closed UnconfirmedAuditNotification authorization policy.
pub fn unconfirmed_audit_notification_authorizer<F>(mut self, authorizer: F) -> Self
where
F: Fn(&UnconfirmedAuditNotificationAuthorizationContext) -> bool + Send + Sync + 'static,
{
self.config.unconfirmed_audit_notification_authorizer = Some(Arc::new(authorizer));
self
}

/// Enable periodic fault detection / reliability evaluation.
///
/// Reliability evaluation only; Event Enrollment evaluation is configured
Expand Down Expand Up @@ -615,6 +637,15 @@ impl BipServerBuilder {
self
}

/// Set the fail-closed UnconfirmedAuditNotification authorization policy.
pub fn unconfirmed_audit_notification_authorizer<F>(mut self, authorizer: F) -> Self
where
F: Fn(&UnconfirmedAuditNotificationAuthorizationContext) -> bool + Send + Sync + 'static,
{
self.config.unconfirmed_audit_notification_authorizer = Some(Arc::new(authorizer));
self
}

/// Enable periodic fault detection / reliability evaluation.
///
/// Reliability evaluation only; Event Enrollment evaluation is configured
Expand Down Expand Up @@ -926,6 +957,8 @@ mod sc_builder;
pub(crate) use requests::{EXECUTED_CONFIRMED, EXECUTED_UNCONFIRMED};
#[cfg(test)]
mod audit_notification_tests;
#[cfg(test)]
mod unconfirmed_audit_notification_tests;
#[cfg(feature = "sc-tls")]
pub use sc_builder::ScServerBuilder;
mod responses;
Expand Down
Loading
Loading