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",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ntpc"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
"chrono",
|
||||||
|
"clap",
|
||||||
|
"cross",
|
||||||
|
"log",
|
||||||
|
"logsink",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint-dig"
|
name = "num-bigint-dig"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
@ -17,6 +17,7 @@ members = [
|
|||||||
"tools/crypt",
|
"tools/crypt",
|
||||||
"tools/init",
|
"tools/init",
|
||||||
"tools/md2txt",
|
"tools/md2txt",
|
||||||
|
"tools/ntpc",
|
||||||
"tools/rdb",
|
"tools/rdb",
|
||||||
"tools/red",
|
"tools/red",
|
||||||
"tools/rsh",
|
"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 mem;
|
||||||
pub mod net;
|
pub mod net;
|
||||||
pub mod signal;
|
pub mod signal;
|
||||||
|
pub mod time;
|
||||||
|
@ -5,6 +5,7 @@ pub mod pipe;
|
|||||||
pub mod poll;
|
pub mod poll;
|
||||||
pub mod socket;
|
pub mod socket;
|
||||||
pub mod term;
|
pub mod term;
|
||||||
|
pub mod time;
|
||||||
pub mod timer;
|
pub mod timer;
|
||||||
|
|
||||||
use std::io;
|
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"),
|
("dnsq", "bin/dnsq"),
|
||||||
("ping", "bin/ping"),
|
("ping", "bin/ping"),
|
||||||
("ncap", "sbin/ncap"),
|
("ncap", "sbin/ncap"),
|
||||||
|
("ntpc", "bin/ntpc"),
|
||||||
// colors
|
// colors
|
||||||
("colors", "bin/colors"),
|
("colors", "bin/colors"),
|
||||||
("colors-bar", "bin/colors-bar"),
|
("colors-bar", "bin/colors-bar"),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user