Skip to content
4 changes: 4 additions & 0 deletions ntp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ single-step-panic-threshold = 1800
startup-step-panic-threshold = { forward="inf", backward = 1800 }
#accumulated-step-panic-threshold = 1800
#minimum-agreeing-sources = 3

# Set to false to run without CAP_SYS_TIME.
# The daemon tracks offset internally and serves corrected time to clients.
#update-system-clock = true
140 changes: 140 additions & 0 deletions ntpd/src/daemon/clock.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use clock_steering::{Clock, TimeOffset, unix::UnixClock};
use ntp_proto::NtpClock;
use ntp_proto::{NtpDuration, NtpTimestamp};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};

use super::util::convert_clock_timestamp;

Expand Down Expand Up @@ -73,3 +76,140 @@ impl NtpClock for NtpClockWrapper {
})
}
}

/// Trait for clocks that can convert a system timestamp to "true" time.
/// For regular clocks this is a no-op; for soft-clock mode it adds the
/// tracked offset.
pub trait TrueTimeClock: NtpClock {
fn to_true_time(&self, system_time: NtpTimestamp) -> NtpTimestamp;
}

impl TrueTimeClock for NtpClockWrapper {
fn to_true_time(&self, system_time: NtpTimestamp) -> NtpTimestamp {
system_time
}
}

/// Clock wrapper that optionally tracks offset without steering the OS.
/// When `update_system_clock` is false, all steering calls become no-ops
/// and the offset is tracked internally. `now()` always returns system
/// time (needed by the Kalman filter and sources); use `to_true_time()`
/// to convert a system timestamp to the estimated true time.
#[derive(Clone)]
pub struct SoftClock<C: NtpClock + Clone> {
inner: C,
enabled: Arc<AtomicBool>,
state: Arc<Mutex<SoftClockState>>,
}

#[derive(Clone, Copy, Debug)]
struct SoftClockState {
offset: NtpDuration,
frequency_ppm: f64,
last_update: NtpTimestamp,
}

impl<C: NtpClock + Clone> SoftClock<C> {
pub fn new(inner: C, update_system_clock: bool) -> Self {
Self {
inner,
enabled: Arc::new(AtomicBool::new(update_system_clock)),
state: Arc::new(Mutex::new(SoftClockState {
offset: NtpDuration::ZERO,
frequency_ppm: 0.0,
last_update: NtpTimestamp::default(),
})),
}
}

fn now_true_from_system(&self, sys_ts: NtpTimestamp) -> NtpTimestamp {
let state = self.state.lock().unwrap();
let elapsed = sys_ts - state.last_update;
let elapsed_secs = duration_to_seconds(elapsed);
let drift = NtpDuration::from_seconds(elapsed_secs * state.frequency_ppm * 1e-6);
sys_ts + state.offset + drift
}
}

impl<C: NtpClock + Clone> NtpClock for SoftClock<C> {
type Error = C::Error;

/// Always returns system time. The Kalman filter and source tasks
/// need system time for consistent offset calculations.
fn now(&self) -> Result<NtpTimestamp, Self::Error> {
self.inner.now()
}

fn set_frequency(&self, freq: f64) -> Result<NtpTimestamp, Self::Error> {
if self.enabled.load(Ordering::Relaxed) {
self.inner.set_frequency(freq)
} else {
let mut state = self.state.lock().unwrap();
state.frequency_ppm = freq;
state.last_update = self.inner.now()?;
Ok(state.last_update)
}
}

fn get_frequency(&self) -> Result<f64, Self::Error> {
if self.enabled.load(Ordering::Relaxed) {
self.inner.get_frequency()
} else {
Ok(self.state.lock().unwrap().frequency_ppm)
}
}

fn step_clock(&self, offset: NtpDuration) -> Result<NtpTimestamp, Self::Error> {
if self.enabled.load(Ordering::Relaxed) {
self.inner.step_clock(offset)
} else {
let mut state = self.state.lock().unwrap();
state.offset = offset;
state.last_update = self.inner.now()?;
Ok(state.last_update)
}
}

fn disable_ntp_algorithm(&self) -> Result<(), Self::Error> {
if self.enabled.load(Ordering::Relaxed) {
self.inner.disable_ntp_algorithm()
} else {
Ok(())
}
}

fn error_estimate_update(
&self,
_est_error: NtpDuration,
_max_error: NtpDuration,
) -> Result<(), Self::Error> {
if self.enabled.load(Ordering::Relaxed) {
self.inner.error_estimate_update(_est_error, _max_error)
} else {
Ok(())
}
}

fn status_update(&self, leap_status: ntp_proto::NtpLeapIndicator) -> Result<(), Self::Error> {
if self.enabled.load(Ordering::Relaxed) {
self.inner.status_update(leap_status)
} else {
Ok(())
}
}
}

