1use serde::Deserialize;
2
3#[derive(Debug, Clone, Deserialize, Default)]
4pub struct RtimeConfig {
5 #[serde(default)]
6 pub general: GeneralConfig,
7 #[serde(default)]
8 pub clock: ClockConfig,
9 #[serde(default)]
10 pub ntp: NtpConfig,
11 #[serde(default)]
12 pub ptp: PtpConfig,
13 #[serde(default)]
14 pub metrics: MetricsConfig,
15 #[serde(default)]
16 pub management: ManagementConfig,
17}
18
19#[derive(Debug, Clone, Deserialize)]
20pub struct GeneralConfig {
21 #[serde(default = "default_log_level")]
22 pub log_level: String,
23}
24
25impl Default for GeneralConfig {
26 fn default() -> Self {
27 Self {
28 log_level: default_log_level(),
29 }
30 }
31}
32
33fn default_log_level() -> String {
34 "info".into()
35}
36
37#[derive(Debug, Clone, Deserialize)]
38pub struct ClockConfig {
39 #[serde(default = "default_true")]
40 pub discipline: bool,
41 #[serde(default = "default_step_threshold_ms")]
42 pub step_threshold_ms: f64,
43 #[serde(default = "default_panic_threshold_ms")]
44 pub panic_threshold_ms: f64,
45 #[serde(default = "default_true")]
46 pub allow_initial_step: bool,
47 #[serde(default = "default_clock_interface")]
48 pub interface: String,
49}
50
51impl Default for ClockConfig {
52 fn default() -> Self {
53 Self {
54 discipline: true,
55 step_threshold_ms: default_step_threshold_ms(),
56 panic_threshold_ms: default_panic_threshold_ms(),
57 allow_initial_step: true,
58 interface: default_clock_interface(),
59 }
60 }
61}
62
63fn default_true() -> bool {
64 true
65}
66
67fn default_step_threshold_ms() -> f64 {
68 128.0
69}
70
71fn default_panic_threshold_ms() -> f64 {
72 1_000_000.0
78}
79
80fn default_clock_interface() -> String {
81 "system".into()
82}
83
84#[derive(Debug, Clone, Deserialize)]
85pub struct NtpConfig {
86 #[serde(default = "default_true")]
87 pub enabled: bool,
88 #[serde(default = "default_ntp_listen")]
89 pub listen: String,
90 #[serde(default = "default_rate_limit")]
91 pub rate_limit: f64,
92 #[serde(default = "default_rate_burst")]
93 pub rate_burst: u32,
94 #[serde(default)]
95 pub nts: NtsConfig,
96 #[serde(default)]
97 pub sources: Vec<NtpSourceConfig>,
98}
99
100impl Default for NtpConfig {
101 fn default() -> Self {
102 Self {
103 enabled: true,
104 listen: default_ntp_listen(),
105 rate_limit: default_rate_limit(),
106 rate_burst: default_rate_burst(),
107 nts: NtsConfig::default(),
108 sources: Vec::new(),
109 }
110 }
111}
112
113fn default_ntp_listen() -> String {
114 "127.0.0.1:123".into()
115}
116
117fn default_rate_limit() -> f64 {
118 16.0
119}
120
121fn default_rate_burst() -> u32 {
122 32
123}
124
125#[derive(Debug, Clone, Deserialize)]
126pub struct NtsConfig {
127 #[serde(default)]
128 pub enabled: bool,
129 #[serde(default = "default_nts_ke_listen")]
130 pub ke_listen: String,
131 pub certificate: Option<String>,
132 pub private_key: Option<String>,
133}
134
135impl Default for NtsConfig {
136 fn default() -> Self {
137 Self {
138 enabled: false,
139 ke_listen: default_nts_ke_listen(),
140 certificate: None,
141 private_key: None,
142 }
143 }
144}
145
146fn default_nts_ke_listen() -> String {
147 "127.0.0.1:4460".into()
148}
149
150#[derive(Debug, Clone, Deserialize)]
151pub struct NtpSourceConfig {
152 pub address: String,
153 #[serde(default)]
154 pub nts: bool,
155 #[serde(default = "default_min_poll")]
156 pub min_poll: i8,
157 #[serde(default = "default_max_poll")]
158 pub max_poll: i8,
159}
160
161fn default_min_poll() -> i8 {
162 4
163}
164
165fn default_max_poll() -> i8 {
166 10
167}
168
169#[derive(Debug, Clone, Deserialize)]
170pub struct PtpConfig {
171 #[serde(default)]
172 pub enabled: bool,
173 #[serde(default)]
174 pub domain: u8,
175 #[serde(default = "default_ptp_interface")]
176 pub interface: String,
177 #[serde(default = "default_ptp_transport")]
178 pub transport: String,
179 #[serde(default = "default_priority")]
180 pub priority1: u8,
181 #[serde(default = "default_priority")]
182 pub priority2: u8,
183 #[serde(default = "default_delay_mechanism")]
184 pub delay_mechanism: String,
185}
186
187impl Default for PtpConfig {
188 fn default() -> Self {
189 Self {
190 enabled: false,
191 domain: 0,
192 interface: default_ptp_interface(),
193 transport: default_ptp_transport(),
194 priority1: default_priority(),
195 priority2: default_priority(),
196 delay_mechanism: default_delay_mechanism(),
197 }
198 }
199}
200
201fn default_ptp_interface() -> String {
202 "eth0".into()
203}
204
205fn default_ptp_transport() -> String {
206 "udp-ipv4".into()
207}
208
209fn default_priority() -> u8 {
210 128
211}
212
213fn default_delay_mechanism() -> String {
214 "e2e".into()
215}
216
217#[derive(Debug, Clone, Deserialize)]
218pub struct MetricsConfig {
219 #[serde(default = "default_true")]
220 pub enabled: bool,
221 #[serde(default = "default_metrics_listen")]
222 pub listen: String,
223}
224
225impl Default for MetricsConfig {
226 fn default() -> Self {
227 Self {
228 enabled: true,
229 listen: default_metrics_listen(),
230 }
231 }
232}
233
234fn default_metrics_listen() -> String {
235 "127.0.0.1:9100".into()
236}
237
238#[derive(Debug, Clone, Deserialize)]
239pub struct ManagementConfig {
240 #[serde(default = "default_true")]
241 pub enabled: bool,
242 #[serde(default = "default_management_listen")]
243 pub listen: String,
244 pub api_key: Option<String>,
248}
249
250impl Default for ManagementConfig {
251 fn default() -> Self {
252 Self {
253 enabled: true,
254 listen: default_management_listen(),
255 api_key: None,
256 }
257 }
258}
259
260fn default_management_listen() -> String {
261 "127.0.0.1:9200".into()
262}
263
264impl RtimeConfig {
265 pub fn validate(&self) -> Result<(), ConfigError> {
268 if !self.clock.step_threshold_ms.is_finite() || self.clock.step_threshold_ms <= 0.0 {
270 return Err(ConfigError::InvalidValue(
271 "clock.step_threshold_ms must be a positive finite number".into(),
272 ));
273 }
274 if !self.clock.panic_threshold_ms.is_finite() || self.clock.panic_threshold_ms <= 0.0 {
275 return Err(ConfigError::InvalidValue(
276 "clock.panic_threshold_ms must be a positive finite number".into(),
277 ));
278 }
279 if self.clock.step_threshold_ms >= self.clock.panic_threshold_ms {
280 return Err(ConfigError::InvalidValue(format!(
281 "clock.step_threshold_ms ({}) must be less than clock.panic_threshold_ms ({})",
282 self.clock.step_threshold_ms, self.clock.panic_threshold_ms
283 )));
284 }
285
286 if !self.ntp.rate_limit.is_finite() || self.ntp.rate_limit <= 0.0 {
288 return Err(ConfigError::InvalidValue(
289 "ntp.rate_limit must be a positive finite number".into(),
290 ));
291 }
292 if self.ntp.rate_burst == 0 {
293 return Err(ConfigError::InvalidValue(
294 "ntp.rate_burst must be greater than 0".into(),
295 ));
296 }
297
298 for (i, source) in self.ntp.sources.iter().enumerate() {
300 if source.min_poll < 0 || source.min_poll > 17 {
301 return Err(ConfigError::InvalidValue(format!(
302 "ntp.sources[{}].min_poll must be between 0 and 17",
303 i
304 )));
305 }
306 if source.max_poll < 0 || source.max_poll > 17 {
307 return Err(ConfigError::InvalidValue(format!(
308 "ntp.sources[{}].max_poll must be between 0 and 17",
309 i
310 )));
311 }
312 if source.min_poll > source.max_poll {
313 return Err(ConfigError::InvalidValue(format!(
314 "ntp.sources[{}].min_poll must be <= max_poll",
315 i
316 )));
317 }
318 }
319
320 self.ntp
322 .listen
323 .parse::<std::net::SocketAddr>()
324 .map_err(|_| {
325 ConfigError::InvalidValue(format!(
326 "invalid ntp.listen address: {}",
327 self.ntp.listen
328 ))
329 })?;
330 if self.metrics.enabled {
331 self.metrics
332 .listen
333 .parse::<std::net::SocketAddr>()
334 .map_err(|_| {
335 ConfigError::InvalidValue(format!(
336 "invalid metrics.listen address: {}",
337 self.metrics.listen
338 ))
339 })?;
340 }
341 if self.management.enabled {
342 self.management
343 .listen
344 .parse::<std::net::SocketAddr>()
345 .map_err(|_| {
346 ConfigError::InvalidValue(format!(
347 "invalid management.listen address: {}",
348 self.management.listen
349 ))
350 })?;
351 }
352 if self.ntp.nts.enabled {
353 self.ntp
354 .nts
355 .ke_listen
356 .parse::<std::net::SocketAddr>()
357 .map_err(|_| {
358 ConfigError::InvalidValue(format!(
359 "invalid nts.ke_listen address: {}",
360 self.ntp.nts.ke_listen
361 ))
362 })?;
363
364 let cert_path = self.ntp.nts.certificate.as_deref().ok_or_else(|| {
366 ConfigError::InvalidValue("nts.certificate is required when NTS is enabled".into())
367 })?;
368 let key_path = self.ntp.nts.private_key.as_deref().ok_or_else(|| {
369 ConfigError::InvalidValue("nts.private_key is required when NTS is enabled".into())
370 })?;
371 for (label, path) in [
372 ("nts.certificate", cert_path),
373 ("nts.private_key", key_path),
374 ] {
375 let meta = std::fs::metadata(path).map_err(|e| {
376 ConfigError::InvalidValue(format!("{} file '{}': {}", label, path, e))
377 })?;
378 if !meta.is_file() {
379 return Err(ConfigError::InvalidValue(format!(
380 "{} path '{}' is not a regular file",
381 label, path
382 )));
383 }
384 #[cfg(unix)]
385 {
386 use std::os::unix::fs::PermissionsExt;
387 let mode = meta.permissions().mode();
388 if mode & 0o004 != 0 {
389 return Err(ConfigError::InvalidValue(format!(
390 "{} file '{}' is world-readable (mode {:o}); tighten permissions",
391 label, path, mode
392 )));
393 }
394 }
395 }
396 }
397
398 if self
400 .management
401 .api_key
402 .as_ref()
403 .is_some_and(|k| k.trim().is_empty())
404 {
405 return Err(ConfigError::InvalidValue(
406 "management.api_key must not be empty or whitespace-only".into(),
407 ));
408 }
409
410 let mgmt_addr: std::net::SocketAddr = self
412 .management
413 .listen
414 .parse()
415 .unwrap_or_else(|_| "127.0.0.1:9200".parse().unwrap());
416 if self.management.enabled
417 && !mgmt_addr.ip().is_loopback()
418 && self.management.api_key.is_none()
419 {
420 return Err(ConfigError::InvalidValue(format!(
421 "management API on non-loopback address {} requires api_key to be set",
422 self.management.listen
423 )));
424 }
425
426 let metrics_addr: std::net::SocketAddr = self
427 .metrics
428 .listen
429 .parse()
430 .unwrap_or_else(|_| "127.0.0.1:9100".parse().unwrap());
431 if self.metrics.enabled && !metrics_addr.ip().is_loopback() {
432 return Err(ConfigError::InvalidValue(format!(
433 "metrics endpoint on non-loopback address {} is not allowed (bind to loopback or use a reverse proxy)",
434 self.metrics.listen
435 )));
436 }
437
438 Ok(())
439 }
440}
441
442#[derive(Debug, thiserror::Error)]
444pub enum ConfigError {
445 #[error("invalid config value: {0}")]
446 InvalidValue(String),
447}