use core::fmt;
pub const HEADER_LEN: usize = 48;
pub const UNIX_EPOCH_OFFSET: u64 = 2_208_988_800;
const FRAC: f64 = 4_294_967_296.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct NtpTimestamp(pub u64);
impl NtpTimestamp {
pub const ZERO: NtpTimestamp = NtpTimestamp(0);
pub fn from_parts(seconds: u32, fraction: u32) -> Self {
NtpTimestamp(((seconds as u64) << 32) | fraction as u64)
}
pub fn seconds(self) -> u32 {
(self.0 >> 32) as u32
}
pub fn fraction(self) -> u32 {
self.0 as u32
}
pub fn from_unix(secs: i64, nanos: u32) -> Self {
let ntp_secs = (secs.wrapping_add(UNIX_EPOCH_OFFSET as i64)) as u64;
let frac = ((nanos as u64) << 32) / 1_000_000_000;
NtpTimestamp(((ntp_secs & 0xFFFF_FFFF) << 32) | (frac & 0xFFFF_FFFF))
}
pub fn seconds_since(self, earlier: NtpTimestamp) -> f64 {
let diff = self.0.wrapping_sub(earlier.0) as i64;
diff as f64 / FRAC
}
pub fn is_zero(self) -> bool {
self.0 == 0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct NtpShort(pub u32);
impl NtpShort {
pub fn to_seconds(self) -> f64 {
self.0 as f64 / 65_536.0
}
pub fn from_seconds(s: f64) -> Self {
let clamped = s.clamp(0.0, 65_535.999);
NtpShort((clamped * 65_536.0) as u32)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LeapIndicator {
NoWarning,
LastMinute61,
LastMinute59,
Unsynchronized,
}
impl LeapIndicator {
fn from_bits(b: u8) -> Self {
match b & 0b11 {
0 => LeapIndicator::NoWarning,
1 => LeapIndicator::LastMinute61,
2 => LeapIndicator::LastMinute59,
_ => LeapIndicator::Unsynchronized,
}
}
fn bits(self) -> u8 {
match self {
LeapIndicator::NoWarning => 0,
LeapIndicator::LastMinute61 => 1,
LeapIndicator::LastMinute59 => 2,
LeapIndicator::Unsynchronized => 3,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Mode {
Reserved,
SymmetricActive,
SymmetricPassive,
Client,
Server,
Broadcast,
Control,
Private,
}
impl Mode {
fn from_bits(b: u8) -> Self {
match b & 0b111 {
1 => Mode::SymmetricActive,
2 => Mode::SymmetricPassive,
3 => Mode::Client,
4 => Mode::Server,
5 => Mode::Broadcast,
6 => Mode::Control,
7 => Mode::Private,
_ => Mode::Reserved,
}
}
fn bits(self) -> u8 {
match self {
Mode::Reserved => 0,
Mode::SymmetricActive => 1,
Mode::SymmetricPassive => 2,
Mode::Client => 3,
Mode::Server => 4,
Mode::Broadcast => 5,
Mode::Control => 6,
Mode::Private => 7,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ParseError {
TooShort { len: usize },
BadVersion { version: u8 },
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::TooShort { len } => {
write!(f, "packet is {len} bytes; NTP header needs {HEADER_LEN}")
}
ParseError::BadVersion { version } => {
write!(f, "unsupported NTP version {version} (expected 3 or 4)")
}
}
}
}
impl std::error::Error for ParseError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct NtpPacket {
pub leap: LeapIndicator,
pub version: u8,
pub mode: Mode,
pub stratum: u8,
pub poll: i8,
pub precision: i8,
pub root_delay: NtpShort,
pub root_dispersion: NtpShort,
pub reference_id: [u8; 4],
pub reference_ts: NtpTimestamp,
pub origin_ts: NtpTimestamp,
pub receive_ts: NtpTimestamp,
pub transmit_ts: NtpTimestamp,
}
fn read_u32(buf: &[u8], at: usize) -> u32 {
let mut b = [0u8; 4];
if let Some(s) = buf.get(at..at + 4) {
b.copy_from_slice(s);
}
u32::from_be_bytes(b)
}
fn read_u64(buf: &[u8], at: usize) -> u64 {
let mut b = [0u8; 8];
if let Some(s) = buf.get(at..at + 8) {
b.copy_from_slice(s);
}
u64::from_be_bytes(b)
}
impl NtpPacket {
pub fn client_request(version: u8, transmit_ts: NtpTimestamp) -> Self {
NtpPacket {
leap: LeapIndicator::NoWarning,
version,
mode: Mode::Client,
stratum: 0,
poll: 0,
precision: 0x20u8 as i8,
root_delay: NtpShort(0),
root_dispersion: NtpShort(0),
reference_id: [0; 4],
reference_ts: NtpTimestamp::ZERO,
origin_ts: NtpTimestamp::ZERO,
receive_ts: NtpTimestamp::ZERO,
transmit_ts,
}
}
pub fn parse(buf: &[u8]) -> Result<NtpPacket, ParseError> {
if buf.len() < HEADER_LEN {
return Err(ParseError::TooShort { len: buf.len() });
}
let b0 = buf[0];
let version = (b0 >> 3) & 0b111;
if !(3..=4).contains(&version) {
return Err(ParseError::BadVersion { version });
}
let mut reference_id = [0u8; 4];
reference_id.copy_from_slice(&buf[12..16]);
Ok(NtpPacket {
leap: LeapIndicator::from_bits(b0 >> 6),
version,
mode: Mode::from_bits(b0),
stratum: buf[1],
poll: buf[2] as i8,
precision: buf[3] as i8,
root_delay: NtpShort(read_u32(buf, 4)),
root_dispersion: NtpShort(read_u32(buf, 8)),
reference_id,
reference_ts: NtpTimestamp(read_u64(buf, 16)),
origin_ts: NtpTimestamp(read_u64(buf, 24)),
receive_ts: NtpTimestamp(read_u64(buf, 32)),
transmit_ts: NtpTimestamp(read_u64(buf, 40)),
})
}
pub fn write(&self, buf: &mut [u8; HEADER_LEN]) {
buf[0] = (self.leap.bits() << 6) | ((self.version & 0b111) << 3) | self.mode.bits();
buf[1] = self.stratum;
buf[2] = self.poll as u8;
buf[3] = self.precision as u8;
buf[4..8].copy_from_slice(&self.root_delay.0.to_be_bytes());
buf[8..12].copy_from_slice(&self.root_dispersion.0.to_be_bytes());
buf[12..16].copy_from_slice(&self.reference_id);
buf[16..24].copy_from_slice(&self.reference_ts.0.to_be_bytes());
buf[24..32].copy_from_slice(&self.origin_ts.0.to_be_bytes());
buf[32..40].copy_from_slice(&self.receive_ts.0.to_be_bytes());
buf[40..48].copy_from_slice(&self.transmit_ts.0.to_be_bytes());
}
pub fn to_bytes(&self) -> [u8; HEADER_LEN] {
let mut buf = [0u8; HEADER_LEN];
self.write(&mut buf);
buf
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ExtensionField<'a> {
pub field_type: u16,
pub value: &'a [u8],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Trailer<'a> {
Extension(ExtensionField<'a>),
Opaque(&'a [u8]),
}
pub fn extension_fields(packet: &[u8]) -> ExtensionIter<'_> {
let rest = packet.get(HEADER_LEN..).unwrap_or(&[]);
ExtensionIter { rest }
}
pub struct ExtensionIter<'a> {
rest: &'a [u8],
}
impl<'a> Iterator for ExtensionIter<'a> {
type Item = Trailer<'a>;
fn next(&mut self) -> Option<Trailer<'a>> {
if self.rest.is_empty() {
return None;
}
if self.rest.len() >= 4 {
let field_type = u16::from_be_bytes([self.rest[0], self.rest[1]]);
let len = u16::from_be_bytes([self.rest[2], self.rest[3]]) as usize;
if len >= 16 && len.is_multiple_of(4) && len <= self.rest.len() {
let value = &self.rest[4..len];
self.rest = &self.rest[len..];
return Some(Trailer::Extension(ExtensionField { field_type, value }));
}
}
let opaque = self.rest;
self.rest = &[];
Some(Trailer::Opaque(opaque))
}
}
pub fn offset_delay(t1: f64, t2: f64, t3: f64, t4: f64) -> (f64, f64) {
let offset = ((t2 - t1) + (t3 - t4)) / 2.0;
let delay = (t4 - t1) - (t3 - t2);
(offset, delay)
}
#[cfg(test)]
mod precision_tests {
use super::*;
const TICK: f64 = 1.0 / 4_294_967_296.0;
#[test]
fn differences_keep_sub_nanosecond_resolution() {
let secs = 1_787_856_000i64;
let a = NtpTimestamp::from_unix(secs, 0);
let b = NtpTimestamp::from_unix(secs, 1);
let exact = b.seconds_since(a);
assert!(
exact > 0.0,
"a 1 ns step vanished entirely in the fixed-point difference"
);
assert!(
(exact - 1e-9).abs() <= TICK,
"fixed-point difference gave {exact} s for a 1 ns step"
);
let ulp = (secs as f64).next_up() - secs as f64;
assert!(
ulp > 200e-9,
"this test assumes an f64 at the Unix epoch is coarse; ULP is {ulp} s"
);
}
#[test]
fn the_2038_exponent_step_does_not_reach_the_difference() {
let secs = 2_147_500_000i64;
let a = NtpTimestamp::from_unix(secs, 0);
let b = NtpTimestamp::from_unix(secs, 100);
let exact = b.seconds_since(a);
assert!(
(exact - 100e-9).abs() <= TICK,
"a 100 ns step past 2038 measured as {exact} s"
);
let ulp = (secs as f64).next_up() - secs as f64;
assert!(
ulp > 400e-9,
"expected the post-2038 f64 gap to exceed 400 ns, got {ulp} s"
);
}
#[test]
fn a_difference_spans_the_era_boundary() {
let before = NtpTimestamp::from_unix(2_085_978_495, 0);
let after = NtpTimestamp::from_unix(2_085_978_497, 0);
let delta = after.seconds_since(before);
assert!(
(delta - 2.0).abs() <= TICK,
"two seconds across the era wrap measured as {delta}"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_header() {
let p = NtpPacket {
leap: LeapIndicator::NoWarning,
version: 4,
mode: Mode::Server,
stratum: 2,
poll: 6,
precision: -20,
root_delay: NtpShort::from_seconds(0.015),
root_dispersion: NtpShort::from_seconds(0.002),
reference_id: *b"GPS\0",
reference_ts: NtpTimestamp::from_unix(1_756_200_000, 0),
origin_ts: NtpTimestamp(0x0102_0304_0506_0708),
receive_ts: NtpTimestamp::from_unix(1_756_200_100, 500_000_000),
transmit_ts: NtpTimestamp::from_unix(1_756_200_100, 500_100_000),
};
let bytes = p.to_bytes();
let q = NtpPacket::parse(&bytes).expect("parse back");
assert_eq!(p, q);
}
#[test]
fn field_offsets_match_rfc5905() {
let mut p = NtpPacket::client_request(4, NtpTimestamp(0xAABB_CCDD_EEFF_0011));
p.origin_ts = NtpTimestamp(0x1111_1111_1111_1111);
p.receive_ts = NtpTimestamp(0x2222_2222_2222_2222);
p.reference_ts = NtpTimestamp(0x3333_3333_3333_3333);
let b = p.to_bytes();
assert_eq!(b[0], 0b00_100_011); assert_eq!(&b[16..24], &[0x33; 8]); assert_eq!(&b[24..32], &[0x11; 8]); assert_eq!(&b[32..40], &[0x22; 8]); assert_eq!(&b[40..48], &0xAABB_CCDD_EEFF_0011u64.to_be_bytes()); }
#[test]
fn short_and_bad_version_are_errors() {
assert_eq!(
NtpPacket::parse(&[0u8; 20]),
Err(ParseError::TooShort { len: 20 })
);
let mut b = [0u8; 48];
b[0] = 2 << 3; assert_eq!(
NtpPacket::parse(&b),
Err(ParseError::BadVersion { version: 2 })
);
}
#[test]
fn timestamp_wraparound_diff() {
let before = NtpTimestamp(u64::MAX - (1u64 << 31)); let after = NtpTimestamp(1u64 << 31); let d = after.seconds_since(before);
assert!((d - 1.0).abs() < 1e-9, "got {d}");
}
#[test]
fn exchange_math() {
let t1 = 10.000; let t2 = 10.125; let t3 = 10.126;
let t4 = 10.051; let (offset, delay) = offset_delay(t1, t2, t3, t4);
assert!((offset - 0.100).abs() < 1e-9, "offset {offset}");
assert!((delay - 0.050).abs() < 1e-9, "delay {delay}");
}
#[test]
fn extension_iteration_handles_garbage() {
let mut buf = vec![0u8; 48];
buf[0] = (4 << 3) | 3;
buf.extend_from_slice(&0x0104u16.to_be_bytes()); buf.extend_from_slice(&16u16.to_be_bytes()); buf.extend_from_slice(&[0xAB; 12]); buf.extend_from_slice(&[1, 2, 3]); let items: Vec<_> = extension_fields(&buf).collect();
assert_eq!(items.len(), 2);
match items[0] {
Trailer::Extension(ef) => {
assert_eq!(ef.field_type, 0x0104);
assert_eq!(ef.value, &[0xAB; 12][..]);
}
_ => panic!("expected extension"),
}
assert!(matches!(items[1], Trailer::Opaque(&[1, 2, 3])));
}
}