impl<C: NtpClock + Clone> TrueTimeClock for SoftClock<C> {
fn to_true_time(&self, system_time: NtpTimestamp) -> NtpTimestamp {
if self.enabled.load(Ordering::Relaxed) {
system_time
} else {
self.now_true_from_system(system_time)
}
}
}

fn duration_to_seconds(d: NtpDuration) -> f64 {
let (secs, nanos) = d.as_seconds_nanos();
secs as f64 + nanos as f64 / 1_000_000_000.0
}
15 changes: 15 additions & 0 deletions ntpd/src/daemon/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,10 @@ fn default_metrics_exporter_listen() -> SocketAddr {
"127.0.0.1:9975".parse().unwrap()
}


fn default_update_system_clock() -> bool {
true
}
#[derive(Deserialize, Debug, Default)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct DaemonSynchronizationConfig {
Expand All @@ -393,6 +397,9 @@ pub struct DaemonSynchronizationConfig {

#[serde(default)]
pub algorithm: AlgorithmConfig,

#[serde(default = "default_update_system_clock", rename = "update-system-clock")]
pub update_system_clock: bool,
}

#[derive(Deserialize, Debug, Default)]
Expand Down Expand Up @@ -517,6 +524,14 @@ impl Config {
info!("No sources configured. Daemon will not change system time.");
}


if !self.synchronization.update_system_clock {
warn!(
"update-system-clock is disabled. The daemon will not adjust the OS clock. \
Ensure [[server]] blocks are configured if you intend to serve time, \
otherwise this instance will only monitor without acting."
);
}
if !self.sources.is_empty()
&& self.count_sources()
< self
Expand Down
10 changes: 7 additions & 3 deletions ntpd/src/daemon/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use std::{error::Error, io::IsTerminal, path::Path};

