1use super::*;
2
3#[cfg(test)]
4#[path = "sc_dcc_mtls_tests.rs"]
5mod dcc_mtls_tests;
6
7impl BACnetServer<bacnet_transport::sc::ScTransport<bacnet_transport::sc_tls::TlsWebSocket>> {
8 #[cfg_attr(docsrs, doc(cfg(feature = "sc-tls")))]
10 pub fn sc_builder() -> ScServerBuilder {
11 ScServerBuilder {
12 config: ServerConfig::default(),
13 db: None,
14 configured_device_bindings: Vec::new(),
15 hub_url: String::new(),
16 tls_config: None,
17 vmac: [0; 6],
18 device_uuid: [0; 16],
19 heartbeat_interval_ms: 30_000,
20 heartbeat_timeout_ms: 60_000,
21 reconnect: None,
22 }
23 }
24}
25
26#[cfg_attr(docsrs, doc(cfg(feature = "sc-tls")))]
32pub struct ScServerBuilder {
33 pub(super) config: ServerConfig,
34 db: Option<ObjectDatabase>,
35 pub(super) configured_device_bindings: Vec<DeviceBinding>,
36 hub_url: String,
37 tls_config: Option<bacnet_transport::sc_tls::ScNodeTlsConfig>,
38 vmac: bacnet_transport::sc_frame::Vmac,
39 device_uuid: [u8; 16],
40 heartbeat_interval_ms: u64,
41 heartbeat_timeout_ms: u64,
42 reconnect: Option<bacnet_transport::sc::ScReconnectConfig>,
43}
44
45impl ScServerBuilder {
46 pub fn hub_url(mut self, url: &str) -> Self {
48 self.hub_url = url.to_string();
49 self
50 }
51
52 pub fn tls_config(mut self, config: bacnet_transport::sc_tls::ScNodeTlsConfig) -> Self {
70 self.tls_config = Some(config);
71 self
72 }
73
74 pub fn vmac(mut self, vmac: [u8; 6]) -> Self {
76 self.vmac = vmac;
77 self
78 }
79
80 pub fn device_uuid(mut self, uuid: [u8; 16]) -> Self {
89 self.device_uuid = uuid;
90 self
91 }
92
93 pub fn device(mut self, identity: bacnet_objects::DeviceIdentity) -> Self {
96 self.config.device = identity;
97 self
98 }
99
100 pub fn database(mut self, db: ObjectDatabase) -> Self {
102 self.db = Some(db);
103 self
104 }
105
106 pub fn device_binding(mut self, binding: DeviceBinding) -> Result<Self, Error> {
108 register_configured_binding(&mut self.configured_device_bindings, binding)?;
109 Ok(self)
110 }
111
112 pub fn heartbeat_interval_ms(mut self, ms: u64) -> Self {
114 self.heartbeat_interval_ms = ms;
115 self
116 }
117
118 pub fn heartbeat_timeout_ms(mut self, ms: u64) -> Self {
120 self.heartbeat_timeout_ms = ms;
121 self
122 }
123
124 pub fn reconnect(mut self, config: bacnet_transport::sc::ScReconnectConfig) -> Self {
126 self.reconnect = Some(config);
127 self
128 }
129
130 pub fn dcc_password(mut self, password: impl Into<String>) -> Self {
132 self.config.dcc_password = Some(password.into());
133 self
134 }
135
136 pub fn dcc_policy(mut self, policy: DccPolicy) -> Self {
138 self.config.dcc_policy = policy;
139 self
140 }
141
142 pub fn dcc_source_restriction(
144 mut self,
145 restriction: Option<super::DccSourceRestriction>,
146 ) -> Self {
147 self.config.dcc_source_restriction = restriction;
148 self
149 }
150
151 pub fn dcc_disable_rate_limit(mut self, limit: Option<super::DccDisableRateLimit>) -> Self {
153 self.config.dcc_disable_rate_limit = limit;
154 self
155 }
156
157 pub fn audit_notification_sink(mut self, sink: ObjectIdentifier) -> Self {
159 self.config.audit_notification_sink = Some(sink);
160 self
161 }
162
163 pub fn enable_fault_detection(mut self, enabled: bool) -> Self {
168 self.config.enable_fault_detection = enabled;
169 self
170 }
171
172 pub fn enable_event_enrollment(mut self, enabled: bool) -> Self {
174 self.config.enable_event_enrollment = enabled;
175 self
176 }
177
178 pub fn event_enrollment_interval_secs(mut self, secs: u64) -> Self {
180 self.config.event_enrollment_interval_secs = secs;
181 self
182 }
183
184 pub fn cov_policy(mut self, policy: CovPolicy) -> Self {
187 self.config.cov_policy = policy;
188 self
189 }
190
191 pub async fn build(
198 mut self,
199 ) -> Result<
200 BACnetServer<bacnet_transport::sc::ScTransport<bacnet_transport::sc_tls::TlsWebSocket>>,
201 Error,
202 > {
203 if let Some(config) = &self.reconnect {
204 config.validate()?;
205 }
206 if self.device_uuid == [0; 16] {
207 return Err(Error::Encoding(
208 "SC server builder: device_uuid is required and must not be all zero".into(),
209 ));
210 }
211 DeviceBindingTable::from_configured(self.configured_device_bindings.clone(), |mac| {
213 mac == bacnet_transport::sc_frame::BROADCAST_VMAC
214 })?;
215
216 let tls_config = self
217 .tls_config
218 .ok_or_else(|| Error::Encoding("SC server builder: tls_config is required".into()))?;
219
220 self.config.request_admission_policy.validate()?;
221 self.config.validate_dcc_config()?;
222 self.config.time_sync_policy.validate()?;
223 self.config.read_property_multiple_budget.validate()?;
224 self.config.get_alarm_summary_budget.validate()?;
225 self.config.get_enrollment_summary_budget.validate()?;
226 self.config.atomic_read_file_budget.validate()?;
227 self.config.atomic_write_file_budget.validate()?;
228 self.config.read_range_budget.validate()?;
229 self.config.get_event_information_budget.validate()?;
230 self.config.cov_policy.validate()?;
231 let db = crate::local_device::prepare_database(
232 &mut self.config, self.db,
233 bacnet_transport::sc::ScTransport::<bacnet_transport::sc_tls::TlsWebSocket>::LOCAL_RECEIVE_APDU_CAPACITY,
234 )?;
235 super::segmented_receive::validate_segment_timeout(&self.config)?;
236
237 let ws = bacnet_transport::sc_tls::TlsWebSocket::connect(&self.hub_url, tls_config.clone())
238 .await?;
239
240 let mut transport = bacnet_transport::sc::ScTransport::new(ws, self.vmac)
241 .with_device_uuid(self.device_uuid)
242 .with_heartbeat_interval_ms(self.heartbeat_interval_ms)
243 .with_heartbeat_timeout_ms(self.heartbeat_timeout_ms);
244 if let Some(rc) = self.reconnect {
245 let hub_url = self.hub_url.clone();
246 let tls_config = tls_config.clone();
247 transport = transport
248 .with_connector(move || {
249 let hub_url = hub_url.clone();
250 let tls_config = tls_config.clone();
251 async move {
252 bacnet_transport::sc_tls::TlsWebSocket::connect(&hub_url, tls_config).await
253 }
254 })
255 .with_reconnect(rc);
256 }
257
258 BACnetServer::start_with_clock_mode_and_bindings(
259 self.config,
260 db,
261 transport,
262 Some(ClockConfig::default()),
263 self.configured_device_bindings,
264 )
265 .await
266 }
267}
268
269#[cfg(test)]
270pub(super) const TEST_DEVICE_UUID: [u8; 16] = [
271 0x8e, 0x62, 0xac, 0x46, 0xd7, 0x08, 0x42, 0x26, 0x91, 0x37, 0x76, 0xa3, 0x2b, 0x61, 0x93, 0x15,
272];
273
274#[cfg(test)]
275pub(super) fn test_tls_config() -> bacnet_transport::sc_tls::ScNodeTlsConfig {
276 let rcgen::CertifiedKey { cert, signing_key } =
277 rcgen::generate_simple_self_signed(vec!["node".into()]).unwrap();
278 bacnet_transport::sc_tls::ScNodeTlsConfig::from_der(
279 vec![cert.der().clone()],
280 vec![cert.der().clone()],
281 tokio_rustls::rustls::pki_types::PrivatePkcs8KeyDer::from(signing_key.serialize_der())
282 .into(),
283 )
284 .unwrap()
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use bacnet_transport::sc::ScReconnectConfig;
291
292 #[tokio::test]
293 async fn sc_server_builder_requires_device_uuid_before_dial() {
294 for explicit_zero in [false, true] {
295 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
296 listener.set_nonblocking(true).unwrap();
297 let url = format!("wss://{}", listener.local_addr().unwrap());
298 let mut builder = BACnetServer::sc_builder()
299 .hub_url(&url)
300 .tls_config(test_tls_config());
301 if explicit_zero {
302 builder = builder.device_uuid([0; 16]);
303 }
304 let error = tokio::time::timeout(std::time::Duration::from_secs(1), builder.build())
305 .await
306 .expect("UUID failure must not wait for TLS/network I/O")
307 .err()
308 .expect("missing/zero UUID must fail before dialing");
309 assert!(
310 matches!(&error, Error::Encoding(message) if message == "SC server builder: device_uuid is required and must not be all zero"),
311 "expected UUID configuration error, got {error:?}"
312 );
313 assert_eq!(
314 listener.accept().unwrap_err().kind(),
315 std::io::ErrorKind::WouldBlock
316 );
317 let _probe = std::net::TcpStream::connect(listener.local_addr().unwrap()).unwrap();
319 let listener = tokio::net::TcpListener::from_std(listener).unwrap();
320 let _accepted =
321 tokio::time::timeout(std::time::Duration::from_secs(1), listener.accept())
322 .await
323 .unwrap()
324 .unwrap();
325 }
326 }
327
328 #[tokio::test]
329 async fn alarm_summary_sc_budget_validation_before_dial() {
330 for budget in [
331 GetAlarmSummaryBudget {
332 max_objects: 0,
333 ..Default::default()
334 },
335 GetAlarmSummaryBudget {
336 max_service_ack_bytes: 0,
337 ..Default::default()
338 },
339 ] {
340 let builder = BACnetServer::sc_builder()
341 .hub_url("not-a-websocket-url")
342 .tls_config(test_tls_config())
343 .device_uuid(TEST_DEVICE_UUID)
344 .get_alarm_summary_budget(budget);
345 assert_eq!(builder.config.get_alarm_summary_budget, budget);
346 assert!(
347 matches!(builder.build().await.err(), Some(Error::Encoding(m)) if m.contains("alarm_summary_max_"))
348 );
349 }
350 }
351
352 #[tokio::test]
353 async fn rpm_sc_budget_validation_before_dial() {
354 for budget in [
355 ReadPropertyMultipleBudget {
356 max_result_elements: 0,
357 ..Default::default()
358 },
359 ReadPropertyMultipleBudget {
360 max_service_ack_bytes: 0,
361 ..Default::default()
362 },
363 ] {
364 let builder = BACnetServer::sc_builder()
365 .hub_url("not-a-websocket-url")
366 .tls_config(test_tls_config())
367 .device_uuid(TEST_DEVICE_UUID)
368 .read_property_multiple_budget(budget);
369 assert_eq!(builder.config.read_property_multiple_budget, budget);
370 let error = builder.build().await.err().unwrap();
371 assert!(matches!(error, Error::Encoding(m) if m.contains("rpm_max_")));
372 let error = BACnetServer::sc_builder()
373 .device_uuid(TEST_DEVICE_UUID)
374 .read_property_multiple_budget(budget)
375 .build()
376 .await
377 .err()
378 .unwrap();
379 assert!(matches!(error, Error::Encoding(m) if m.contains("tls_config")));
380 }
381 }
382
383 #[tokio::test]
384 async fn sc_server_builder_rejects_invalid_reconnect_before_tls_and_bindings() {
385 for broadcast_binding in [false, true] {
386 for max_retries in [0, 10] {
387 for (initial_delay_ms, max_delay_ms) in [(0, 1), (1, 0), (0, 0), (2, 1)] {
388 let mut builder = BACnetServer::sc_builder().reconnect(ScReconnectConfig {
389 initial_delay_ms,
390 max_delay_ms,
391 max_retries,
392 });
393 if broadcast_binding {
394 let device = ObjectIdentifier::new(ObjectType::DEVICE, 46).unwrap();
395 let binding = DeviceBinding::local(
396 device,
397 bacnet_transport::sc_frame::BROADCAST_VMAC,
398 )
399 .unwrap();
400 builder = builder.device_binding(binding).unwrap();
401 }
402 let error = builder
403 .build()
404 .await
405 .err()
406 .expect("invalid reconnect must fail");
407 assert!(
408 matches!(&error, Error::OutOfRange(message) if message.contains("reconnect")),
409 "expected reconnect error before TLS/bindings, got {error:?}"
410 );
411 }
412 }
413 }
414 }
415
416 #[tokio::test]
417 async fn sc_server_builder_valid_reconnect_preserves_missing_tls_error() {
418 for reconnect in [
419 None,
420 Some(ScReconnectConfig::default()),
421 Some(ScReconnectConfig {
422 initial_delay_ms: 1,
423 max_delay_ms: 1,
424 max_retries: 0,
425 }),
426 Some(ScReconnectConfig {
427 initial_delay_ms: 1,
428 max_delay_ms: 2,
429 max_retries: u32::MAX,
430 }),
431 ] {
432 let mut builder = BACnetServer::sc_builder().device_uuid(TEST_DEVICE_UUID);
433 if let Some(config) = reconnect {
434 builder = builder.reconnect(config);
435 }
436 assert!(matches!(
437 builder.build().await,
438 Err(Error::Encoding(message)) if message == "SC server builder: tls_config is required"
439 ));
440 }
441 }
442
443 #[test]
444 fn sc_server_builder_cov_policy_configures_server_config() {
445 let policy = CovPolicy {
446 max_subscriptions_per_peer: 12,
447 ..CovPolicy::default()
448 };
449 let builder = BACnetServer::sc_builder().cov_policy(policy.clone());
450 assert_eq!(builder.config.cov_policy, policy);
451 }
452
453 #[tokio::test]
454 async fn sc_invalid_cov_policy_precedes_dial() {
455 let error = BACnetServer::sc_builder()
456 .hub_url("not-a-websocket-url")
457 .tls_config(test_tls_config())
458 .device_uuid(TEST_DEVICE_UUID)
459 .cov_policy(CovPolicy {
460 max_subscriptions_global: 0,
461 ..CovPolicy::default()
462 })
463 .build()
464 .await
465 .err()
466 .unwrap();
467 assert!(matches!(error, Error::Encoding(m) if m.contains("max_subscriptions_global")));
468 }
469
470 #[tokio::test]
471 async fn admission_sc_invalid_policy_precedes_dial_and_preserves_reconnect_precedence() {
472 for bad in [0, usize::MAX] {
473 let policy = RequestAdmissionPolicy {
474 max_confirmed_in_flight: 1,
475 confirmed_recovery_reserve: 0,
476 max_unconfirmed_in_flight: bad,
477 ..Default::default()
478 };
479 let error = BACnetServer::sc_builder()
480 .hub_url("not-a-websocket-url")
481 .tls_config(test_tls_config())
482 .device_uuid(TEST_DEVICE_UUID)
483 .request_admission_policy(policy)
484 .build()
485 .await
486 .err()
487 .unwrap();
488 assert!(matches!(error, Error::Encoding(m) if m.contains("max_unconfirmed_in_flight")));
489 let error = BACnetServer::sc_builder()
490 .request_admission_policy(policy)
491 .reconnect(ScReconnectConfig {
492 initial_delay_ms: 0,
493 ..Default::default()
494 })
495 .build()
496 .await
497 .err()
498 .unwrap();
499 assert!(matches!(error, Error::OutOfRange(m) if m.contains("reconnect")));
500 }
501 }
502
503 #[tokio::test]
504 async fn admission_sc_invalid_policy_recovery_precedes_dial() {
505 for reserve in [64, 65] {
506 let policy = RequestAdmissionPolicy {
507 confirmed_recovery_reserve: reserve,
508 ..Default::default()
509 };
510 let error = BACnetServer::sc_builder()
511 .hub_url("not-a-websocket-url")
512 .tls_config(test_tls_config())
513 .device_uuid(TEST_DEVICE_UUID)
514 .request_admission_policy(policy)
515 .build()
516 .await
517 .err()
518 .unwrap();
519 assert!(
520 matches!(error, Error::Encoding(m) if m.contains("confirmed_recovery_reserve"))
521 );
522 let error = BACnetServer::sc_builder()
523 .request_admission_policy(policy)
524 .reconnect(ScReconnectConfig {
525 initial_delay_ms: 0,
526 ..Default::default()
527 })
528 .build()
529 .await
530 .err()
531 .unwrap();
532 assert!(matches!(error, Error::OutOfRange(m) if m.contains("reconnect")));
533 }
534 }
535
536 #[tokio::test]
537 async fn admission_sc_invalid_policy_peer_limits_precede_dial() {
538 for confirmed in [true, false] {
539 for bad in [0, usize::MAX] {
540 let mut policy = RequestAdmissionPolicy::default();
541 let name = if confirmed {
542 policy.max_confirmed_in_flight_per_peer = bad;
543 "max_confirmed_in_flight_per_peer"
544 } else {
545 policy.max_unconfirmed_in_flight_per_peer = bad;
546 "max_unconfirmed_in_flight_per_peer"
547 };
548 let builder = BACnetServer::sc_builder()
549 .hub_url("not-a-websocket-url")
550 .tls_config(test_tls_config())
551 .device_uuid(TEST_DEVICE_UUID)
552 .request_admission_policy(policy);
553 assert_eq!(builder.config.request_admission_policy, policy);
554 let error = builder.build().await.err().unwrap();
555 assert!(matches!(error, Error::Encoding(m) if m.contains(name)));
556 }
557 }
558 }
559}