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