use ::tracing::info;
pub use config::Config;
use self::clock::{NtpClockWrapper, SoftClock};
use ntp_proto::{KalmanClockController, TimeSyncControllerWrapper};
pub use observer::ObservableState;
pub use system::spawn;
Expand Down Expand Up @@ -155,13 +156,15 @@ fn run(options: &NtpDaemonOptions) -> Result<(), Box<dyn Error>> {
let clock_config = config::ClockConfig::default();

::tracing::debug!("Configuration loaded, spawning daemon jobs");
let clock = clock_config.clock;
let clock = SoftClock::new(clock_config.clock, config.synchronization.update_system_clock);
let (main_loop_handle, channels) =
spawn::<TimeSyncControllerWrapper<KalmanClockController<_>>>(
spawn::<SoftClock<NtpClockWrapper>, TimeSyncControllerWrapper<KalmanClockController<SoftClock<NtpClockWrapper>>>>(
config.synchronization.synchronization_base,
config.synchronization.algorithm,
config.source_defaults,
clock_config,
clock.clone(),
clock_config.interface,
clock_config.timestamp_mode,
&config.sources,
&config.servers,
#[cfg(target_os = "linux")]
Expand All @@ -182,6 +185,7 @@ fn run(options: &NtpDaemonOptions) -> Result<(), Box<dyn Error>> {
channels.server_data_receiver,
channels.system_snapshot_receiver,
clock,
config.synchronization.update_system_clock,
);

let _ = notify_ready().await;
Expand Down
10 changes: 9 additions & 1 deletion ntpd/src/daemon/observer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pub struct ObservableState {
pub system: SystemSnapshot,
pub sources: Vec<ObservableSourceState>,
pub servers: Vec<ObservableServerState>,
pub system_clock_adjustment: bool,
pub accumulated_offset_ms: Option<f64>,
}

#[derive(Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -74,12 +76,13 @@ pub fn spawn<C: 'static + NtpClock + Send>(
server_reader: tokio::sync::watch::Receiver<Vec<ServerData>>,
system_reader: tokio::sync::watch::Receiver<SystemSnapshot>,
clock: C,
update_system_clock: bool,
) -> JoinHandle<std::io::Result<()>> {
let config = config.clone();
tokio::spawn(
(async move {
let result =
observer(config, sources_reader, server_reader, system_reader, clock).await;
observer(config, sources_reader, server_reader, system_reader, clock, update_system_clock).await;
if let Err(ref e) = result {
warn!("Abnormal termination of the state observer: {e}");
warn!("The state observer will not be available");
Expand All @@ -96,6 +99,7 @@ async fn observer<C: 'static + NtpClock + Send>(
server_reader: tokio::sync::watch::Receiver<Vec<ServerData>>,
system_reader: tokio::sync::watch::Receiver<SystemSnapshot>,
clock: C,
update_system_clock: bool,
) -> std::io::Result<()> {
let start_time = Instant::now();
let timeout = std::time::Duration::from_millis(500);
Expand Down Expand Up @@ -150,6 +154,7 @@ async fn observer<C: 'static + NtpClock + Send>(
server_reader,
system_reader,
now,
update_system_clock,
)
.await
};
Expand All @@ -172,6 +177,7 @@ async fn handle_connection(
server_reader: tokio::sync::watch::Receiver<Vec<ServerData>>,
system_reader: tokio::sync::watch::Receiver<SystemSnapshot>,
now: NtpTimestamp,
update_system_clock: bool,
) -> std::io::Result<()> {
let observe = ObservableState {
program: ProgramData::with_dynamics(start_time.elapsed().as_secs_f64(), now),
Expand All @@ -183,6 +189,8 @@ async fn handle_connection(
.collect(),
system: *system_reader.borrow(),
servers: server_reader.borrow().iter().map(Into::into).collect(),
system_clock_adjustment: update_system_clock,
accumulated_offset_ms: None, // TODO: wire through SoftClock when available
};

super::sockets::write_json(stream, &observe).await?;
Expand Down
20 changes: 16 additions & 4 deletions ntpd/src/daemon/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use tokio::task::JoinHandle;
use tracing::{Instrument, Span, debug, instrument, warn};

use super::{config::ServerConfig, util::convert_net_timestamp};
use super::clock::TrueTimeClock;

// Maximum size of udp packet we handle
const MAX_PACKET_SIZE: usize = 1024;
Expand Down Expand Up @@ -98,18 +99,20 @@ impl<'de> Deserialize<'de> for Counter {
}
}

pub struct ServerTask<C: 'static + NtpClock + Send> {
pub struct ServerTask<C: 'static + NtpClock + TrueTimeClock + Send> {
config: ServerConfig,
network_wait_period: std::time::Duration,
keyset: tokio::sync::watch::Receiver<Arc<KeySet>>,
server: Server<C>,
clock: C,
stats: ServerStats,
}

impl<C: 'static + NtpClock + Send> ServerTask<C> {
impl<C: 'static + NtpClock + TrueTimeClock + Clone + Send> ServerTask<C> {
#[instrument(level = tracing::Level::ERROR, name = "Ntp Server", skip_all, fields(address = debug(config.listen)))]
pub fn spawn(
server: Server<C>,
clock: C,
config: ServerConfig,
stats: ServerStats,
keyset: tokio::sync::watch::Receiver<Arc<KeySet>>,
Expand All @@ -122,6 +125,7 @@ impl<C: 'static + NtpClock + Send> ServerTask<C> {
network_wait_period,
keyset,
server,
clock,
stats,
};

Expand Down Expand Up @@ -173,9 +177,10 @@ impl<C: 'static + NtpClock + Send> ServerTask<C> {
..
}) if let Some(timestamp) = timestamp_data.selected_timestamp() => {
let mut send_buf = [0u8; MAX_PACKET_SIZE];
let recv_time = self.clock.to_true_time(convert_net_timestamp(timestamp));
match self.server.handle(
source_addr.ip(),
convert_net_timestamp(timestamp),
recv_time,
&buf[..length],
&mut send_buf[..length],
&mut self.stats,
Expand Down Expand Up @@ -280,6 +285,12 @@ mod tests {
}
}

impl TrueTimeClock for TestClock {
fn to_true_time(&self, system_time: NtpTimestamp) -> NtpTimestamp {
system_time
}
}

fn serialize_packet_unencrypted(send_packet: &NtpPacket) -> Vec<u8> {
let mut buf = vec![0; MAX_PACKET_SIZE];
let mut cursor = Cursor::new(buf.as_mut_slice());
Expand All @@ -304,13 +315,14 @@ mod tests {

let server = Server::new_internal(
config.clone().into(),
clock,
clock.clone(),
server_info,
keyset.borrow().clone(),
);

let join = ServerTask::spawn(
server,
clock,
config,
ServerStats::default(),
keyset,
Expand Down
Loading
Loading