user: add a basic NTP client
This commit is contained in:
parent
fb25e70714
commit
f30cafb3bd
13
userspace/Cargo.lock
generated
13
userspace/Cargo.lock
generated
@ -1536,6 +1536,19 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntpc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"chrono",
|
||||
"clap",
|
||||
"cross",
|
||||
"log",
|
||||
"logsink",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.2"
|
||||
|
@ -17,6 +17,7 @@ members = [
|
||||
"tools/crypt",
|
||||
"tools/init",
|
||||
"tools/md2txt",
|
||||
"tools/ntpc",
|
||||
"tools/rdb",
|
||||
"tools/red",
|
||||
"tools/rsh",
|
||||
|
4
userspace/etc/rc.d/20-timesync
Executable file
4
userspace/etc/rc.d/20-timesync
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Sync every 20 minutes
|
||||
/sbin/service start -- /bin/ntpc -i 1200 time.google.com:123
|
@ -8,3 +8,4 @@ pub mod io;
|
||||
pub mod mem;
|
||||
pub mod net;
|
||||
pub mod signal;
|
||||
pub mod time;
|
||||
|
@ -5,6 +5,7 @@ pub mod pipe;
|
||||
pub mod poll;
|
||||
pub mod socket;
|
||||
pub mod term;
|
||||
pub mod time;
|
||||
pub mod timer;
|
||||
|
||||
use std::io;
|
||||
|
16
userspace/lib/cross/src/sys/yggdrasil/time.rs
Normal file
16
userspace/lib/cross/src/sys/yggdrasil/time.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use runtime::rt as yggdrasil_rt;
|
||||
use yggdrasil_rt::time::{self, ClockType};
|
||||
|
||||
use crate::time::{SystemClock, SystemTime};
|
||||
|
||||
pub fn now(clock: SystemClock) -> SystemTime {
|
||||
let clock = match clock {
|
||||
SystemClock::Monotonic => ClockType::Monotonic,
|
||||
SystemClock::RealTime => ClockType::RealTime,
|
||||
};
|
||||
let raw = time::get_clock(clock).expect("Could not get clock");
|
||||
SystemTime {
|
||||
seconds: raw.seconds(),
|
||||
nanoseconds: raw.subsec_nanos() as u32,
|
||||
}
|
||||
}
|
19
userspace/lib/cross/src/time.rs
Normal file
19
userspace/lib/cross/src/time.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use crate::sys;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SystemTime {
|
||||
pub seconds: u64,
|
||||
pub nanoseconds: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SystemClock {
|
||||
Monotonic,
|
||||
RealTime,
|
||||
}
|
||||
|
||||
impl SystemTime {
|
||||
pub fn now(clock: SystemClock) -> Self {
|
||||
sys::time::now(clock)
|
||||
}
|
||||
}
|
17
userspace/tools/ntpc/Cargo.toml
Normal file
17
userspace/tools/ntpc/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "ntpc"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
logsink.workspace = true
|
||||
cross.workspace = true
|
||||
|
||||
bytemuck.workspace = true
|
||||
chrono.workspace = true
|
||||
thiserror.workspace = true
|
||||
clap.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
249
userspace/tools/ntpc/src/main.rs
Normal file
249
userspace/tools/ntpc/src/main.rs
Normal file
@ -0,0 +1,249 @@
|
||||
#![feature(yggdrasil_os)]
|
||||
use std::{
|
||||
io,
|
||||
net::{Ipv4Addr, SocketAddr, ToSocketAddrs, UdpSocket},
|
||||
os::fd::AsRawFd,
|
||||
process::ExitCode,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use chrono::{DateTime, Utc};
|
||||
use clap::Parser;
|
||||
use cross::io::{Poll, TimerFd};
|
||||
|
||||
const NTP_TIMESTAMP_EPOCH: u32 = 2208988800;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum Error {
|
||||
#[error("{0}")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("Timed out")]
|
||||
TimedOut,
|
||||
#[error("Invalid response version")]
|
||||
InvalidResponseVersion,
|
||||
#[error("Invalid response mode")]
|
||||
InvalidResponseMode,
|
||||
#[error("Invalid response origin timestamp")]
|
||||
InvalidResponseOriginTimestamp,
|
||||
#[error("Invalid response timestamp")]
|
||||
InvalidResponseTimestamp,
|
||||
#[error("Invalid server address")]
|
||||
InvalidServerAddress,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
|
||||
#[repr(C)]
|
||||
struct NtpPacketHeader {
|
||||
status: u8,
|
||||
ignored: [u8; 3],
|
||||
root_delay: u32,
|
||||
root_dispersion: u32,
|
||||
reference_id: u32,
|
||||
reference_timestamp: u64,
|
||||
origin_timestamp: u64,
|
||||
receive_timestamp: u64,
|
||||
transmit_timestamp: u64,
|
||||
}
|
||||
|
||||
const NANOSECONDS_IN_SECOND: u64 = 1_000_000_000;
|
||||
const MICROSECONDS_IN_SECOND: u64 = 1_000_000;
|
||||
|
||||
fn timestamp_now() -> u64 {
|
||||
let now = SystemTime::now();
|
||||
let duration = now.duration_since(SystemTime::UNIX_EPOCH).unwrap();
|
||||
let seconds = duration
|
||||
.as_secs()
|
||||
.saturating_add(NTP_TIMESTAMP_EPOCH as u64);
|
||||
let microseconds = duration.subsec_micros() as u64;
|
||||
let fraction = microseconds * u32::MAX as u64 / MICROSECONDS_IN_SECOND;
|
||||
(seconds << 32) | fraction
|
||||
}
|
||||
|
||||
fn time_from_timestamp(ts: u64) -> Result<DateTime<Utc>, Error> {
|
||||
let mut seconds = (ts >> 32).saturating_sub(NTP_TIMESTAMP_EPOCH as u64);
|
||||
let mut seconds_fraction = ts & 0xFFFFFFFF;
|
||||
seconds += seconds_fraction / u32::MAX as u64;
|
||||
seconds_fraction %= u32::MAX as u64;
|
||||
let nanoseconds = seconds_fraction * NANOSECONDS_IN_SECOND / (u32::MAX as u64);
|
||||
DateTime::from_timestamp(seconds as i64, nanoseconds as u32)
|
||||
.ok_or(Error::InvalidResponseTimestamp)
|
||||
}
|
||||
|
||||
fn handle_response(response: NtpPacketHeader, timestamp: u64) -> Result<DateTime<Utc>, Error> {
|
||||
let response_version = (response.status >> 3) & 0x7;
|
||||
let response_mode = response.status & 0x7;
|
||||
if response_version != 4 {
|
||||
return Err(Error::InvalidResponseVersion);
|
||||
}
|
||||
if response_mode != 4 {
|
||||
return Err(Error::InvalidResponseMode);
|
||||
}
|
||||
let origin_timestamp = u64::from_be(response.origin_timestamp);
|
||||
if origin_timestamp != timestamp {
|
||||
eprintln!("{origin_timestamp} != {timestamp}");
|
||||
return Err(Error::InvalidResponseOriginTimestamp);
|
||||
}
|
||||
|
||||
let timestamp = u64::from_be(response.transmit_timestamp);
|
||||
time_from_timestamp(timestamp)
|
||||
}
|
||||
|
||||
fn send_request(socket: &UdpSocket, server: &SocketAddr) -> Result<u64, Error> {
|
||||
let send_timestamp = timestamp_now();
|
||||
let client_packet = NtpPacketHeader {
|
||||
status: (4u8 << 3) | 3,
|
||||
ignored: [0; 3],
|
||||
root_delay: 0,
|
||||
root_dispersion: 0,
|
||||
reference_id: 0,
|
||||
reference_timestamp: 0,
|
||||
origin_timestamp: 0,
|
||||
receive_timestamp: 0,
|
||||
transmit_timestamp: send_timestamp.to_be(),
|
||||
};
|
||||
socket.send_to(bytemuck::bytes_of(&client_packet), server)?;
|
||||
Ok(send_timestamp)
|
||||
}
|
||||
|
||||
fn do_request(server: SocketAddr) -> Result<DateTime<Utc>, Error> {
|
||||
const ATTEMPTS: usize = 5;
|
||||
const TIMEOUT_MAX: u64 = 3000;
|
||||
|
||||
let mut poll = Poll::new()?;
|
||||
let mut timer = TimerFd::new()?;
|
||||
let mut buffer = NtpPacketHeader::zeroed();
|
||||
|
||||
let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))?;
|
||||
let mut timeout = 500;
|
||||
let mut response = None;
|
||||
|
||||
poll.add(&timer)?;
|
||||
poll.add(&socket)?;
|
||||
|
||||
for attempt in 0..ATTEMPTS {
|
||||
let send_timestamp = send_request(&socket, &server)?;
|
||||
timer.start(Duration::from_millis(timeout))?;
|
||||
timeout = (timeout * 2).min(TIMEOUT_MAX);
|
||||
|
||||
// Wait for response
|
||||
loop {
|
||||
let event = poll.wait(None)?;
|
||||
match event {
|
||||
Some(fd) if fd == socket.as_raw_fd() => {
|
||||
let (len, remote) = match socket.recv_from(bytemuck::bytes_of_mut(&mut buffer))
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
eprintln!("Receive error: {error}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if remote != server || len < size_of::<NtpPacketHeader>() {
|
||||
eprintln!("Remote != server or len too small");
|
||||
continue;
|
||||
}
|
||||
let t = handle_response(buffer, send_timestamp)
|
||||
.inspect_err(|e| {
|
||||
eprintln!("Response from {server}: {e}");
|
||||
})
|
||||
.ok();
|
||||
if let Some(t) = t {
|
||||
response = Some(t);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(fd) if fd == timer.as_raw_fd() => {
|
||||
break;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
if response.is_some() {
|
||||
break;
|
||||
}
|
||||
|
||||
eprintln!("[{}/{}] Timed out", attempt + 1, ATTEMPTS);
|
||||
}
|
||||
|
||||
response.ok_or(Error::TimedOut)
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Args {
|
||||
#[clap(short, long, help = "Run in a loop, fetching new time every N seconds")]
|
||||
interval: Option<u64>,
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
help = "Only print received time, do not change any settings"
|
||||
)]
|
||||
dry_run: bool,
|
||||
server: String,
|
||||
}
|
||||
|
||||
fn handle_time(args: &Args, time: DateTime<Utc>) -> Result<(), Error> {
|
||||
if args.interval.is_none() {
|
||||
println!("{time}");
|
||||
} else {
|
||||
log::info!("{time}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_server(args: &Args) -> Result<SocketAddr, Error> {
|
||||
let mut server = args.server.to_socket_addrs()?;
|
||||
let server = server.next().ok_or(Error::InvalidServerAddress)?;
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
fn run_loop(args: &Args, interval: Duration) -> Result<(), Error> {
|
||||
let server = resolve_server(args)?;
|
||||
|
||||
loop {
|
||||
let response = do_request(server);
|
||||
|
||||
match response {
|
||||
Ok(time) => {
|
||||
if let Err(error) = handle_time(args, time) {
|
||||
log::error!("handle_time(): {error}");
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("{}: {error}", args.server);
|
||||
}
|
||||
}
|
||||
|
||||
std::thread::sleep(interval);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_once(args: &Args) -> Result<(), Error> {
|
||||
let server = resolve_server(args)?;
|
||||
let response = do_request(server)?;
|
||||
handle_time(args, response)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run(args: &Args) -> Result<(), Error> {
|
||||
match args.interval {
|
||||
Some(interval) => {
|
||||
let interval = Duration::from_secs(interval);
|
||||
run_loop(args, interval)
|
||||
}
|
||||
None => run_once(args),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
logsink::setup_logging(true);
|
||||
let args = Args::parse();
|
||||
match run(&args) {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
@ -60,6 +60,7 @@ const PROGRAMS: &[(&str, &str)] = &[
|
||||
("dnsq", "bin/dnsq"),
|
||||
("ping", "bin/ping"),
|
||||
("ncap", "sbin/ncap"),
|
||||
("ntpc", "bin/ntpc"),
|
||||
// colors
|
||||
("colors", "bin/colors"),
|
||||
("colors-bar", "bin/colors-bar"),
|
||||
|
Loading…
x
Reference in New Issue
Block a user