Skip to main content

bacnet_server/server/
config.rs

1use super::*;
2use crate::mutation::{MutationAuthorizationContext, MutationAuthorizer, MutationPolicy};
3
4/// Server configuration.
5#[derive(Clone)]
6pub struct ServerConfig {
7    /// Explicit concrete Network Port for one owned NORMAL B/IP transport.
8    /// None leaves configured objects unbound and receiving-port wildcard unavailable.
9    pub registered_network_port: Option<ObjectIdentifier>,
10    /// Optional target Audit Reporter profile. Its recipient is provisioned on
11    /// the built-in Device and becomes writable while the runtime is installed.
12    pub audit_reporters: Option<AuditReportersConfig>,
13    /// Per-service GetAlarmSummary database scan and encoded response limits.
14    pub get_alarm_summary_budget: GetAlarmSummaryBudget,
15    /// Local complete-response limits for GetEnrollmentSummary.
16    pub get_enrollment_summary_budget: GetEnrollmentSummaryBudget,
17    /// Local AtomicReadFile raw-count and complete service-ACK limits.
18    pub atomic_read_file_budget: AtomicReadFileBudget,
19    /// Local AtomicWriteFile payload admission limits.
20    pub atomic_write_file_budget: AtomicWriteFileBudget,
21    /// ReadRange directional page item and logical service-byte limits.
22    pub read_range_budget: ReadRangeBudget,
23    /// GetEventInformation database admission and strict response-page limits.
24    pub get_event_information_budget: GetEventInformationBudget,
25    /// Per-service RPM work and encoded response limits (finite by default).
26    pub read_property_multiple_budget: ReadPropertyMultipleBudget,
27    /// Local interface to bind.
28    pub interface: Ipv4Addr,
29    /// UDP port (default 0xBAC0 = 47808).
30    pub port: u16,
31    /// Directed broadcast address.
32    pub broadcast_address: Ipv4Addr,
33    /// Raw local APDU receive ceiling, clamped to the transport's stable capacity.
34    /// The effective value must be at least 50 and equal the selected Device's
35    /// `Max_APDU_Length_Accepted`. I-Am retains this raw value; only originated
36    /// Confirmed-Request headers floor it to a supported encoding.
37    pub max_apdu_length: u32,
38    /// Segmentation support level.
39    ///
40    /// Enforced, not just advertised: the dispatch loop reassembles inbound
41    /// segmented requests only under `BOTH`/`RECEIVE` and transmits
42    /// segmented responses only under `BOTH`/`TRANSMIT` (Clauses 5.4.5.1 and
43    /// 5.4.5.3); anything else draws a SEGMENTATION_NOT_SUPPORTED Abort. The
44    /// default is `NONE`, so a default-configured server refuses segmented
45    /// traffic in both directions — set this to what the device should
46    /// actually honor.
47    pub segmentation_supported: Segmentation,
48    /// Vendor identifier.
49    pub vendor_id: u16,
50    /// APDU timeout in ms of confirmed COV and event notifications and of the
51    /// WriteProperty a Command action sends to another device (default
52    /// 3000 ms): how long each attempt waits for the reply once its send has
53    /// completed, before the next retry. The transport bounds the send itself.
54    /// It is also the base of the COV hold-off after a failed confirmed report,
55    /// one full retry cycle: this timeout times the attempts (#896).
56    pub cov_retry_timeout_ms: u64,
57    /// Opt-in inbound time-sync restrictions; default allows all, with no step cap.
58    pub time_sync_policy: TimeSyncPolicy,
59    /// Optional fast, nonblocking observer invoked after the clock changes.
60    /// A panic is caught (with unwind builds); the change stands and ingress
61    /// continues. This callback cannot authorize or roll back synchronization.
62    pub on_time_sync: Option<Arc<dyn Fn(TimeSyncData) + Send + Sync>>,
63    /// Local mutation authorization mode (default: permissive). SC mTLS channel/peer
64    /// authentication is not service authorization; addresses here are claimed,
65    /// never certificate principals. See [`MutationPolicy`]. Each decision also
66    /// carries the reassembled ingress [`TransportProvenance`](bacnet_transport::port::TransportProvenance)
67    /// snapshot and the derived channel/relay [`MutationTrust`](crate::mutation::MutationTrust)
68    /// scope (never leaf identity); unknown origin never satisfies a
69    /// baseline-only allow rule. Denials mutate nothing and write no audit log.
70    /// `DenyAll` also denies each Channel write of an inbound WriteGroup,
71    /// counted in `BACnetServer::mutation_decision_counters` (#1319).
72    pub mutation_policy: MutationPolicy,
73    /// Opt-in mutation authorizer; `None` allows only in permissive mode.
74    /// See [`MutationAuthorizer`]. It also decides each Channel write of an
75    /// inbound WriteGroup, with no invoke ID and the unconfirmed service in its
76    /// context, so it can allow some Channels and not others (#1319).
77    pub mutation_authorizer: Option<MutationAuthorizer>,
78    /// Optional LifeSafetyOperation authorization policy.
79    ///
80    /// Absence is fail-closed: requests receive SERVICES /
81    /// SERVICE_REQUEST_DENIED before object mutation.
82    pub life_safety_operation_authorizer: Option<LifeSafetyOperationAuthorizer>,
83    /// Exactly one explicitly configured Audit Log notification sink.
84    ///
85    /// Absence is fail-closed; the server never selects a sink by database
86    /// iteration order.
87    /// A built-in AuditLogObject configured with `set_member_of` also forwards
88    /// record-changing accepted batches once, after commit, to that parent using
89    /// a configured DeviceBinding. No backlog, retries, or durable send progress.
90    pub audit_notification_sink: Option<ObjectIdentifier>,
91    /// Optional fast, nonblocking ConfirmedAuditNotification authorizer.
92    ///
93    /// Absence, `false`, or a panic denies the request before mutation.
94    pub audit_notification_authorizer: Option<AuditNotificationAuthorizer>,
95    /// Optional fast, nonblocking UnconfirmedAuditNotification authorizer.
96    ///
97    /// Absence, `false`, or a panic silently denies the request before mutation.
98    pub unconfirmed_audit_notification_authorizer: Option<UnconfirmedAuditNotificationAuthorizer>,
99    /// Optional password required for DeviceCommunicationControl.
100    pub dcc_password: Option<String>,
101    /// Local DCC authorization. Supplying a password alone does not enable DCC.
102    pub dcc_policy: DccPolicy,
103    /// Optional exact claimed-source restriction, valid only with RequirePassword.
104    pub dcc_source_restriction: Option<DccSourceRestriction>,
105    /// Optional global DISABLE_INITIATION budget; does not enable DCC authorization.
106    pub dcc_disable_rate_limit: Option<DccDisableRateLimit>,
107    /// Optional password required for ReinitializeDevice.
108    pub reinit_password: Option<String>,
109    /// Enable periodic fault detection / reliability evaluation.
110    /// When true, the server invokes every object's opt-in, object-owned
111    /// reliability evaluation hook every 10 seconds. Stock objects currently
112    /// inherit the no-op default.
113    ///
114    /// This governs reliability evaluation only. Event Enrollment evaluation
115    /// is configured separately via [`enable_event_enrollment`](Self::enable_event_enrollment).
116    pub enable_fault_detection: bool,
117    /// Enable periodic Event Enrollment evaluation (default `true`).
118    ///
119    /// When true, the server re-reads the property each Event Enrollment object
120    /// names in its `Object_Property_Reference` and applies the configured event
121    /// algorithm. Startup: the task is spawned by [`start`](BACnetServer::start)
122    /// and its first pass runs immediately, then once per interval. Shutdown:
123    /// [`stop`](BACnetServer::stop) aborts it and awaits the abort.
124    ///
125    /// This switch governs the evaluation task; it is not the per-object
126    /// `Event_Detection_Enable` property of ASHRAE 135-2020 Clause 13.2.2.1.
127    /// Setting it false stops evaluation without performing the reset that
128    /// clause requires of a disabled detector (`Event_State` to NORMAL, with the
129    /// corresponding timestamp and acknowledgment state), so a device carrying
130    /// active enrollments will hold whatever state it last detected.
131    ///
132    /// Evaluation is a no-op on databases holding no Event Enrollment objects,
133    /// so the default is on.
134    ///
135    /// Successful enabled transitions commit `Event_State`,
136    /// `Acked_Transitions`, and `Event_Time_Stamps` atomically and are then
137    /// routed through the shared EventNotification sender. Event Enrollment
138    /// message text remains intentionally absent, and exact event-specific
139    /// notification values are deferred to the payload projection work.
140    pub enable_event_enrollment: bool,
141    /// Interval in seconds between Event Enrollment evaluation passes (default 10).
142    ///
143    /// This is a sampling cadence with no basis in ASHRAE 135-2020, which
144    /// prescribes no evaluation frequency and leaves acquisition of a monitored
145    /// value a local matter (Clause 12.12). It is not the `Time_Delay` of an
146    /// event algorithm, which is how long a condition must persist before a
147    /// transition is indicated (Clause 13.3) — a coarse interval delays
148    /// detection and can miss a condition that both appears and clears between
149    /// two passes.
150    ///
151    /// A value of `0` is clamped to one second. Ignored when
152    /// [`enable_event_enrollment`](Self::enable_event_enrollment) is false.
153    pub event_enrollment_interval_secs: u64,
154    /// Discovery rate-limiting and duplicate suppression policy.
155    pub discovery_policy: DiscoveryPolicy,
156    /// COV quota, rate accounting, and notification work budget policy.
157    pub cov_policy: CovPolicy,
158    /// Positive independent top-level request handler limits.
159    pub request_admission_policy: RequestAdmissionPolicy,
160}
161
162impl std::fmt::Debug for ServerConfig {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        f.debug_struct("ServerConfig")
165            .field("audit_reporters", &self.audit_reporters)
166            .field("get_alarm_summary_budget", &self.get_alarm_summary_budget)
167            .field(
168                "get_enrollment_summary_budget",
169                &self.get_enrollment_summary_budget,
170            )
171            .field("atomic_read_file_budget", &self.atomic_read_file_budget)
172            .field("atomic_write_file_budget", &self.atomic_write_file_budget)
173            .field("read_range_budget", &self.read_range_budget)
174            .field(
175                "get_event_information_budget",
176                &self.get_event_information_budget,
177            )
178            .field(
179                "read_property_multiple_budget",
180                &self.read_property_multiple_budget,
181            )
182            .field("interface", &self.interface)
183            .field("port", &self.port)
184            .field("broadcast_address", &self.broadcast_address)
185            .field("max_apdu_length", &self.max_apdu_length)
186            .field("segmentation_supported", &self.segmentation_supported)
187            .field("vendor_id", &self.vendor_id)
188            .field("cov_retry_timeout_ms", &self.cov_retry_timeout_ms)
189            .field("time_sync_policy", &self.time_sync_policy)
190            .field("mutation_policy", &self.mutation_policy)
191            .field(
192                "on_time_sync",
193                &self.on_time_sync.as_ref().map(|_| "<callback>"),
194            )
195            .field(
196                "mutation_authorizer",
197                &self.mutation_authorizer.as_ref().map(|_| "<callback>"),
198            )
199            .field(
200                "life_safety_operation_authorizer",
201                &self
202                    .life_safety_operation_authorizer
203                    .as_ref()
204                    .map(|_| "<callback>"),
205            )
206            .field("audit_notification_sink", &self.audit_notification_sink)
207            .field(
208                "audit_notification_authorizer",
209                &self
210                    .audit_notification_authorizer
211                    .as_ref()
212                    .map(|_| "<callback>"),
213            )
214            .field(
215                "unconfirmed_audit_notification_authorizer",
216                &self
217                    .unconfirmed_audit_notification_authorizer
218                    .as_ref()
219                    .map(|_| "<callback>"),
220            )
221            .field("dcc_password", &self.dcc_password.as_ref().map(|_| "***"))
222            .field("dcc_policy", &self.dcc_policy)
223            .field("dcc_source_restriction", &self.dcc_source_restriction)
224            .field("dcc_disable_rate_limit", &self.dcc_disable_rate_limit)
225            .field(
226                "reinit_password",
227                &self.reinit_password.as_ref().map(|_| "***"),
228            )
229            .field("enable_fault_detection", &self.enable_fault_detection)
230            .field("enable_event_enrollment", &self.enable_event_enrollment)
231            .field(
232                "event_enrollment_interval_secs",
233                &self.event_enrollment_interval_secs,
234            )
235            .field("discovery_policy", &self.discovery_policy)
236            .field("cov_policy", &self.cov_policy)
237            .field("request_admission_policy", &self.request_admission_policy)
238            .finish()
239    }
240}
241
242impl Default for ServerConfig {
243    fn default() -> Self {
244        Self {
245            registered_network_port: None,
246            audit_reporters: None,
247            interface: Ipv4Addr::UNSPECIFIED,
248            read_property_multiple_budget: ReadPropertyMultipleBudget::default(),
249            get_alarm_summary_budget: GetAlarmSummaryBudget::default(),
250            get_enrollment_summary_budget: GetEnrollmentSummaryBudget::default(),
251            atomic_read_file_budget: AtomicReadFileBudget::default(),
252            atomic_write_file_budget: AtomicWriteFileBudget::default(),
253            read_range_budget: ReadRangeBudget::default(),
254            get_event_information_budget: GetEventInformationBudget::default(),
255            port: 0xBAC0,
256            broadcast_address: Ipv4Addr::BROADCAST,
257            max_apdu_length: 1476,
258            segmentation_supported: Segmentation::NONE,
259            vendor_id: 0,
260            cov_retry_timeout_ms: 3000,
261            time_sync_policy: TimeSyncPolicy::default(),
262            on_time_sync: None,
263            mutation_policy: MutationPolicy::default(),
264            mutation_authorizer: None,
265            life_safety_operation_authorizer: None,
266            audit_notification_sink: None,
267            audit_notification_authorizer: None,
268            unconfirmed_audit_notification_authorizer: None,
269            dcc_password: None,
270            dcc_policy: DccPolicy::default(),
271            dcc_source_restriction: None,
272            dcc_disable_rate_limit: None,
273            reinit_password: None,
274            enable_fault_detection: false,
275            enable_event_enrollment: true,
276            event_enrollment_interval_secs: 10,
277            discovery_policy: DiscoveryPolicy::default(),
278            cov_policy: CovPolicy::default(),
279            request_admission_policy: RequestAdmissionPolicy::default(),
280        }
281    }
282}
283
284impl<T: TransportPort + 'static> ServerBuilder<T> {
285    /// Select local mutation authorization (default: [`MutationPolicy::Permissive`]).
286    /// SC mTLS channel/peer authentication is not service authorization; identities
287    /// here are claimed link/routed addresses, never certificate principals.
288    pub fn mutation_policy(mut self, policy: MutationPolicy) -> Self {
289        self.config.mutation_policy = policy;
290        self
291    }
292
293    /// Set opt-in mutation policy. See [`MutationAuthorizer`] for callback rules,
294    /// the Channel writes of an inbound WriteGroup among them (#1319).
295    pub fn mutation_authorizer<F>(mut self, authorizer: F) -> Self
296    where
297        F: Fn(&MutationAuthorizationContext) -> bool + Send + Sync + 'static,
298    {
299        self.config.mutation_authorizer = Some(Arc::new(authorizer));
300        self
301    }
302}
303
304impl BipServerBuilder {
305    /// Select local mutation authorization (default: [`MutationPolicy::Permissive`]).
306    /// SC mTLS channel/peer authentication is not service authorization; identities
307    /// here are claimed link/routed addresses, never certificate principals.
308    ///
309    /// ```
310    /// use bacnet_server::{mutation::MutationPolicy, server::BACnetServer};
311    /// let builder = BACnetServer::bip_builder().mutation_policy(MutationPolicy::DenyAll);
312    /// ```
313    pub fn mutation_policy(mut self, policy: MutationPolicy) -> Self {
314        self.config.mutation_policy = policy;
315        self
316    }
317
318    /// Set opt-in mutation policy. See [`MutationAuthorizer`] for callback rules,
319    /// the Channel writes of an inbound WriteGroup among them (#1319).
320    ///
321    /// ```
322    /// use bacnet_server::server::BACnetServer;
323    /// let builder = BACnetServer::bip_builder().mutation_authorizer(|_| false);
324    /// ```
325    pub fn mutation_authorizer<F>(mut self, authorizer: F) -> Self
326    where
327        F: Fn(&MutationAuthorizationContext) -> bool + Send + Sync + 'static,
328    {
329        self.config.mutation_authorizer = Some(Arc::new(authorizer));
330        self
331    }
332}
333
334#[cfg(feature = "sc-tls")]
335impl ScServerBuilder {
336    /// Select local mutation authorization (default: [`MutationPolicy::Permissive`]).
337    /// SC mTLS channel/peer authentication is not service authorization; identities
338    /// here are claimed link/routed addresses, never certificate principals.
339    pub fn mutation_policy(mut self, policy: MutationPolicy) -> Self {
340        self.config.mutation_policy = policy;
341        self
342    }
343
344    /// Set opt-in mutation policy. See [`MutationAuthorizer`] for callback rules,
345    /// the Channel writes of an inbound WriteGroup among them (#1319).
346    pub fn mutation_authorizer<F>(mut self, authorizer: F) -> Self
347    where
348        F: Fn(&MutationAuthorizationContext) -> bool + Send + Sync + 'static,
349    {
350        self.config.mutation_authorizer = Some(Arc::new(authorizer));
351        self
352    }
353}