1pub mod dynamic;
2pub mod format;
3mod peer;
4mod server;
5pub mod subnet;
6
7use ntp_os_clock::DefaultNtpClock;
8use ntp_udp::{EnableTimestamps, InterfaceName};
9pub use peer::*;
10pub use server::*;
11
12use clap::Parser;
13use ntp_proto::{DefaultTimeSyncController, SystemConfig, TimeSyncController};
14use serde::{de, Deserialize, Deserializer};
15use std::{
16 io::ErrorKind,
17 os::unix::fs::PermissionsExt,
18 path::{Path, PathBuf},
19 sync::Arc,
20};
21use thiserror::Error;
22use tokio::{fs::read_to_string, io};
23use tracing::{info, warn};
24use tracing_subscriber::filter::EnvFilter;
25
26use crate::spawn::PeerId;
27
28use self::format::LogFormat;
29
30fn deserialize_option_env_filter<'de, D>(deserializer: D) -> Result<Option<EnvFilter>, D::Error>
31where
32 D: Deserializer<'de>,
33{
34 let data: Option<String> = Deserialize::deserialize(deserializer)?;
35
36 if let Some(dirs) = data {
37 if dirs.is_empty() {
39 Ok(None)
40 } else {
41 Ok(Some(EnvFilter::try_new(dirs).map_err(de::Error::custom)?))
42 }
43 } else {
44 Ok(None)
45 }
46}
47
48fn parse_env_filter(input: &str) -> Result<Arc<EnvFilter>, tracing_subscriber::filter::ParseError> {
49 EnvFilter::builder()
50 .with_regex(false)
51 .parse(input)
52 .map(Arc::new)
53}
54
55#[derive(Parser, Debug)]
56pub struct CmdArgs {
57 #[arg(
58 short,
59 long = "peer",
60 global = true,
61 value_name = "SERVER",
62 value_parser = PeerConfig::try_from_str,
63 help = "Override the peers in the configuration file"
64 )]
65 pub peers: Vec<PeerConfig>,
66
67 #[arg(
68 short,
69 long,
70 global = true,
71 value_name = "FILE",
72 help = "Path of the configuration file"
73 )]
74 pub config: Option<PathBuf>,
75
76 #[arg(
77 long,
78 short,
79 global = true,
80 value_name = "FILTER",
81 value_parser = parse_env_filter,
82 env = "NTP_LOG",
83 help = "Filter to apply to log messages"
84 )]
85 pub log_filter: Option<Arc<EnvFilter>>,
86
87 #[arg(
88 long,
89 global = true,
90 value_name = "FORMAT",
91 env = "NTP_LOG_FORMAT",
92 help = "Output format for logs (full, compact, pretty, json)"
93 )]
94 pub log_format: Option<LogFormat>,
95
96 #[arg(
97 short,
98 long = "server",
99 global = true,
100 value_name = "ADDR",
101 value_parser = ServerConfig::try_from_str,
102 help = "Override the servers to run from the configuration file"
103 )]
104 pub servers: Vec<ServerConfig>,
105}
106
107fn deserialize_ntp_clock<'de, D>(deserializer: D) -> Result<DefaultNtpClock, D::Error>
108where
109 D: Deserializer<'de>,
110{
111 let data: Option<PathBuf> = Deserialize::deserialize(deserializer)?;
112
113 if let Some(path) = data {
114 tracing::info!("using custom clock {path:?}");
115 DefaultNtpClock::from_path(&path).map_err(|e| serde::de::Error::custom(e.to_string()))
116 } else {
117 tracing::debug!("using REALTIME clock");
118 Ok(DefaultNtpClock::realtime())
119 }
120}
121
122fn deserialize_interface<'de, D>(deserializer: D) -> Result<Option<InterfaceName>, D::Error>
123where
124 D: Deserializer<'de>,
125{
126 let opt_interface_name: Option<InterfaceName> = Deserialize::deserialize(deserializer)?;
127
128 if let Some(interface_name) = opt_interface_name {
129 tracing::info!("using custom interface {}", interface_name);
130 } else {
131 tracing::info!("using default interface");
132 }
133
134 Ok(opt_interface_name)
135}
136
137#[derive(Deserialize, Debug, Copy, Clone, Default)]
138#[serde(rename_all = "kebab-case")]
139pub struct ClockConfig {
140 #[serde(deserialize_with = "deserialize_ntp_clock", default)]
141 pub clock: DefaultNtpClock,
142 #[serde(deserialize_with = "deserialize_interface", default)]
143 pub interface: Option<InterfaceName>,
144 pub enable_timestamps: EnableTimestamps,
145}
146
147#[derive(Deserialize, Debug, Default, Clone, Copy)]
148#[serde(rename_all = "kebab-case", deny_unknown_fields)]
149pub struct CombinedSystemConfig {
150 #[serde(flatten)]
151 pub system: SystemConfig,
152 #[serde(flatten)]
153 pub algorithm: <DefaultTimeSyncController<DefaultNtpClock, PeerId> as TimeSyncController<
154 DefaultNtpClock,
155 PeerId,
156 >>::AlgorithmConfig,
157}
158
159#[derive(Deserialize, Debug, Default)]
160#[serde(rename_all = "kebab-case", deny_unknown_fields)]
161pub struct Config {
162 #[serde(alias = "peer")]
163 pub peers: Vec<PeerConfig>,
164 #[serde(alias = "server", default)]
165 pub servers: Vec<ServerConfig>,
166 #[serde(alias = "nts-ke-server", default)]
167 pub nts_ke: Option<NtsKeConfig>,
168 #[serde(default)]
169 pub system: CombinedSystemConfig,
170 #[serde(deserialize_with = "deserialize_option_env_filter", default)]
171 pub log_filter: Option<EnvFilter>,
172 #[serde(default)]
173 pub log_format: LogFormat,
174 #[serde(default)]
175 pub observe: ObserveConfig,
176 #[serde(default)]
177 pub configure: ConfigureConfig,
178 #[serde(default)]
179 pub keyset: KeysetConfig,
180 #[serde(default)]
181 pub clock: ClockConfig,
182}
183
184const fn default_observe_permissions() -> u32 {
185 0o666
186}
187
188#[derive(Clone, Deserialize, Debug)]
189#[serde(rename_all = "kebab-case", deny_unknown_fields)]
190pub struct ObserveConfig {
191 #[serde(default)]
192 pub path: Option<PathBuf>,
193 #[serde(default = "default_observe_permissions")]
194 pub mode: u32,
195}
196
197const fn default_configure_permissions() -> u32 {
198 0o660
199}
200
201impl Default for ObserveConfig {
202 fn default() -> Self {
203 Self {
204 path: None,
205 mode: default_observe_permissions(),
206 }
207 }
208}
209
210#[derive(Clone, Deserialize, Debug)]
211#[serde(rename_all = "kebab-case", deny_unknown_fields)]
212pub struct ConfigureConfig {
213 #[serde(default)]
214 pub path: Option<std::path::PathBuf>,
215 #[serde(default = "default_configure_permissions")]
216 pub mode: u32,
217}
218
219impl Default for ConfigureConfig {
220 fn default() -> Self {
221 Self {
222 path: None,
223 mode: default_configure_permissions(),
224 }
225 }
226}
227
228#[derive(Error, Debug)]
229pub enum ConfigError {
230 #[error("io error while reading config: {0}")]
231 Io(#[from] io::Error),
232 #[error("config toml parsing error: {0}")]
233 Toml(#[from] toml::de::Error),
234}
235
236impl Config {
237 async fn from_file(file: impl AsRef<Path>) -> Result<Config, ConfigError> {
238 let meta = std::fs::metadata(&file).unwrap();
239 let perm = meta.permissions();
240
241 if perm.mode() as libc::mode_t & libc::S_IWOTH != 0 {
242 warn!("Unrestricted config file permissions: Others can write.");
243 }
244
245 let contents = read_to_string(file).await?;
246 Ok(toml::de::from_str(&contents)?)
247 }
248
249 async fn from_first_file(file: Option<impl AsRef<Path>>) -> Result<Config, ConfigError> {
250 if let Some(f) = file {
252 let path: &Path = f.as_ref();
253 info!(?path, "using config file");
254 return Config::from_file(f).await;
255 }
256
257 let global_path = Path::new("/etc/ntpd-rs/ntp.toml");
259 if global_path.exists() {
260 info!("using config file at default location `{:?}`", global_path);
261 match Config::from_file(global_path).await {
262 Err(ConfigError::Io(e)) if e.kind() == ErrorKind::PermissionDenied => {
263 info!("permission denied on global config file! using default config ...");
264 }
265 other => {
266 return other;
267 }
268 }
269 }
270
271 Ok(Config::default())
272 }
273
274 pub async fn from_args(
275 file: Option<impl AsRef<Path>>,
276 peers: Vec<PeerConfig>,
277 servers: Vec<ServerConfig>,
278 ) -> Result<Config, ConfigError> {
279 let mut config = Config::from_first_file(file).await?;
280
281 if !peers.is_empty() {
282 if !config.peers.is_empty() {
283 info!("overriding peers from configuration");
284 }
285 config.peers = peers;
286 }
287
288 if !servers.is_empty() {
289 if !config.servers.is_empty() {
290 info!("overriding servers from configuration");
291 }
292 config.servers = servers;
293 }
294
295 Ok(config)
296 }
297
298 fn count_peers(&self) -> usize {
300 let mut count = 0;
301 for peer in &self.peers {
302 match peer {
303 PeerConfig::Standard(_) => count += 1,
304 PeerConfig::Nts(_) => count += 1,
305 PeerConfig::Pool(config) => count += config.max_peers,
306 }
307 }
308 count
309 }
310
311 pub fn check(&self) -> bool {
314 let mut ok = true;
315
316 if self.peers.is_empty() {
321 warn!("No peers configured. Daemon will not change system time.");
322 ok = false;
323 }
324
325 if self.count_peers() < self.system.system.min_intersection_survivors {
326 warn!("Fewer peers configured than are required to agree on the current time. Daemon will not change system time.");
327 ok = false;
328 }
329
330 ok
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use std::{ffi::OsString, str::FromStr};
337
338 use ntp_proto::{NtpDuration, StepThreshold};
339
340 use super::*;
341
342 #[test]
343 fn test_config() {
344 let config: Config = toml::from_str("[[peers]]\naddr = \"example.com\"").unwrap();
345 assert_eq!(
346 config.peers,
347 vec![PeerConfig::Standard(StandardPeerConfig {
348 addr: NormalizedAddress::new_unchecked("example.com", 123),
349 })]
350 );
351
352 let config: Config =
353 toml::from_str("log-filter = \"\"\n[[peers]]\naddr = \"example.com\"").unwrap();
354 assert!(config.log_filter.is_none());
355 assert_eq!(
356 config.peers,
357 vec![PeerConfig::Standard(StandardPeerConfig {
358 addr: NormalizedAddress::new_unchecked("example.com", 123),
359 })]
360 );
361
362 let config: Config =
363 toml::from_str("log-filter = \"info\"\n[[peers]]\naddr = \"example.com\"").unwrap();
364 assert!(config.log_filter.is_some());
365 assert_eq!(
366 config.peers,
367 vec![PeerConfig::Standard(StandardPeerConfig {
368 addr: NormalizedAddress::new_unchecked("example.com", 123),
369 })]
370 );
371
372 let config: Config =
373 toml::from_str("[[peers]]\naddr = \"example.com\"\n[system]\npanic-threshold = 0")
374 .unwrap();
375 assert_eq!(
376 config.peers,
377 vec![PeerConfig::Standard(StandardPeerConfig {
378 addr: NormalizedAddress::new_unchecked("example.com", 123),
379 })]
380 );
381 assert_eq!(
382 config.system.system.panic_threshold.forward,
383 Some(NtpDuration::from_seconds(0.))
384 );
385 assert_eq!(
386 config.system.system.panic_threshold.backward,
387 Some(NtpDuration::from_seconds(0.))
388 );
389
390 let config: Config = toml::from_str(
391 "[[peers]]\naddr = \"example.com\"\n[system]\npanic-threshold = \"inf\"",
392 )
393 .unwrap();
394 assert_eq!(
395 config.peers,
396 vec![PeerConfig::Standard(StandardPeerConfig {
397 addr: NormalizedAddress::new_unchecked("example.com", 123),
398 })]
399 );
400 assert!(config.system.system.panic_threshold.forward.is_none());
401 assert!(config.system.system.panic_threshold.backward.is_none());
402
403 let config: Config = toml::from_str(
404 r#"
405 log-filter = "info"
406 log-format = "full"
407 [[peers]]
408 addr = "example.com"
409 [observe]
410 path = "/foo/bar/observe"
411 mode = 0o567
412 [configure]
413 path = "/foo/bar/configure"
414 mode = 0o123
415 "#,
416 )
417 .unwrap();
418 assert!(config.log_filter.is_some());
419
420 assert_eq!(config.observe.path, Some(PathBuf::from("/foo/bar/observe")));
421 assert_eq!(config.observe.mode, 0o567);
422
423 assert_eq!(
424 config.configure.path,
425 Some(PathBuf::from("/foo/bar/configure"))
426 );
427 assert_eq!(config.configure.mode, 0o123);
428
429 assert_eq!(
430 config.peers,
431 vec![PeerConfig::Standard(StandardPeerConfig {
432 addr: NormalizedAddress::new_unchecked("example.com", 123),
433 })]
434 );
435 }
436
437 #[test]
438 fn clap_no_arguments() {
439 use clap::Parser;
440
441 let arguments: [OsString; 0] = [];
442 let parsed_empty = CmdArgs::try_parse_from(arguments).unwrap();
443
444 assert!(parsed_empty.peers.is_empty());
445 assert!(parsed_empty.config.is_none());
446 assert!(parsed_empty.log_filter.is_none());
447 }
448
449 #[test]
450 fn clap_external_config() {
451 use clap::Parser;
452
453 let arguments = &["--", "--config", "other.toml"];
454 let parsed_empty = CmdArgs::try_parse_from(arguments).unwrap();
455
456 assert!(parsed_empty.peers.is_empty());
457 assert_eq!(parsed_empty.config, Some("other.toml".into()));
458 assert!(parsed_empty.log_filter.is_none());
459
460 let arguments = &["--", "-c", "other.toml"];
461 let parsed_empty = CmdArgs::try_parse_from(arguments).unwrap();
462
463 assert!(parsed_empty.peers.is_empty());
464 assert_eq!(parsed_empty.config, Some("other.toml".into()));
465 assert!(parsed_empty.log_filter.is_none());
466 }
467
468 #[test]
469 fn clap_log_filter() {
470 use clap::Parser;
471
472 let arguments = &["--", "--log-filter", "debug"];
473 let parsed_empty = CmdArgs::try_parse_from(arguments).unwrap();
474
475 assert!(parsed_empty.peers.is_empty());
476 assert!(parsed_empty.config.is_none());
477 assert_eq!(parsed_empty.log_filter.unwrap().to_string(), "debug");
478
479 let arguments = &["--", "-l", "debug"];
480 let parsed_empty = CmdArgs::try_parse_from(arguments).unwrap();
481
482 assert!(parsed_empty.peers.is_empty());
483 assert!(parsed_empty.config.is_none());
484 assert_eq!(parsed_empty.log_filter.unwrap().to_string(), "debug");
485 }
486
487 #[test]
488 fn clap_peers() {
489 use clap::Parser;
490
491 let arguments = &["--", "--peer", "foo.nl"];
492 let parsed_empty = CmdArgs::try_parse_from(arguments).unwrap();
493
494 assert_eq!(
495 parsed_empty.peers,
496 vec![PeerConfig::Standard(StandardPeerConfig {
497 addr: NormalizedAddress::new_unchecked("foo.nl", 123),
498 })]
499 );
500 assert!(parsed_empty.config.is_none());
501 assert!(parsed_empty.log_filter.is_none());
502
503 let arguments = &["--", "--peer", "foo.rs", "-p", "spam.nl:123"];
504 let parsed_empty = CmdArgs::try_parse_from(arguments).unwrap();
505
506 assert_eq!(
507 parsed_empty.peers,
508 vec![
509 PeerConfig::Standard(StandardPeerConfig {
510 addr: NormalizedAddress::new_unchecked("foo.rs", 123),
511 }),
512 PeerConfig::Standard(StandardPeerConfig {
513 addr: NormalizedAddress::new_unchecked("spam.nl", 123),
514 }),
515 ]
516 );
517 assert!(parsed_empty.config.is_none());
518 assert!(parsed_empty.log_filter.is_none());
519 }
520
521 #[test]
522 fn clap_peers_invalid() {
523 let arguments = &["--", "--peer", ":invalid:ipv6:123"];
524 assert!(CmdArgs::try_parse_from(arguments).is_err());
525 }
526
527 #[test]
528 fn toml_peers_invalid() {
529 let config: Result<Config, _> = toml::from_str(
530 r#"
531 [[peers]]
532 addr = ":invalid:ipv6:123"
533 "#,
534 );
535
536 assert!(config.is_err());
537 }
538
539 #[test]
540 fn system_config_accumulated_threshold() {
541 let config: Result<SystemConfig, _> = toml::from_str(
542 r#"
543 accumulated-threshold = 0
544 "#,
545 );
546
547 let config = config.unwrap();
548 assert!(config.accumulated_threshold.is_none());
549
550 let config: Result<SystemConfig, _> = toml::from_str(
551 r#"
552 accumulated-threshold = 1000
553 "#,
554 );
555
556 let config = config.unwrap();
557 assert_eq!(
558 config.accumulated_threshold,
559 Some(NtpDuration::from_seconds(1000.0))
560 );
561 }
562
563 #[test]
564 fn system_config_startup_panic_threshold() {
565 let config: Result<SystemConfig, _> = toml::from_str(
566 r#"
567 startup-panic-threshold = { forward = 10, backward = 20 }
568 "#,
569 );
570
571 let config = config.unwrap();
572 assert_eq!(
573 config.startup_panic_threshold.forward,
574 Some(NtpDuration::from_seconds(10.0))
575 );
576 assert_eq!(
577 config.startup_panic_threshold.backward,
578 Some(NtpDuration::from_seconds(20.0))
579 );
580 }
581
582 #[test]
583 fn duration_not_nan() {
584 #[derive(Debug, Deserialize)]
585 struct Helper {
586 #[allow(unused)]
587 duration: NtpDuration,
588 }
589
590 let result: Result<Helper, _> = toml::from_str(
591 r#"
592 duration = nan
593 "#,
594 );
595
596 let error = result.unwrap_err();
597 assert!(error.to_string().contains("expected a valid number"));
598 }
599
600 #[test]
601 fn step_threshold_not_nan() {
602 #[derive(Debug, Deserialize)]
603 struct Helper {
604 #[allow(unused)]
605 threshold: StepThreshold,
606 }
607
608 let result: Result<Helper, _> = toml::from_str(
609 r#"
610 threshold = nan
611 "#,
612 );
613
614 let error = result.unwrap_err();
615 assert!(error.to_string().contains("expected a positive number"));
616 }
617
618 #[test]
619 fn deny_unknown_fields() {
620 let config: Result<SystemConfig, _> = toml::from_str(
621 r#"
622 unknown-field = 42
623 "#,
624 );
625
626 let error = config.unwrap_err();
627 assert!(error.to_string().contains("unknown field"));
628 }
629
630 #[test]
631 fn clock_config() {
632 let config: Result<ClockConfig, _> = toml::from_str(
633 r#"
634 interface = "enp0s31f6"
635 enable-timestamps.rx-hardware = true
636 enable-timestamps.tx-software = true
637 "#,
638 );
639
640 let config = config.unwrap();
641
642 let expected = InterfaceName::from_str("enp0s31f6").unwrap();
643 assert_eq!(config.interface, Some(expected));
644
645 assert!(config.enable_timestamps.rx_software);
646 assert!(config.enable_timestamps.tx_software);
647 }
648}