diff --git a/docs/man/ntp.toml.5.md b/docs/man/ntp.toml.5.md index 19b8ac513..5e91e742f 100644 --- a/docs/man/ntp.toml.5.md +++ b/docs/man/ntp.toml.5.md @@ -45,20 +45,20 @@ with any of these options: # CONFIGURATION ## `[source-defaults]` -Some values are shared between all sources in the daemon. You can configure -these in the `[source-defaults]` section. +Some of the behavior of a source is configurable. You can set defaults for those +settings in the `[source-defaults]` section. `poll-interval-limits` = { `min` = *min*, `max` = *max* } (**{ min = 4, max = 10}**) : Specifies the limit on how often a source is queried for a new time. For most instances the defaults will be adequate. The min and max are given as the log2 of the number of seconds (i.e. two to the power of the interval). - An interval of 4 equates to 32 seconds, 10 results in an interval of 1024 + An interval of 4 equates to 16 seconds, 10 results in an interval of 1024 seconds. If specified, both min and max must be specified. `initial-poll-interval` = *interval* (**4**) : Initial poll interval used on startup. The value is given as the log2 of the number of seconds (i.e. two to the power of the interval). The default - value of 4 results in an interval of 32 seconds. + value of 4 results in an interval of 16 seconds. ## `[[source]]` Each `[[source]]` is a set of one or more time sources for the daemon to @@ -101,6 +101,19 @@ sources. : `pool` mode only. Specifies a list of IP addresses of servers in the pool which should not be used. For example: `["127.0.0.1"]`. Empty by default. +`poll-interval-limits` = { `min` = *min*, `max` = *max* } (defaults from `[source-defaults]`) +: Specifies the limit on how often a source is queried for a new time. For + most instances the defaults will be adequate. The min and max are given as + the log2 of the number of seconds (i.e. two to the power of the interval). + An interval of 4 equates to 16 seconds, 10 results in an interval of 1024 + seconds. If only one of the two boundaries is specified, the other is + inherited from `[source-defaults]` + +`initial-poll-interval` = *interval* (defaults from `[source-defaults]`) +: Initial poll interval used on startup. The value is given as the log2 of + the number of seconds (i.e. two to the power of the interval). The default + value of 4 results in an interval of 16 seconds. + ## `[[server]]` The NTP daemon can be configured to distribute time via any number of `[[server]]` sections. If no such sections have been defined, the daemon runs in diff --git a/docs/precompiled/man/ntp.toml.5 b/docs/precompiled/man/ntp.toml.5 index d38e7e4d5..691534910 100644 --- a/docs/precompiled/man/ntp.toml.5 +++ b/docs/precompiled/man/ntp.toml.5 @@ -64,15 +64,16 @@ a rough idea of the current time. .SH CONFIGURATION .SS \f[V][source-defaults]\f[R] .PP -Some values are shared between all sources in the daemon. -You can configure these in the \f[V][source-defaults]\f[R] section. +Some of the behavior of a source is configurable. +You can set defaults for those settings in the +\f[V][source-defaults]\f[R] section. .TP \f[V]poll-interval-limits\f[R] = { \f[V]min\f[R] = \f[I]min\f[R], \f[V]max\f[R] = \f[I]max\f[R] } (\f[B]{ min = 4, max = 10}\f[R]) Specifies the limit on how often a source is queried for a new time. For most instances the defaults will be adequate. The min and max are given as the log2 of the number of seconds (i.e.\ two to the power of the interval). -An interval of 4 equates to 32 seconds, 10 results in an interval of +An interval of 4 equates to 16 seconds, 10 results in an interval of 1024 seconds. If specified, both min and max must be specified. .TP @@ -80,7 +81,7 @@ If specified, both min and max must be specified. Initial poll interval used on startup. The value is given as the log2 of the number of seconds (i.e.\ two to the power of the interval). -The default value of 4 results in an interval of 32 seconds. +The default value of 4 results in an interval of 16 seconds. .SS \f[V][[source]]\f[R] .PP Each \f[V][[source]]\f[R] is a set of one or more time sources for the @@ -132,6 +133,22 @@ Specifies a list of IP addresses of servers in the pool which should not be used. For example: \f[V][\[dq]127.0.0.1\[dq]]\f[R]. Empty by default. +.TP +\f[V]poll-interval-limits\f[R] = { \f[V]min\f[R] = \f[I]min\f[R], \f[V]max\f[R] = \f[I]max\f[R] } (defaults from \f[V][source-defaults]\f[R]) +Specifies the limit on how often a source is queried for a new time. +For most instances the defaults will be adequate. +The min and max are given as the log2 of the number of seconds +(i.e.\ two to the power of the interval). +An interval of 4 equates to 16 seconds, 10 results in an interval of +1024 seconds. +If only one of the two boundaries is specified, the other is inherited +from \f[V][source-defaults]\f[R] +.TP +\f[V]initial-poll-interval\f[R] = \f[I]interval\f[R] (defaults from \f[V][source-defaults]\f[R]) +Initial poll interval used on startup. +The value is given as the log2 of the number of seconds (i.e.\ two to +the power of the interval). +The default value of 4 results in an interval of 16 seconds. .SS \f[V][[server]]\f[R] .PP The NTP daemon can be configured to distribute time via any number of diff --git a/ntpd/src/daemon/config/mod.rs b/ntpd/src/daemon/config/mod.rs index fef163ea3..930d5721e 100644 --- a/ntpd/src/daemon/config/mod.rs +++ b/ntpd/src/daemon/config/mod.rs @@ -441,9 +441,9 @@ impl Config { match source { NtpSourceConfig::Standard(_) => count += 1, NtpSourceConfig::Nts(_) => count += 1, - NtpSourceConfig::Pool(config) => count += config.count, + NtpSourceConfig::Pool(config) => count += config.first.count, #[cfg(feature = "unstable_nts-pool")] - NtpSourceConfig::NtsPool(config) => count += config.count, + NtpSourceConfig::NtsPool(config) => count += config.first.count, NtpSourceConfig::Sock(_) => count += 1, } } @@ -477,11 +477,19 @@ impl Config { #[cfg(feature = "unstable_ntpv5")] if self.sources.iter().any(|config| match config { NtpSourceConfig::Sock(_) => false, - NtpSourceConfig::Standard(config) => matches!(config.ntp_version, Some(NtpVersion::V5)), - NtpSourceConfig::Nts(config) => matches!(config.ntp_version, Some(NtpVersion::V5)), - NtpSourceConfig::Pool(config) => matches!(config.ntp_version, Some(NtpVersion::V5)), + NtpSourceConfig::Standard(config) => { + matches!(config.first.ntp_version, Some(NtpVersion::V5)) + } + NtpSourceConfig::Nts(config) => { + matches!(config.first.ntp_version, Some(NtpVersion::V5)) + } + NtpSourceConfig::Pool(config) => { + matches!(config.first.ntp_version, Some(NtpVersion::V5)) + } #[cfg(feature = "unstable_nts-pool")] - NtpSourceConfig::NtsPool(config) => matches!(config.ntp_version, Some(NtpVersion::V5)), + NtpSourceConfig::NtsPool(config) => { + matches!(config.first.ntp_version, Some(NtpVersion::V5)) + } }) { warn!("Forcing a source into NTPv5, which is still a draft. There is no guarantee that the server will remain compatible with this or future versions of ntpd-rs."); ok = false; @@ -532,10 +540,13 @@ mod tests { toml::from_str("[[source]]\nmode = \"server\"\naddress = \"example.com\"").unwrap(); assert_eq!( config.sources, - vec![NtpSourceConfig::Standard(StandardSource { - address: NormalizedAddress::new_unchecked("example.com", 123).into(), - #[cfg(feature = "unstable_ntpv5")] - ntp_version: None, + vec![NtpSourceConfig::Standard(FlattenedPair { + first: StandardSource { + address: NormalizedAddress::new_unchecked("example.com", 123).into(), + #[cfg(feature = "unstable_ntpv5")] + ntp_version: None, + }, + second: Default::default() })] ); assert!(config.observability.log_level.is_none()); @@ -547,10 +558,13 @@ mod tests { assert_eq!(config.observability.log_level, Some(LogLevel::Info)); assert_eq!( config.sources, - vec![NtpSourceConfig::Standard(StandardSource { - address: NormalizedAddress::new_unchecked("example.com", 123).into(), - #[cfg(feature = "unstable_ntpv5")] - ntp_version: None, + vec![NtpSourceConfig::Standard(FlattenedPair { + first: StandardSource { + address: NormalizedAddress::new_unchecked("example.com", 123).into(), + #[cfg(feature = "unstable_ntpv5")] + ntp_version: None, + }, + second: Default::default() })] ); @@ -560,10 +574,13 @@ mod tests { .unwrap(); assert_eq!( config.sources, - vec![NtpSourceConfig::Standard(StandardSource { - address: NormalizedAddress::new_unchecked("example.com", 123).into(), - #[cfg(feature = "unstable_ntpv5")] - ntp_version: None, + vec![NtpSourceConfig::Standard(FlattenedPair { + first: StandardSource { + address: NormalizedAddress::new_unchecked("example.com", 123).into(), + #[cfg(feature = "unstable_ntpv5")] + ntp_version: None, + }, + second: Default::default() })] ); assert_eq!( @@ -589,10 +606,13 @@ mod tests { .unwrap(); assert_eq!( config.sources, - vec![NtpSourceConfig::Standard(StandardSource { - address: NormalizedAddress::new_unchecked("example.com", 123).into(), - #[cfg(feature = "unstable_ntpv5")] - ntp_version: None, + vec![NtpSourceConfig::Standard(FlattenedPair { + first: StandardSource { + address: NormalizedAddress::new_unchecked("example.com", 123).into(), + #[cfg(feature = "unstable_ntpv5")] + ntp_version: None, + }, + second: Default::default() })] ); assert!(config @@ -633,10 +653,13 @@ mod tests { assert_eq!( config.sources, - vec![NtpSourceConfig::Standard(StandardSource { - address: NormalizedAddress::new_unchecked("example.com", 123).into(), - #[cfg(feature = "unstable_ntpv5")] - ntp_version: None, + vec![NtpSourceConfig::Standard(FlattenedPair { + first: StandardSource { + address: NormalizedAddress::new_unchecked("example.com", 123).into(), + #[cfg(feature = "unstable_ntpv5")] + ntp_version: None, + }, + second: Default::default() })] ); diff --git a/ntpd/src/daemon/config/ntp_source.rs b/ntpd/src/daemon/config/ntp_source.rs index d7f7eb271..8c151a1fd 100644 --- a/ntpd/src/daemon/config/ntp_source.rs +++ b/ntpd/src/daemon/config/ntp_source.rs @@ -7,9 +7,9 @@ use std::{ }; use ntp_proto::tls_utils::Certificate; -use ntp_proto::NtpDuration; #[cfg(feature = "unstable_ntpv5")] use ntp_proto::NtpVersion; +use ntp_proto::{NtpDuration, PollInterval, PollIntervalLimits, SourceConfig}; use serde::{de, Deserialize, Deserializer}; use super::super::keyexchange::certificates_from_file; @@ -117,18 +117,65 @@ pub struct SockSourceConfig { pub measurement_noise_estimate: NtpDuration, } +#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)] +#[serde(deny_unknown_fields)] +pub struct PartialPollIntervalLimits { + pub min: Option, + pub max: Option, +} + +#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct PartialSourceConfig { + /// Minima and maxima for the poll interval of clients + #[serde(default)] + pub poll_interval_limits: PartialPollIntervalLimits, + + /// Initial poll interval of the system + pub initial_poll_interval: Option, +} + +impl PartialSourceConfig { + pub fn with_defaults(self, defaults: SourceConfig) -> SourceConfig { + SourceConfig { + poll_interval_limits: PollIntervalLimits { + min: self + .poll_interval_limits + .min + .unwrap_or(defaults.poll_interval_limits.min), + max: self + .poll_interval_limits + .max + .unwrap_or(defaults.poll_interval_limits.max), + }, + initial_poll_interval: self + .initial_poll_interval + .unwrap_or(defaults.initial_poll_interval), + } + } +} + +#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)] +#[serde(deny_unknown_fields)] +pub struct FlattenedPair { + #[serde(flatten)] + pub first: T, + #[serde(flatten)] + pub second: U, +} + #[derive(Debug, Deserialize, PartialEq, Eq, Clone)] #[serde(tag = "mode")] pub enum NtpSourceConfig { #[serde(rename = "server")] - Standard(StandardSource), + Standard(FlattenedPair), #[serde(rename = "nts")] - Nts(NtsSourceConfig), + Nts(FlattenedPair), #[serde(rename = "pool")] - Pool(PoolSourceConfig), + Pool(FlattenedPair), #[cfg(feature = "unstable_nts-pool")] #[serde(rename = "nts-pool")] - NtsPool(NtsPoolSourceConfig), + NtsPool(FlattenedPair), #[serde(rename = "sock")] Sock(SockSourceConfig), } @@ -359,7 +406,12 @@ impl<'a> TryFrom<&'a str> for NtpSourceConfig { type Error = std::io::Error; fn try_from(value: &'a str) -> Result { - StandardSource::try_from(value).map(Self::Standard) + StandardSource::try_from(value).map(|first| { + Self::Standard(FlattenedPair { + first, + second: Default::default(), + }) + }) } } @@ -369,11 +421,11 @@ mod tests { fn source_addr(config: &NtpSourceConfig) -> String { match config { - NtpSourceConfig::Standard(c) => c.address.to_string(), - NtpSourceConfig::Nts(c) => c.address.to_string(), - NtpSourceConfig::Pool(c) => c.addr.to_string(), + NtpSourceConfig::Standard(c) => c.first.address.to_string(), + NtpSourceConfig::Nts(c) => c.first.address.to_string(), + NtpSourceConfig::Pool(c) => c.first.addr.to_string(), #[cfg(feature = "unstable_nts-pool")] - NtpSourceConfig::NtsPool(c) => c.addr.to_string(), + NtpSourceConfig::NtsPool(c) => c.first.addr.to_string(), NtpSourceConfig::Sock(_c) => "".to_string(), } } @@ -429,7 +481,7 @@ mod tests { assert!(matches!(test.source, NtpSourceConfig::Pool(_))); assert_eq!(source_addr(&test.source), "example.com:123"); if let NtpSourceConfig::Pool(config) = test.source { - assert_eq!(config.count, 4); + assert_eq!(config.first.count, 4); } let test: TestConfig = toml::from_str( @@ -444,7 +496,7 @@ mod tests { assert!(matches!(test.source, NtpSourceConfig::Pool(_))); assert_eq!(source_addr(&test.source), "example.com:123"); if let NtpSourceConfig::Pool(config) = test.source { - assert_eq!(config.count, 42); + assert_eq!(config.first.count, 42); } let test: TestConfig = toml::from_str( @@ -509,6 +561,35 @@ mod tests { assert!(matches!(source, NtpSourceConfig::Standard(_))); } + #[test] + fn test_source_config_parsing() { + #[derive(Deserialize, Debug)] + struct TestConfig { + #[allow(unused)] + source: NtpSourceConfig, + } + + let test: Result = toml::from_str( + r#" + [source] + mode = "server" + address = "example.com" + initial-poll-interval = 7 + "#, + ); + assert!(test.is_ok()); + + let test2: Result = toml::from_str( + r#" + [source] + mode = "server" + address = "example.com" + does-not-exist = 7 + "#, + ); + assert!(test2.is_err()); + } + #[test] fn test_normalize_addr() { let addr = NormalizedAddress::from_string_ntp("[::1]:456".into()).unwrap(); diff --git a/ntpd/src/daemon/system.rs b/ntpd/src/daemon/system.rs index ddc68ef31..f22d2b9cc 100644 --- a/ntpd/src/daemon/system.rs +++ b/ntpd/src/daemon/system.rs @@ -114,7 +114,10 @@ pub async fn spawn { system - .add_spawner(StandardSpawner::new(cfg.clone(), source_defaults_config)) + .add_spawner(StandardSpawner::new( + cfg.first.clone(), + cfg.second.clone().with_defaults(source_defaults_config), + )) .map_err(|e| { tracing::error!("Could not spawn source: {}", e); std::io::Error::new(std::io::ErrorKind::Other, e) @@ -122,7 +125,10 @@ pub async fn spawn { system - .add_spawner(NtsSpawner::new(cfg.clone(), source_defaults_config)) + .add_spawner(NtsSpawner::new( + cfg.first.clone(), + cfg.second.clone().with_defaults(source_defaults_config), + )) .map_err(|e| { tracing::error!("Could not spawn source: {}", e); std::io::Error::new(std::io::ErrorKind::Other, e) @@ -130,7 +136,10 @@ pub async fn spawn { system - .add_spawner(PoolSpawner::new(cfg.clone(), source_defaults_config)) + .add_spawner(PoolSpawner::new( + cfg.first.clone(), + cfg.second.clone().with_defaults(source_defaults_config), + )) .map_err(|e| { tracing::error!("Could not spawn source: {}", e); std::io::Error::new(std::io::ErrorKind::Other, e) @@ -139,7 +148,10 @@ pub async fn spawn { system - .add_spawner(NtsPoolSpawner::new(cfg.clone(), source_defaults_config)) + .add_spawner(NtsPoolSpawner::new( + cfg.first.clone(), + cfg.second.clone().with_defaults(source_defaults_config), + )) .map_err(|e| { tracing::error!("Could not spawn source: {}", e); std::io::Error::new(std::io::ErrorKind::Other, e) diff --git a/ntpd/src/force_sync/mod.rs b/ntpd/src/force_sync/mod.rs index cff0abb0b..4aa55d243 100644 --- a/ntpd/src/force_sync/mod.rs +++ b/ntpd/src/force_sync/mod.rs @@ -8,12 +8,8 @@ use std::{ use algorithm::{SingleShotController, SingleShotControllerConfig}; use ntp_proto::{NtpClock, NtpDuration}; -#[cfg(feature = "unstable_nts-pool")] -use crate::daemon::config::NtsPoolSourceConfig; use crate::daemon::{ - config::{self, PoolSourceConfig}, - initialize_logging_parse_config, nts_key_provider, spawn, - tracing::LogLevel, + config, initialize_logging_parse_config, nts_key_provider, spawn, tracing::LogLevel, }; mod algorithm; @@ -129,11 +125,9 @@ pub(crate) async fn force_sync(config: Option) -> std::io::Result total_sources += 1, - config::NtpSourceConfig::Pool(PoolSourceConfig { count, .. }) => total_sources += count, + config::NtpSourceConfig::Pool(ref cfg) => total_sources += cfg.first.count, #[cfg(feature = "unstable_nts-pool")] - config::NtpSourceConfig::NtsPool(NtsPoolSourceConfig { count, .. }) => { - total_sources += count - } + config::NtpSourceConfig::NtsPool(cfg) => total_sources += cfg.first.count, } }