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 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}