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}