use clap::Parser;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use ntpsec_rs_core::daemon_engine::*;
use ntpsec_rs_core::ntp_config::*;
use ntpsec_rs_core::ntp_io::*;
use ntpsec_rs_core::Adjustment;
#[derive(Parser, Debug)]
#[command(name = "ntpd-rs", about = "NTP daemon", version = "1.3.3")]
struct Cli {
#[arg(short = 'c', long, default_value = "/etc/ntp.conf")]
config: PathBuf,
#[arg(short = 'n', long)]
nofork: bool,
#[arg(short = 'g', long)]
panicgate: bool,
#[arg(short = 'x', long)]
slew: bool,
#[arg(short = 'q', long)]
query: bool,
#[arg(short = 'f', long)]
driftfile: Option<PathBuf>,
#[arg(long)]
lab_daemon: bool,
#[arg(long)]
trace: Option<PathBuf>,
#[arg(long)]
record_trace: Option<PathBuf>,
#[arg(long)]
seccomp: bool,
#[arg(short = 'u', long, default_value = "ntp")]
user: String,
}
fn main() {
let cli = Cli::parse();
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_target(false)
.init();
tracing::info!(
"ntpd-rs v{} — NTPsec daemon (Rust)",
env!("CARGO_PKG_VERSION")
);
tracing::info!("Config: {}", cli.config.display());
let config = match read_config_file(&cli.config) {
Ok(tree) => {
tracing::info!("Loaded {} configuration directives", tree.options.len());
for err in &tree.errors {
tracing::warn!("Config error: {}", err);
}
tree
}
Err(e) => {
tracing::error!("Failed to read config: {}", e);
std::process::exit(1);
}
};
let keys_paths: Vec<String> = config
.options
.iter()
.filter_map(|opt| {
if let ntpsec_rs_core::ntp_config::ConfigOption::Keys(p) = opt {
Some(p.clone())
} else {
None
}
})
.collect();
let lab_config = config.clone();
let mut engine = DaemonEngine::new(config);
for path in &keys_paths {
if let Ok(content) = std::fs::read_to_string(path) {
match engine.auth.parse_keys_file(&content) {
Ok(count) => tracing::info!("Loaded {} keys from '{}'", count, path),
Err(e) => tracing::warn!("Failed to load keys from '{}': {}", path, e),
}
} else {
tracing::warn!("Cannot read key file '{}'", path);
}
}
if cli.lab_daemon {
return run_lab_daemon(lab_config, &cli);
}
if cli.slew {
engine.loop_filter.step_threshold = f64::MAX;
}
if cli.panicgate {
engine.loop_filter.panic_threshold = f64::MAX;
}
let mut clock = ntpsec_rs_io::RealSystemClock::new();
let mut network = ntpsec_rs_io::RealNetworkIo::new();
let mut store = ntpsec_rs_io::FileStateStore::new(&std::path::Path::new("/var/lib/ntp"));
if let Ok(freq) = store.load_drift() {
engine.loop_filter.frequency = freq;
tracing::info!("Loaded drift: {:.3} ppm", freq);
}
if let Err(e) = network.bind("0.0.0.0:123") {
tracing::warn!("Cannot bind to port 123: {e} (try running as root)");
}
if cli.query {
tracing::info!("Query-only mode: polling peers and setting clock");
run_query_mode(&mut engine, &mut clock, &mut network, &mut store);
return;
}
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
signal_hook_init(r);
tracing::info!("Entering main event loop with {} peers", engine.peers.len());
let mut iteration: u64 = 0;
while running.load(Ordering::Relaxed) {
iteration += 1;
let now = clock.now();
let timer_actions = engine.tick(now);
execute_actions(&timer_actions, &mut clock, &mut network, &mut store);
match network.recv() {
Ok(dgram) => {
let event = DaemonEvent::PacketReceived(dgram);
let actions = engine.handle(event);
execute_actions(&actions, &mut clock, &mut network, &mut store);
}
Err(IoError::RecvFailed(_)) => {
}
Err(e) => {
if iteration % 100 == 0 {
tracing::debug!("Recv error: {e}");
}
}
}
if iteration % 100 == 0 {
tracing::info!(
"Status: peers={} stratum={} offset={:.6}s freq={:.3}ppm",
engine.system.peer_count,
engine.system.stratum,
engine.system.sys_offset,
engine.loop_filter.frequency_ppm(),
);
}
std::thread::sleep(std::time::Duration::from_secs(1));
}
let shutdown_actions = engine.handle(DaemonEvent::Shutdown);
execute_actions(&shutdown_actions, &mut clock, &mut network, &mut store);
tracing::info!("ntpd-rs shutting down");
}
fn execute_actions<C: SystemClock, N: NetworkIo, S: StateStore>(
actions: &[DaemonAction],
clock: &mut C,
network: &mut N,
store: &mut S,
) {
for action in actions {
match action {
DaemonAction::Send { destination, bytes } => {
if let Err(e) = network.send(bytes, destination) {
tracing::warn!("Send failed: {e}");
}
}
DaemonAction::AdjustClock(adj) => match adj {
Adjustment::Step(offset) => {
if let Err(e) = clock.step(*offset) {
tracing::error!("Step failed: {e}");
} else {
tracing::info!("Stepped clock by {:.6}s", offset);
}
}
Adjustment::Slew(offset, freq) => {
if let Err(e) = clock.slew(*offset, *freq) {
tracing::error!("Slew failed: {e}");
} else {
tracing::trace!("Slewed clock by {:.6}s at {:.3}ppm", offset, freq);
}
}
Adjustment::Panic(offset) => {
tracing::error!("Panic: offset {:.6}s exceeds threshold!", offset);
std::process::exit(1);
}
Adjustment::Ignore => {}
},
DaemonAction::PersistDrift(freq) => {
if let Err(e) = store.save_drift(*freq) {
tracing::error!("Failed to save drift: {e}");
} else {
tracing::debug!("Saved drift: {:.3} ppm", freq);
}
}
DaemonAction::Log(msg) => {
tracing::info!("{}", msg);
}
DaemonAction::AppendStatistic { stream, line } => {
if let Err(e) = store.append_stats(stream, line) {
tracing::warn!("Failed to write to {stream}: {e}");
}
}
}
}
}
fn run_query_mode(
engine: &mut DaemonEngine,
clock: &mut ntpsec_rs_io::RealSystemClock,
network: &mut ntpsec_rs_io::RealNetworkIo,
store: &mut ntpsec_rs_io::FileStateStore,
) {
let max_iterations = 10;
for _i in 0..max_iterations {
let now = clock.now();
let timer_actions = engine.tick(now);
execute_actions(&timer_actions, clock, network, store);
match network.recv() {
Ok(dgram) => {
let actions = engine.handle(DaemonEvent::PacketReceived(dgram));
execute_actions(&actions, clock, network, store);
}
Err(IoError::RecvFailed(_)) => {}
Err(e) => {
tracing::debug!("Recv error: {e}");
}
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
if engine.system.peer_count > 0 && engine.system.sys_offset.is_finite() {
tracing::info!("Setting clock: offset={:.6}s", engine.system.sys_offset);
if let Err(e) = clock.step(engine.system.sys_offset) {
tracing::error!("Failed to step clock: {e}");
}
} else {
tracing::warn!("No synchronization source available");
}
}
fn run_lab_daemon(config: ConfigTree, cli: &Cli) {
tracing::info!("Starting lab daemon (deterministic replay mode)");
let mut engine = DaemonEngine::new(config);
if cli.panicgate {
engine.loop_filter.panic_threshold = f64::MAX;
}
let mut trace = if let Some(trace_path) = cli.trace.as_ref() {
match std::fs::read_to_string(trace_path) {
Ok(content) => match PacketTrace::from_json(&content) {
Ok(t) => {
tracing::info!(
"Loaded trace with {} entries from {}",
t.len(),
trace_path.display()
);
t
}
Err(e) => {
tracing::warn!(
"Failed to parse trace '{}': {}, starting empty",
trace_path.display(),
e
);
PacketTrace::new()
}
},
Err(e) => {
tracing::warn!(
"Cannot read trace '{}': {}, starting empty",
trace_path.display(),
e
);
PacketTrace::new()
}
}
} else {
PacketTrace::new()
};
let replay_dgrams: Vec<ReceivedDatagram> = trace
.iter()
.filter(|e| e.direction == TraceDirection::Received)
.map(|e| ReceivedDatagram {
bytes: e.bytes.clone(),
source: e.source,
destination: e.destination,
rx_timestamp: e.timestamp,
interface_index: None,
timestamp_source: TimestampSource::UserspaceFallback,
})
.collect();
let replay_count = replay_dgrams.len();
let mut clock = SimulatedClock::unix_epoch();
let mut store = MemoryStateStore::new();
let mut network = ReplayNetwork::new(replay_dgrams);
tracing::info!(
"Lab daemon initialized with {} peers, {} timers, {} replay datagrams",
engine.peers.len(),
engine.timers.len(),
replay_count,
);
if let Ok(freq) = store.load_drift() {
engine.loop_filter.frequency = freq;
}
for iter in 0..10 {
let now = clock.now();
let timer_actions = engine.tick(now);
execute_actions(&timer_actions, &mut clock, &mut network, &mut store);
loop {
match network.recv() {
Ok(dgram) => {
let actions = engine.handle(DaemonEvent::PacketReceived(dgram));
execute_actions(&actions, &mut clock, &mut network, &mut store);
}
Err(_) => break,
}
}
if iter % 3 == 0 {
tracing::info!(
"[lab status] peers={} stratum={} offset={:.6}s freq={:.3}ppm sent={}",
engine.system.peer_count,
engine.system.stratum,
engine.system.sys_offset,
engine.loop_filter.frequency_ppm(),
network.sent_packets.len(),
);
}
clock.advance(4.0);
}
if let Some(record_path) = cli.record_trace.as_ref() {
for (dest, bytes) in &network.sent_packets {
trace.push(TraceEntry {
timestamp: clock.now(),
direction: TraceDirection::Sent,
source: NetAddr::ipv4(0x7f000001, 123),
destination: *dest,
bytes: bytes.clone(),
});
}
if std::fs::write(record_path, trace.to_json()).is_ok() {
tracing::info!(
"Recorded trace with {} entries to {}",
trace.len(),
record_path.display()
);
}
}
tracing::info!(
"Lab daemon final state: {} peers, stratum={}, offset={:.6}s, freq={:.3}ppm, sent={} packets",
engine.peers.len(),
engine.system.stratum,
engine.system.sys_offset,
engine.loop_filter.frequency_ppm(),
network.sent_packets.len(),
);
}
fn signal_hook_init(running: Arc<AtomicBool>) {
let _ = running;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cli_default_config() {
let cli = Cli::parse_from(["ntpd-rs"]);
assert_eq!(cli.config, PathBuf::from("/etc/ntp.conf"));
}
#[test]
fn test_cli_custom_config() {
let cli = Cli::parse_from(["ntpd-rs", "-c", "/tmp/test.conf"]);
assert_eq!(cli.config, PathBuf::from("/tmp/test.conf"));
}
#[test]
fn test_cli_flags() {
let cli = Cli::parse_from(["ntpd-rs", "-n", "-g", "-x", "-q", "--lab-daemon"]);
assert!(cli.nofork);
assert!(cli.panicgate);
assert!(cli.slew);
assert!(cli.query);
assert!(cli.lab_daemon);
}
#[test]
fn test_cli_driftfile() {
let cli = Cli::parse_from(["ntpd-rs", "-f", "/tmp/drift"]);
assert_eq!(cli.driftfile, Some(PathBuf::from("/tmp/drift")));
}
#[test]
fn test_execute_actions_no_panic() {
let mut clock = ntpsec_rs_io::RealSystemClock::new();
let mut network = ntpsec_rs_io::RealNetworkIo::new();
let mut store = ntpsec_rs_io::FileStateStore::new(&std::path::Path::new("/tmp"));
let actions = vec![
DaemonAction::Log("test log".to_string()),
DaemonAction::AdjustClock(Adjustment::Ignore),
DaemonAction::Send {
destination: NetAddr::ipv4(0x7f000001, 123),
bytes: vec![0u8; 48],
},
DaemonAction::PersistDrift(0.0),
DaemonAction::AppendStatistic {
stream: "loopstats".to_string(),
line: "test".to_string(),
},
];
execute_actions(&actions, &mut clock, &mut network, &mut store);
}
}