Skip to main content

bacnet_server/server/
config.rs

1use super::*;
2use crate::ServerPolicy;
3use crate::mutation::MutationPolicy;
4
5/// Server configuration.
6#[derive(Clone)]
7pub struct ServerConfig {
8    /// Explicit concrete Network Port for one owned B/IP transport, in
9    /// NORMAL, FOREIGN or BBMD mode (a BBMD that also registers as a foreign
10    /// device is refused at start). None leaves configured objects unbound
11    /// and receiving-port wildcard unavailable.
12    pub registered_network_port: Option<ObjectIdentifier>,
13    /// Optional target Audit Reporter profile. Its recipient is provisioned on
14    /// the built-in Device and becomes writable while the runtime is installed.
15    pub audit_reporters: Option<AuditReportersConfig>,
16    /// Per-service GetAlarmSummary database scan and encoded response limits.
17    pub get_alarm_summary_budget: GetAlarmSummaryBudget,
18    /// Local complete-response limits for GetEnrollmentSummary.
19    pub get_enrollment_summary_budget: GetEnrollmentSummaryBudget,
20    /// Local AtomicReadFile raw-count and complete service-ACK limits.
21    pub atomic_read_file_budget: AtomicReadFileBudget,
22    /// Local AtomicWriteFile payload admission limits.
23    pub atomic_write_file_budget: AtomicWriteFileBudget,
24    /// ReadRange directional page item and logical service-byte limits.
25    pub read_range_budget: ReadRangeBudget,
26    /// GetEventInformation database admission and strict response-page limits.
27    pub get_event_information_budget: GetEventInformationBudget,
28    /// Per-service RPM work and encoded response limits (finite by default).
29    pub read_property_multiple_budget: ReadPropertyMultipleBudget,
30    /// Local interface to bind.
31    pub interface: Ipv4Addr,
32    /// UDP port (default 0xBAC0 = 47808).
33    pub port: u16,
34    /// Directed broadcast address.
35    pub broadcast_address: Ipv4Addr,
36    /// B/IP only: bind the interface address itself, so devices on other
37    /// addresses of this host can share the port (#1538). Off by default.
38    /// Needs an explicit interface and a nonzero port; see
39    /// `BipTransport::set_share_port_by_address` for what changes. Only the
40    /// B/IP builder reads it; a transport passed to `start` has its own.
41    pub share_port_by_address: bool,
42    /// Device declarations shared by stock objects, discovery and active limits.
43    /// Startup resolves its receive ceiling against the owned transport. An
44    /// explicitly supplied selected Device must match; an empty database may
45    /// later add this same OID, but never rebinds the runtime to another Device.
46    pub device: bacnet_objects::DeviceIdentity,
47    /// APDU timeout in ms of confirmed COV and event notifications and of the
48    /// WriteProperty a Command action sends to another device (default
49    /// 3000 ms): how long each attempt waits for the reply once its send has
50    /// completed, before the next retry. The transport bounds the send itself.
51    /// It is also the base of the COV hold-off after a failed confirmed report,
52    /// one full retry cycle: this timeout times the attempts (#896).
53    pub cov_retry_timeout_ms: u64,
54    /// Opt-in inbound time-sync restrictions; default allows all, with no step cap.
55    pub time_sync_policy: TimeSyncPolicy,
56    /// Optional fast, nonblocking observer invoked after the clock changes.
57    /// A panic is caught (with unwind builds); the change stands and ingress
58    /// continues. This callback cannot authorize or roll back synchronization.
59    pub on_time_sync: Option<Arc<dyn Fn(TimeSyncData) + Send + Sync>>,
60    /// Optional ReinitializeDevice handler, called with the requester's
61    /// [`ReinitializeContext`] once the request passes its password and state
62    /// checks. Without it every such request is refused with
63    /// SERVICES / SERVICE_REQUEST_DENIED.
64    ///
65    /// The rules are on [`ReinitializeHandler`]. In short: the SimpleACK goes
66    /// out only after the handler returns, so register any restart with
67    /// [`ReinitializeContext::after_reply`] rather than restarting inline; it
68    /// runs once the reply has been handed to the transport (#1565), and
69    /// never for a refusal. The handler runs synchronously with the
70    /// object database write-locked, so keep it quick and hand slow work to a
71    /// task; its database edits skip the COV, event and Audit work of
72    /// [`BACnetServer::write_local`], so apply changes that need it there
73    /// afterwards. Without [`reinit_password`](Self::reinit_password) any peer
74    /// reaches it, and neither [`mutation_policy`](Self::mutation_policy) nor
75    /// [`policy`](Self::policy) covers this service, so restrict sources
76    /// through the context. Refuse
77    /// with [`Error::protocol`]: a panic or an [`Error::reject`] is answered
78    /// SERVICES / OTHER.
79    pub on_reinitialize: Option<ReinitializeHandler>,
80    /// Local mutation authorization mode (default: permissive). SC mTLS channel/peer
81    /// authentication is not service authorization. See [`MutationPolicy`].
82    /// `DenyAll` denies every covered mutation without asking
83    /// [`policy`](Self::policy), each Channel write of an inbound WriteGroup
84    /// included, counted in `BACnetServer::mutation_decision_counters` (#1319).
85    /// Denials mutate nothing and write no audit log.
86    pub mutation_policy: MutationPolicy,
87    /// The application's authorization decisions (default: none). See
88    /// [`ServerPolicy`] for what it decides, each method's default and the
89    /// direct-SC principal its contexts carry. `None` behaves as those
90    /// defaults: covered mutations are allowed (without being decoded for
91    /// authorization first), and LifeSafetyOperation and both
92    /// AuditNotification services are denied.
93    pub policy: Option<Arc<dyn ServerPolicy>>,
94    /// Exactly one explicitly configured Audit Log notification sink.
95    ///
96    /// Absence is fail-closed; the server never selects a sink by database
97    /// iteration order. Storing a notification also needs
98    /// [`policy`](Self::policy) to authorize it.
99    /// A built-in AuditLogObject configured with `set_member_of` also forwards
100    /// record-changing accepted batches once, after commit, to that parent using
101    /// a configured DeviceBinding. No backlog, retries, or durable send progress.
102    pub audit_notification_sink: Option<ObjectIdentifier>,
103    /// Optional password required for DeviceCommunicationControl.
104    pub dcc_password: Option<String>,
105    /// Local DCC authorization. Supplying a password alone does not enable DCC.
106    pub dcc_policy: DccPolicy,
107    /// Optional exact claimed-source restriction, valid only with RequirePassword.
108    pub dcc_source_restriction: Option<DccSourceRestriction>,
109    /// Optional global DISABLE_INITIATION budget; does not enable DCC authorization.
110    pub dcc_disable_rate_limit: Option<DccDisableRateLimit>,
111    /// Optional password required for ReinitializeDevice.
112    pub reinit_password: Option<String>,
113    /// Enable periodic fault detection / reliability evaluation.
114    /// When true, the server invokes every object's opt-in, object-owned
115    /// reliability evaluation hook every 10 seconds. Stock objects currently
116    /// inherit the no-op default.
117    ///
118    /// This governs reliability evaluation only. Event Enrollment evaluation
119    /// is configured separately via [`enable_event_enrollment`](Self::enable_event_enrollment).
120    pub enable_fault_detection: bool,
121    /// Enable periodic Event Enrollment evaluation (default `true`).
122    ///
123    /// When true, the server re-reads the property each Event Enrollment object
124    /// names in its `Object_Property_Reference` and applies the configured event
125    /// algorithm. Startup: the task is spawned by [`start`](BACnetServer::start)
126    /// and its first pass runs immediately, then once per interval. Shutdown:
127    /// [`stop`](BACnetServer::stop) aborts it and awaits the abort.
128    ///
129    /// This switch governs the evaluation task; it is not the per-object
130    /// `Event_Detection_Enable` property of ASHRAE 135-2020 Clause 13.2.2.1.
131    /// Setting it false stops evaluation without performing the reset that
132    /// clause requires of a disabled detector (`Event_State` to NORMAL, with the
133    /// corresponding timestamp and acknowledgment state), so a device carrying
134    /// active enrollments will hold whatever state it last detected.
135    ///
136    /// Evaluation is a no-op on databases holding no Event Enrollment objects,
137    /// so the default is on.
138    ///
139    /// Successful enabled transitions commit `Event_State`,
140    /// `Acked_Transitions`, and `Event_Time_Stamps` atomically and are then
141    /// routed through the shared EventNotification sender. Event Enrollment
142    /// message text remains intentionally absent, and exact event-specific
143    /// notification values are deferred to the payload projection work.
144    pub enable_event_enrollment: bool,
145    /// Interval in seconds between Event Enrollment evaluation passes (default 10).
146    ///
147    /// This is a sampling cadence with no basis in ASHRAE 135-2020, which
148    /// prescribes no evaluation frequency and leaves acquisition of a monitored
149    /// value a local matter (Clause 12.12). It is not the `Time_Delay` of an
150    /// event algorithm, which is how long a condition must persist before a
151    /// transition is indicated (Clause 13.3) — a coarse interval delays
152    /// detection and can miss a condition that both appears and clears between
153    /// two passes.
154    ///
155    /// A value of `0` is clamped to one second. Ignored when
156    /// [`enable_event_enrollment`](Self::enable_event_enrollment) is false.
157    pub event_enrollment_interval_secs: u64,
158    /// Discovery rate-limiting and duplicate suppression policy.
159    pub discovery_policy: DiscoveryPolicy,
160    /// COV quota, rate accounting, and notification work budget policy.
161    pub cov_policy: CovPolicy,
162    /// Positive independent top-level request handler limits.
163    pub request_admission_policy: RequestAdmissionPolicy,
164}
165
166impl std::fmt::Debug for ServerConfig {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        f.debug_struct("ServerConfig")
169            .field("audit_reporters", &self.audit_reporters)
170            .field("get_alarm_summary_budget", &self.get_alarm_summary_budget)
171            .field(
172                "get_enrollment_summary_budget",
173                &self.get_enrollment_summary_budget,
174            )
175            .field("atomic_read_file_budget", &self.atomic_read_file_budget)
176            .field("atomic_write_file_budget", &self.atomic_write_file_budget)
177            .field("read_range_budget", &self.read_range_budget)
178            .field(
179                "get_event_information_budget",
180                &self.get_event_information_budget,
181            )
182            .field(
183                "read_property_multiple_budget",
184                &self.read_property_multiple_budget,
185            )
186            .field("interface", &self.interface)
187            .field("port", &self.port)
188            .field("broadcast_address", &self.broadcast_address)
189            .field("share_port_by_address", &self.share_port_by_address)
190            .field("device", &self.device)
191            .field("cov_retry_timeout_ms", &self.cov_retry_timeout_ms)
192            .field("time_sync_policy", &self.time_sync_policy)
193            .field("mutation_policy", &self.mutation_policy)
194            .field(
195                "on_time_sync",
196                &self.on_time_sync.as_ref().map(|_| "<callback>"),
197            )
198            .field(
199                "on_reinitialize",
200                &self.on_reinitialize.as_ref().map(|_| "<callback>"),
201            )
202            .field("policy", &self.policy.as_ref().map(|_| "<policy>"))
203            .field("audit_notification_sink", &self.audit_notification_sink)
204            .field("dcc_password", &self.dcc_password.as_ref().map(|_| "***"))
205            .field("dcc_policy", &self.dcc_policy)
206            .field("dcc_source_restriction", &self.dcc_source_restriction)
207            .field("dcc_disable_rate_limit", &self.dcc_disable_rate_limit)
208            .field(
209                "reinit_password",
210                &self.reinit_password.as_ref().map(|_| "***"),
211            )
212            .field("enable_fault_detection", &self.enable_fault_detection)
213            .field("enable_event_enrollment", &self.enable_event_enrollment)
214            .field(
215                "event_enrollment_interval_secs",
216                &self.event_enrollment_interval_secs,
217            )
218            .field("discovery_policy", &self.discovery_policy)
219            .field("cov_policy", &self.cov_policy)
220            .field("request_admission_policy", &self.request_admission_policy)
221            .finish()
222    }
223}
224
225impl ServerConfig {
226    /// The installed policy, if it decides mutations
227    /// ([`ServerPolicy::decides_mutations`]). `None` takes the no-policy
228    /// path: allowed without decoding for authorization.
229    pub(crate) fn mutation_decider(&self) -> Option<&Arc<dyn ServerPolicy>> {
230        self.policy
231            .as_ref()
232            .filter(|policy| policy.decides_mutations())
233    }
234}
235
236impl Default for ServerConfig {
237    fn default() -> Self {
238        Self {
239            device: bacnet_objects::DeviceIdentity::default(),
240            registered_network_port: None,
241            audit_reporters: None,
242            interface: Ipv4Addr::UNSPECIFIED,
243            read_property_multiple_budget: ReadPropertyMultipleBudget::default(),
244            get_alarm_summary_budget: GetAlarmSummaryBudget::default(),
245            get_enrollment_summary_budget: GetEnrollmentSummaryBudget::default(),
246            atomic_read_file_budget: AtomicReadFileBudget::default(),
247            atomic_write_file_budget: AtomicWriteFileBudget::default(),
248            read_range_budget: ReadRangeBudget::default(),
249            get_event_information_budget: GetEventInformationBudget::default(),
250            port: 0xBAC0,
251            broadcast_address: Ipv4Addr::BROADCAST,
252            share_port_by_address: false,
253            cov_retry_timeout_ms: 3000,
254            time_sync_policy: TimeSyncPolicy::default(),
255            on_time_sync: None,
256            on_reinitialize: None,
257            mutation_policy: MutationPolicy::default(),
258            policy: None,
259            audit_notification_sink: None,
260            dcc_password: None,
261            dcc_policy: DccPolicy::default(),
262            dcc_source_restriction: None,
263            dcc_disable_rate_limit: None,
264            reinit_password: None,
265            enable_fault_detection: false,
266            enable_event_enrollment: true,
267            event_enrollment_interval_secs: 10,
268            discovery_policy: DiscoveryPolicy::default(),
269            cov_policy: CovPolicy::default(),
270            request_admission_policy: RequestAdmissionPolicy::default(),
271        }
272    }
273}
274
275impl<T: TransportPort + 'static> ServerBuilder<T> {
276    /// Select local mutation authorization (default: [`MutationPolicy::Permissive`]).
277    /// SC mTLS channel/peer authentication is not service authorization.
278    pub fn mutation_policy(mut self, policy: MutationPolicy) -> Self {
279        self.config.mutation_policy = policy;
280        self
281    }
282
283    /// Install the server's authorization policy; see [`ServerPolicy`], or
284    /// [`FnPolicy`](crate::FnPolicy) to build one from closures.
285    /// It replaces any policy set before; put every decision in one policy
286    /// (one `FnPolicy` chain) rather than calling this twice.
287    pub fn policy(mut self, policy: impl ServerPolicy) -> Self {
288        self.config.policy = Some(Arc::new(policy));
289        self
290    }
291}
292
293impl BipServerBuilder {
294    /// Set the local interface IP.
295    pub fn interface(mut self, ip: Ipv4Addr) -> Self {
296        self.config.interface = ip;
297        self
298    }
299
300    /// Set the UDP port.
301    pub fn port(mut self, port: u16) -> Self {
302        self.config.port = port;
303        self
304    }
305
306    /// Set the directed broadcast address.
307    pub fn broadcast_address(mut self, addr: Ipv4Addr) -> Self {
308        self.config.broadcast_address = addr;
309        self
310    }
311
312    /// Set the B/IP mode: a normal node (the default), a foreign device or
313    /// a BBMD (#938). A foreign device's `build()` doesn't wait for its BBMD
314    /// to accept the registration; poll
315    /// [`BACnetServer::foreign_registration`] for how it went. A registered
316    /// Network Port reports the mode in `BACnet_IP_Mode`.
317    ///
318    /// ```
319    /// use bacnet_server::server::BACnetServer;
320    /// use bacnet_transport::bbmd::ForeignDevicePolicy;
321    /// use bacnet_transport::bip::{BbmdOptions, BipMode};
322    /// let builder = BACnetServer::bip_builder().mode(BipMode::Bbmd(BbmdOptions {
323    ///     foreign_device_policy: Some(ForeignDevicePolicy::default()),
324    ///     ..BbmdOptions::default()
325    /// }));
326    /// ```
327    pub fn mode(mut self, mode: bacnet_transport::bip::BipMode) -> Self {
328        self.mode = mode;
329        self
330    }
331
332    /// Make the server a B/IP-M node in `multicast`'s group (Annex J.8), or,
333    /// with `None` (the default), not: its broadcasts go to the group, and
334    /// it takes broadcasts only from the group. Needs an explicit
335    /// [`interface`](Self::interface), and a nonzero [`port`](Self::port)
336    /// unless the group has its own; `build()` fails on what
337    /// [`BipTransport::set_multicast`](bacnet_transport::bip::BipTransport::set_multicast)
338    /// refuses. With [`BipMode::Bbmd`]
339    /// the server is the BBMD that serves the group (Annex J.8.3), and its
340    /// own BDT row must be two-hop. A registered Network Port serves the
341    /// group as `BACnet_IP_Multicast_Address`, writable when the server
342    /// answers ReinitializeDevice (#941); on a BBMD a written group is
343    /// activated together with the BDT, both or neither (#1963).
344    pub fn multicast(mut self, multicast: Option<BipMulticast>) -> Self {
345        self.multicast = multicast;
346        self
347    }
348
349    /// Bind the interface address itself, so devices on other addresses of
350    /// this host can share the port, each getting only its own unicast
351    /// (#1538). Off by default, which binds `0.0.0.0`. Needs an explicit
352    /// [`interface`](Self::interface) and a nonzero port, or `build` fails.
353    /// See [`BipTransport::set_share_port_by_address`] for what changes.
354    pub fn share_port_by_address(mut self, enabled: bool) -> Self {
355        self.config.share_port_by_address = enabled;
356        self
357    }
358
359    /// Select local mutation authorization (default: [`MutationPolicy::Permissive`]).
360    /// SC mTLS channel/peer authentication is not service authorization.
361    ///
362    /// ```
363    /// use bacnet_server::{mutation::MutationPolicy, server::BACnetServer};
364    /// let builder = BACnetServer::bip_builder().mutation_policy(MutationPolicy::DenyAll);
365    /// ```
366    pub fn mutation_policy(mut self, policy: MutationPolicy) -> Self {
367        self.config.mutation_policy = policy;
368        self
369    }
370
371    /// Install the server's authorization policy; see [`ServerPolicy`], or
372    /// [`FnPolicy`](crate::FnPolicy) to build one from closures.
373    /// It replaces any policy set before; put every decision in one policy
374    /// (one `FnPolicy` chain) rather than calling this twice.
375    ///
376    /// ```
377    /// use bacnet_server::server::BACnetServer;
378    /// use bacnet_server::FnPolicy;
379    /// // Refuse every remote write; LifeSafety and Audit keep denying.
380    /// let builder = BACnetServer::bip_builder().policy(FnPolicy::new().mutation(|_| false));
381    /// ```
382    pub fn policy(mut self, policy: impl ServerPolicy) -> Self {
383        self.config.policy = Some(Arc::new(policy));
384        self
385    }
386}
387
388#[cfg(feature = "sc-tls")]
389impl ScServerBuilder {
390    /// Select local mutation authorization (default: [`MutationPolicy::Permissive`]).
391    /// SC mTLS channel/peer authentication is not service authorization.
392    pub fn mutation_policy(mut self, policy: MutationPolicy) -> Self {
393        self.config.mutation_policy = policy;
394        self
395    }
396
397    /// Install the server's authorization policy; see [`ServerPolicy`], or
398    /// [`FnPolicy`](crate::FnPolicy) to build one from closures.
399    /// It replaces any policy set before; put every decision in one policy
400    /// (one `FnPolicy` chain) rather than calling this twice.
401    pub fn policy(mut self, policy: impl ServerPolicy) -> Self {
402        self.config.policy = Some(Arc::new(policy));
403        self
404    }
405}
406
407impl<T: TransportPort + 'static> BACnetServer<T> {
408    /// Start a server builder for a caller-supplied transport type, with default configuration and
409    /// stock Device generation unless a database is supplied.
410    pub fn generic_builder() -> ServerBuilder<T> {
411        ServerBuilder {
412            config: ServerConfig::default(),
413            db: None,
414            transport: None,
415            configured_device_bindings: Vec::new(),
416        }
417    }
418}