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}