From 9c3c59b1c480c1b7cc2d9090a720e7b28a0a9c8f Mon Sep 17 00:00:00 2001 From: David Venhoek Date: Wed, 11 Dec 2024 13:44:41 +0100 Subject: [PATCH] Added per-source configuration of poll interval. --- docs/man/ntp.toml.5.md | 17 ++++- docs/precompiled/man/ntp.toml.5 | 21 +++++- ntpd/src/daemon/config/mod.rs | 75 ++++++++++++------- ntpd/src/daemon/config/ntp_source.rs | 105 ++++++++++++++++++++++++--- ntpd/src/daemon/spawn/pool.rs | 38 ++++++---- ntpd/src/daemon/spawn/standard.rs | 42 ++++++----- ntpd/src/daemon/system.rs | 20 ++++- ntpd/src/force_sync/mod.rs | 12 +-- 8 files changed, 241 insertions(+), 89 deletions(-) diff --git a/docs/man/ntp.toml.5.md b/docs/man/ntp.toml.5.md index a4e899f5b..437423100 100644 --- a/docs/man/ntp.toml.5.md +++ b/docs/man/ntp.toml.5.md @@ -45,8 +45,8 @@ 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 @@ -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 32 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 32 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 a397e4fea..983bb4de1 100644 --- a/docs/precompiled/man/ntp.toml.5 +++ b/docs/precompiled/man/ntp.toml.5 @@ -64,8 +64,9 @@ 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. @@ -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 32 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 32 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 755f47e3e..3f75f1795 100644 --- a/ntpd/src/daemon/config/ntp_source.rs +++ b/ntpd/src/daemon/config/ntp_source.rs @@ -6,9 +6,9 @@ use std::{ sync::{Arc, Mutex}, }; -use ntp_proto::NtpDuration; #[cfg(feature = "unstable_ntpv5")] use ntp_proto::NtpVersion; +use ntp_proto::{NtpDuration, PollInterval, PollIntervalLimits, SourceConfig}; use rustls::pki_types::CertificateDer; use serde::{de, Deserialize, Deserializer}; @@ -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/spawn/pool.rs b/ntpd/src/daemon/spawn/pool.rs index 19f961fbd..9ddaf0f33 100644 --- a/ntpd/src/daemon/spawn/pool.rs +++ b/ntpd/src/daemon/spawn/pool.rs @@ -210,14 +210,17 @@ mod tests { let address_strings = ["127.0.0.1:123", "127.0.0.2:123", "127.0.0.3:123"]; let addresses = address_strings.map(|addr| addr.parse().unwrap()); - let mut pool = PoolSpawner::new(PoolSourceConfig { - addr: NormalizedAddress::with_hardcoded_dns("example.com", 123, addresses.to_vec()) - .into(), - count: 2, - ignore: vec![], - #[cfg(feature = "unstable_ntpv5")] - ntp_version: Some(ntp_proto::NtpVersion::V5), - }, SourceConfig::default()); + let mut pool = PoolSpawner::new( + PoolSourceConfig { + addr: NormalizedAddress::with_hardcoded_dns("example.com", 123, addresses.to_vec()) + .into(), + count: 2, + ignore: vec![], + #[cfg(feature = "unstable_ntpv5")] + ntp_version: Some(ntp_proto::NtpVersion::V5), + }, + SourceConfig::default(), + ); let spawner_id = pool.get_id(); let (action_tx, mut action_rx) = mpsc::channel(MESSAGE_BUFFER_SIZE); @@ -252,14 +255,17 @@ mod tests { let address_strings = ["127.0.0.1:123", "127.0.0.2:123", "127.0.0.3:123"]; let addresses = address_strings.map(|addr| addr.parse().unwrap()); - let mut pool = PoolSpawner::new(PoolSourceConfig { - addr: NormalizedAddress::with_hardcoded_dns("example.com", 123, addresses.to_vec()) - .into(), - count: 2, - ignore: vec![], - #[cfg(feature = "unstable_ntpv5")] - ntp_version: Some(ntp_proto::NtpVersion::V4), - }, SourceConfig::default()); + let mut pool = PoolSpawner::new( + PoolSourceConfig { + addr: NormalizedAddress::with_hardcoded_dns("example.com", 123, addresses.to_vec()) + .into(), + count: 2, + ignore: vec![], + #[cfg(feature = "unstable_ntpv5")] + ntp_version: Some(ntp_proto::NtpVersion::V4), + }, + SourceConfig::default(), + ); let spawner_id = pool.get_id(); let (action_tx, mut action_rx) = mpsc::channel(MESSAGE_BUFFER_SIZE); diff --git a/ntpd/src/daemon/spawn/standard.rs b/ntpd/src/daemon/spawn/standard.rs index 6b850a612..506d4b903 100644 --- a/ntpd/src/daemon/spawn/standard.rs +++ b/ntpd/src/daemon/spawn/standard.rs @@ -199,15 +199,18 @@ mod tests { #[cfg(feature = "unstable_ntpv5")] #[tokio::test] async fn respects_ntp_version_force_v5() { - let mut spawner = StandardSpawner::new(StandardSource { - address: NormalizedAddress::with_hardcoded_dns( - "example.com", - 123, - vec!["127.0.0.1:123".parse().unwrap()], - ) - .into(), - ntp_version: Some(ntp_proto::NtpVersion::V5), - }, SourceConfig::default()); + let mut spawner = StandardSpawner::new( + StandardSource { + address: NormalizedAddress::with_hardcoded_dns( + "example.com", + 123, + vec!["127.0.0.1:123".parse().unwrap()], + ) + .into(), + ntp_version: Some(ntp_proto::NtpVersion::V5), + }, + SourceConfig::default(), + ); let spawner_id = spawner.get_id(); let (action_tx, mut action_rx) = mpsc::channel(MESSAGE_BUFFER_SIZE); @@ -226,15 +229,18 @@ mod tests { #[cfg(feature = "unstable_ntpv5")] #[tokio::test] async fn respects_ntp_version_force_v4() { - let mut spawner = StandardSpawner::new(StandardSource { - address: NormalizedAddress::with_hardcoded_dns( - "example.com", - 123, - vec!["127.0.0.1:123".parse().unwrap()], - ) - .into(), - ntp_version: Some(ntp_proto::NtpVersion::V4), - }, SourceConfig::default()); + let mut spawner = StandardSpawner::new( + StandardSource { + address: NormalizedAddress::with_hardcoded_dns( + "example.com", + 123, + vec!["127.0.0.1:123".parse().unwrap()], + ) + .into(), + ntp_version: Some(ntp_proto::NtpVersion::V4), + }, + SourceConfig::default(), + ); let spawner_id = spawner.get_id(); let (action_tx, mut action_rx) = mpsc::channel(MESSAGE_BUFFER_SIZE); 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, } }