1use super::dcc_policy::{address_length_fits, routed_entry_names_direct_source};
4use super::{BipServerBuilder, ServerBuilder, TransportPort, runtime_clock};
5use bacnet_encoding::npdu::NpduAddress;
6use bacnet_network::layer::ReceivedApdu;
7use bacnet_objects::clock::ClockFrame;
8use bacnet_types::constructed::BACnetAddress;
9use bacnet_types::error::Error;
10use std::collections::HashMap;
11use std::sync::{Arc, Mutex};
12use std::time::{Duration, Instant};
13
14#[derive(Clone, Debug, Eq, PartialEq, Hash)]
20pub enum TimeSyncSource {
21 Direct(Vec<u8>),
24 Routed {
29 network: u16,
31 address: Vec<u8>,
33 },
34}
35
36impl TimeSyncSource {
37 fn validate(&self) -> Result<(), Error> {
38 let address = match self {
39 Self::Direct(address) => address,
40 Self::Routed { network, address } => {
41 if !(1..=65534).contains(network) {
42 return Err(denied("source network must be 1..=65534"));
43 }
44 address
45 }
46 };
47 if !address_length_fits(address.len()) {
48 return Err(denied(&format!(
49 "source address must contain 1..={} octets",
50 BACnetAddress::MAX_MAC_LEN
51 )));
52 }
53 Ok(())
54 }
55
56 fn from_received(received: &ReceivedApdu, local_network: Option<u16>) -> Result<Self, Error> {
65 use bacnet_endpoint_core::coordinator::CanonicalPeer;
66 let source = match &received.source_network {
67 Some(source) if !source.mac_address.is_empty() => {
68 match CanonicalPeer::from_source(&received.source_mac, Some(source), local_network)
69 {
70 CanonicalPeer::Direct(mac) => Self::Direct(mac.to_vec()),
71 CanonicalPeer::Routed { network, address } => Self::Routed {
72 network,
73 address: address.to_vec(),
74 },
75 }
76 }
77 Some(source) => Self::Routed {
78 network: source.network,
79 address: Vec::new(),
80 },
81 None => Self::Direct(received.source_mac.to_vec()),
82 };
83 source.validate()?;
84 Ok(source)
85 }
86}
87
88#[derive(Clone, Eq, PartialEq)]
91pub struct TimeSyncSourceRestriction(Vec<TimeSyncSource>);
92
93impl std::fmt::Debug for TimeSyncSourceRestriction {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.debug_struct("TimeSyncSourceRestriction")
96 .field("entries", &self.0.len())
97 .finish_non_exhaustive()
98 }
99}
100
101impl TimeSyncSourceRestriction {
102 pub fn new(sources: Vec<TimeSyncSource>) -> Result<Self, Error> {
108 if sources.len() > 256 {
109 return Err(denied("source restriction allows at most 256 entries"));
110 }
111 for source in &sources {
112 source.validate()?;
113 }
114 Ok(Self(sources))
115 }
116
117 pub(super) fn allows(
123 &self,
124 mac: &[u8],
125 routed: Option<&NpduAddress>,
126 local_network: Option<u16>,
127 ) -> bool {
128 match routed {
130 Some(source) => self.0.iter().any(|entry| matches!(entry,
131 TimeSyncSource::Routed { network, address }
132 if *network == source.network && address.as_slice() == source.mac_address.as_slice())),
133 None => self.0.iter().any(|entry| match entry {
134 TimeSyncSource::Direct(address) => address.as_slice() == mac,
135 TimeSyncSource::Routed { network, address } => {
136 routed_entry_names_direct_source(*network, address, mac, local_network)
137 }
138 }),
139 }
140 }
141}
142
143#[derive(Clone, Copy, Debug, PartialEq)]
145pub struct TimeSyncRateLimit {
146 pub max_per_second: f64,
148 pub burst_capacity: u32,
150}
151
152impl TimeSyncRateLimit {
153 fn validate(self) -> Result<(), Error> {
154 if !self.max_per_second.is_finite()
155 || self.max_per_second <= 0.0
156 || self.burst_capacity == 0
157 {
158 return Err(denied(
159 "rate must be positive and finite with a positive burst",
160 ));
161 }
162 Ok(())
163 }
164}
165
166#[derive(Clone, Debug, PartialEq)]
178pub struct TimeSyncPolicy {
179 pub enabled: bool,
181 pub source_restriction: Option<TimeSyncSourceRestriction>,
183 pub max_step: Option<Duration>,
187 pub per_source_rate: Option<TimeSyncRateLimit>,
189 pub global_rate: Option<TimeSyncRateLimit>,
191 pub coalesce_window: Duration,
194 pub global_coalesce_window: Duration,
197 pub max_sources: usize,
201}
202
203impl Default for TimeSyncPolicy {
204 fn default() -> Self {
205 Self {
206 enabled: true,
207 source_restriction: None,
208 max_step: None,
209 per_source_rate: None,
210 global_rate: None,
211 coalesce_window: Duration::ZERO,
212 global_coalesce_window: Duration::ZERO,
213 max_sources: 256,
214 }
215 }
216}
217
218impl TimeSyncPolicy {
219 pub fn validate(&self) -> Result<(), Error> {
221 for limit in [self.per_source_rate, self.global_rate]
222 .into_iter()
223 .flatten()
224 {
225 limit.validate()?;
226 }
227 if !(1..=65536).contains(&self.max_sources) {
228 return Err(denied("max_sources must be 1..=65536"));
229 }
230 Ok(())
231 }
232}
233
234impl<T: TransportPort + 'static> ServerBuilder<T> {
235 pub fn time_sync_policy(mut self, policy: TimeSyncPolicy) -> Self {
237 self.config.time_sync_policy = policy;
238 self
239 }
240}
241
242impl BipServerBuilder {
243 pub fn time_sync_policy(mut self, policy: TimeSyncPolicy) -> Self {
245 self.config.time_sync_policy = policy;
246 self
247 }
248}
249
250#[cfg(feature = "sc-tls")]
251impl super::ScServerBuilder {
252 pub fn time_sync_policy(mut self, policy: TimeSyncPolicy) -> Self {
255 self.config.time_sync_policy = policy;
256 self
257 }
258}
259
260pub(super) fn denied(reason: &str) -> Error {
261 Error::Encoding(format!("time sync: {reason}"))
262}
263
264pub(super) fn step_hundredths(supplied: i128, is_utc: bool, frame: ClockFrame) -> Option<u128> {
266 let local = super::clock::date_time_to_hundredths(frame.local_date, frame.local_time).ok()?;
267 let current = if is_utc {
268 local + i128::from(frame.utc_offset) * 6000
269 - if frame.daylight_savings_status {
270 360_000
271 } else {
272 0
273 }
274 } else {
275 local
276 };
277 Some(supplied.abs_diff(current))
278}
279
280pub(super) fn check_step(delta: Option<u128>, cap: Option<Duration>) -> Result<(), Error> {
281 if let Some(cap) = cap {
282 let delta = delta.ok_or_else(|| denied("clock unreadable for step cap"))?;
283 if delta * 10_000_000 > cap.as_nanos() {
284 return Err(denied("step cap exceeded"));
285 }
286 }
287 Ok(())
288}
289
290struct Bucket {
291 tokens: f64,
292 last_refill: Instant,
293}
294
295impl Bucket {
296 fn new(limit: Option<TimeSyncRateLimit>, now: Instant) -> Self {
297 Self {
298 tokens: limit.map_or(0.0, |l| f64::from(l.burst_capacity)),
299 last_refill: now,
300 }
301 }
302
303 fn refill(&mut self, limit: Option<TimeSyncRateLimit>, now: Instant) {
304 if let Some(limit) = limit {
305 let elapsed = now
306 .saturating_duration_since(self.last_refill)
307 .as_secs_f64();
308 self.tokens =
309 (self.tokens + elapsed * limit.max_per_second).min(f64::from(limit.burst_capacity));
310 }
311 self.last_refill = self.last_refill.max(now);
312 }
313
314 fn ready(&self, limit: Option<TimeSyncRateLimit>) -> bool {
315 limit.is_none() || self.tokens >= 1.0
316 }
317
318 fn deduct(&mut self, limit: Option<TimeSyncRateLimit>) {
319 if limit.is_some() {
320 self.tokens -= 1.0;
321 }
322 }
323}
324
325struct SourceState {
326 bucket: Bucket,
327 last_accepted: Instant,
328}
329
330struct State {
331 global: Bucket,
332 last_accepted: Option<Instant>,
333 sources: HashMap<TimeSyncSource, SourceState>,
334}
335
336pub(super) struct TimeSyncLimiter {
339 policy: TimeSyncPolicy,
340 state: Mutex<State>,
341}
342
343pub(super) fn request_limiters(
346 config: &super::ServerConfig,
347 device_instance: Option<u32>,
348) -> (Arc<super::DiscoveryLimiter>, Arc<TimeSyncLimiter>) {
349 (
350 Arc::new(super::DiscoveryLimiter::new(
351 config.discovery_policy.sanitized(),
352 device_instance,
353 )),
354 Arc::new(TimeSyncLimiter::new(config.time_sync_policy.clone())),
355 )
356}
357
358impl TimeSyncLimiter {
359 pub(super) fn new(policy: TimeSyncPolicy) -> Self {
360 Self {
361 state: Mutex::new(State {
362 global: Bucket::new(policy.global_rate, runtime_clock::now()),
363 last_accepted: None,
364 sources: HashMap::new(),
365 }),
366 policy,
367 }
368 }
369
370 pub(super) fn apply_at(
375 &self,
376 received: &ReceivedApdu,
377 local_network: Option<u16>,
378 now: Instant,
379 apply: impl FnOnce() -> Result<(), Error>,
380 ) -> Result<(), Error> {
381 let _ = received.provenance;
383 let policy = &self.policy;
384 let track_source = policy.per_source_rate.is_some() || !policy.coalesce_window.is_zero();
385 let key = track_source
386 .then(|| TimeSyncSource::from_received(received, local_network))
387 .transpose()?;
388 let mut state = self
389 .state
390 .lock()
391 .map_err(|_| denied("limiter lock poisoned"))?;
392 if state
393 .last_accepted
394 .is_some_and(|last| now.saturating_duration_since(last) < policy.global_coalesce_window)
395 {
396 return Err(denied("coalesced globally"));
397 }
398 state.global.refill(policy.global_rate, now);
399 if !state.global.ready(policy.global_rate) {
400 return Err(denied("global rate limit"));
401 }
402 if let Some(key) = &key {
403 if let Some(source) = state.sources.get_mut(key) {
404 if now.saturating_duration_since(source.last_accepted) < policy.coalesce_window {
405 return Err(denied("coalesced source"));
406 }
407 source.bucket.refill(policy.per_source_rate, now);
408 if !source.bucket.ready(policy.per_source_rate) {
409 return Err(denied("source rate limit"));
410 }
411 } else if state.sources.len() >= policy.max_sources {
412 state.sources.retain(|_, source| {
413 source.bucket.refill(policy.per_source_rate, now);
414 let full = policy.per_source_rate.is_none_or(|limit| {
415 source.bucket.tokens >= f64::from(limit.burst_capacity)
416 });
417 !full
418 || now.saturating_duration_since(source.last_accepted)
419 < policy.coalesce_window
420 });
421 if state.sources.len() >= policy.max_sources {
422 return Err(denied("source table full"));
423 }
424 }
425 }
426 apply()?;
429 state.global.deduct(policy.global_rate);
430 state.last_accepted = Some(now);
431 if let Some(key) = key {
432 let source = state.sources.entry(key).or_insert_with(|| SourceState {
433 bucket: Bucket::new(policy.per_source_rate, now),
434 last_accepted: now,
435 });
436 source.bucket.deduct(policy.per_source_rate);
437 source.last_accepted = now;
438 }
439 Ok(())
440 }
441}
442
443#[cfg(test)]
444#[path = "time_sync_policy_tests.rs"]
445mod tests;