#![warn(clippy::missing_const_for_fn)]
use crate::{
NtpClock, NtpDuration, NtpLeapIndicator, NtpTimestamp, PollInterval, SystemSnapshot,
io::NonBlockingWrite,
};
use rand::random;
mod error;
pub mod extension_fields;
pub mod server_reference_id;
use crate::packet::error::ParsingError;
pub use error::V5Error;
use super::RequestIdentifier;
pub(crate) const DRAFT_VERSION: &str = "draft-ietf-ntp-ntpv5-06";
pub(crate) const UPGRADE_TIMESTAMP: NtpTimestamp = NtpTimestamp::from_bits(*b"NTP5DRFT");
#[repr(u8)]
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum NtpMode {
Request = 3,
Response = 4,
}
impl NtpMode {
const fn from_bits(bits: u8) -> Result<Self, ParsingError<std::convert::Infallible>> {
Ok(match bits {
3 => Self::Request,
4 => Self::Response,
_ => return Err(V5Error::MalformedMode.into_parse_err()),
})
}
const fn to_bits(self) -> u8 {
self as u8
}
#[cfg(test)]
pub(crate) const fn is_request(self) -> bool {
matches!(self, Self::Request)
}
#[cfg(test)]
pub(crate) const fn is_response(self) -> bool {
matches!(self, Self::Response)
}
}
#[repr(u8)]
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum NtpTimescale {
Utc = 0,
Tai = 1,
Ut1 = 2,
LeapSmearedUtc = 3,
}
impl NtpTimescale {
const fn from_bits(bits: u8) -> Result<Self, ParsingError<std::convert::Infallible>> {
Ok(match bits {
0 => Self::Utc,
1 => Self::Tai,
2 => Self::Ut1,
3 => Self::LeapSmearedUtc,
_ => return Err(V5Error::MalformedTimescale.into_parse_err()),
})
}
const fn to_bits(self) -> u8 {
self as u8
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct NtpEra(pub u8);
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct NtpFlags {
pub synchronized: bool,
pub interleaved_mode: bool,
pub authnak: bool,
}
impl NtpFlags {
const fn from_bits(bits: [u8; 2]) -> Result<Self, ParsingError<std::convert::Infallible>> {
if bits[0] != 0x00 || bits[1] & 0b1111_1000 != 0 {
return Err(V5Error::InvalidFlags.into_parse_err());
}
Ok(Self {
synchronized: bits[1] & 0b01 != 0,
interleaved_mode: bits[1] & 0b10 != 0,
authnak: bits[1] & 0b100 != 0,
})
}
const fn as_bits(self) -> [u8; 2] {
let mut flags: u8 = 0;
if self.synchronized {
flags |= 0b01;
}
if self.interleaved_mode {
flags |= 0b10;
}
if self.authnak {
flags |= 0b100;
}
[0x00, flags]
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct NtpServerCookie(pub [u8; 8]);
impl NtpServerCookie {
fn new_random() -> Self {
Self(random())
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct NtpClientCookie(pub [u8; 8]);
impl NtpClientCookie {
fn new_random() -> Self {
Self(random())
}
pub const fn from_ntp_timestamp(ts: NtpTimestamp) -> Self {
Self(ts.to_bits())
}
pub const fn into_ntp_timestamp(self) -> NtpTimestamp {
NtpTimestamp::from_bits(self.0)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct NtpHeaderV5 {
pub leap: NtpLeapIndicator,
pub mode: NtpMode,
pub stratum: u8,
pub poll: PollInterval,
pub precision: i8,
pub timescale: NtpTimescale,
pub era: NtpEra,
pub flags: NtpFlags,
pub root_delay: NtpDuration,
pub root_dispersion: NtpDuration,
pub server_cookie: NtpServerCookie,
pub client_cookie: NtpClientCookie,
pub receive_timestamp: NtpTimestamp,
pub transmit_timestamp: NtpTimestamp,
}
impl NtpHeaderV5 {
fn new() -> Self {
Self {
leap: NtpLeapIndicator::NoWarning,
mode: NtpMode::Request,
stratum: 0,
poll: PollInterval::from_byte(0),
precision: 0,
root_delay: NtpDuration::default(),
root_dispersion: NtpDuration::default(),
receive_timestamp: NtpTimestamp::default(),
transmit_timestamp: NtpTimestamp::default(),
timescale: NtpTimescale::Utc,
era: NtpEra(0),
flags: NtpFlags {
synchronized: false,
interleaved_mode: false,
authnak: false,
},
server_cookie: NtpServerCookie([0; 8]),
client_cookie: NtpClientCookie([0; 8]),
}
}
pub(crate) fn timestamp_response<C: NtpClock>(
system: &SystemSnapshot,
input: Self,
recv_timestamp: NtpTimestamp,
clock: &C,
) -> Self {
Self {
leap: system.time_snapshot.leap_indicator,
mode: NtpMode::Response,
stratum: system.stratum,
poll: input.poll,
precision: system.time_snapshot.precision.log2(),
timescale: NtpTimescale::Utc,
era: NtpEra(0),
flags: NtpFlags {
synchronized: system.stratum < 16,
interleaved_mode: false,
authnak: false,
},
root_delay: system.time_snapshot.root_delay,
root_dispersion: system.time_snapshot.root_dispersion(recv_timestamp),
server_cookie: NtpServerCookie::new_random(),
client_cookie: input.client_cookie,
receive_timestamp: recv_timestamp,
transmit_timestamp: clock.now().expect("Failed to read time"),
}
}
fn kiss_response(packet_from_client: Self) -> Self {
Self {
mode: NtpMode::Response,
flags: NtpFlags {
synchronized: false,
interleaved_mode: false,
authnak: false,
},
server_cookie: NtpServerCookie::new_random(),
client_cookie: packet_from_client.client_cookie,
stratum: 0,
..Self::new()
}
}
pub(crate) fn rate_limit_response(packet_from_client: Self) -> Self {
Self {
poll: packet_from_client.poll.force_inc(),
..Self::kiss_response(packet_from_client)
}
}
pub(crate) fn deny_response(packet_from_client: Self) -> Self {
Self {
poll: PollInterval::NEVER,
..Self::kiss_response(packet_from_client)
}
}
pub(crate) fn nts_nak_response(packet_from_client: Self) -> Self {
Self {
flags: NtpFlags {
synchronized: false,
interleaved_mode: false,
authnak: true,
},
..Self::kiss_response(packet_from_client)
}
}
const WIRE_LENGTH: usize = 48;
const VERSION: u8 = 5;
fn fix_leap_indicator(mut self) -> Self {
if self.flags.synchronized && self.leap == NtpLeapIndicator::Unsynchronized {
self.leap = NtpLeapIndicator::Unknown;
} else if !self.flags.synchronized {
self.leap = NtpLeapIndicator::Unsynchronized;
}
self
}
pub(crate) fn deserialize(
data: &[u8],
) -> Result<(Self, usize), ParsingError<std::convert::Infallible>> {
if data.len() < Self::WIRE_LENGTH {
return Err(ParsingError::IncorrectLength);
}
let version = (data[0] >> 3) & 0b111;
if version != 5 {
return Err(ParsingError::InvalidVersion(version));
}
Ok((
Self {
leap: NtpLeapIndicator::from_bits((data[0] & 0xC0) >> 6),
mode: NtpMode::from_bits(data[0] & 0x07)?,
stratum: data[1],
poll: PollInterval::from_byte(data[2]),
precision: data[3] as i8,
root_delay: NtpDuration::from_bits_time32(data[4..8].try_into().unwrap()),
root_dispersion: NtpDuration::from_bits_time32(data[8..12].try_into().unwrap()),
timescale: NtpTimescale::from_bits(data[12])?,
era: NtpEra(data[13]),
flags: NtpFlags::from_bits(data[14..16].try_into().unwrap())?,
server_cookie: NtpServerCookie(data[16..24].try_into().unwrap()),
client_cookie: NtpClientCookie(data[24..32].try_into().unwrap()),
receive_timestamp: NtpTimestamp::from_bits(data[32..40].try_into().unwrap()),
transmit_timestamp: NtpTimestamp::from_bits(data[40..48].try_into().unwrap()),
}
.fix_leap_indicator(),
Self::WIRE_LENGTH,
))
}
pub(crate) fn serialize(&self, mut w: impl NonBlockingWrite) -> std::io::Result<()> {
w.write_all(&[(self.leap.to_bits() << 6) | (Self::VERSION << 3) | self.mode.to_bits()])?;
w.write_all(&[self.stratum, self.poll.as_byte(), self.precision as u8])?;
w.write_all(&self.root_delay.to_bits_time32())?;
w.write_all(&self.root_dispersion.to_bits_time32())?;
w.write_all(&[self.timescale.to_bits()])?;
w.write_all(&[self.era.0])?;
w.write_all(&self.flags.as_bits())?;
w.write_all(&self.server_cookie.0)?;
w.write_all(&self.client_cookie.0)?;
w.write_all(&self.receive_timestamp.to_bits())?;
w.write_all(&self.transmit_timestamp.to_bits())?;
Ok(())
}
pub fn poll_message(poll_interval: PollInterval) -> (Self, RequestIdentifier) {
let mut packet = Self::new();
packet.poll = poll_interval;
packet.mode = NtpMode::Request;
let client_cookie = NtpClientCookie::new_random();
packet.client_cookie = client_cookie;
(
packet,
RequestIdentifier {
expected_origin_timestamp: client_cookie.into_ntp_timestamp(),
uid: None,
},
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn round_trip_timescale() {
for i in 0..=u8::MAX {
if let Ok(ts) = NtpTimescale::from_bits(i) {
assert_eq!(ts as u8, i);
}
}
}
#[test]
fn flags() {
let flags = NtpFlags::from_bits([0x00, 0x00]).unwrap();
assert!(!flags.synchronized);
assert!(!flags.interleaved_mode);
let flags = NtpFlags::from_bits([0x00, 0x01]).unwrap();
assert!(flags.synchronized);
assert!(!flags.interleaved_mode);
let flags = NtpFlags::from_bits([0x00, 0x02]).unwrap();
assert!(!flags.synchronized);
assert!(flags.interleaved_mode);
let flags = NtpFlags::from_bits([0x00, 0x03]).unwrap();
assert!(flags.synchronized);
assert!(flags.interleaved_mode);
let result = NtpFlags::from_bits([0xFF, 0xFF]);
assert!(matches!(
result,
Err(ParsingError::V5(V5Error::InvalidFlags))
));
}
#[test]
fn parse_request() {
#[expect(clippy::unusual_byte_groupings)] #[rustfmt::skip]
let data = [
0b_11_101_011,
0x00,
0x05,
0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x02,
0x00,
0x00,
0b0000_00_1_0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let (parsed, len) = NtpHeaderV5::deserialize(&data).unwrap();
assert_eq!(len, 48);
assert_eq!(parsed.leap, NtpLeapIndicator::Unsynchronized);
assert!(parsed.mode.is_request());
assert_eq!(parsed.stratum, 0);
assert_eq!(parsed.poll, PollInterval::from_byte(5));
assert_eq!(parsed.precision, 0);
assert_eq!(parsed.timescale, NtpTimescale::Ut1);
assert_eq!(parsed.era, NtpEra(0));
assert!(parsed.flags.interleaved_mode);
assert!(!parsed.flags.synchronized);
assert!(parsed.flags.interleaved_mode);
assert_eq!(parsed.root_delay, NtpDuration::from_seconds(0.0));
assert_eq!(parsed.root_dispersion, NtpDuration::from_seconds(0.0));
assert_eq!(parsed.server_cookie, NtpServerCookie([0x0; 8]));
assert_eq!(
parsed.client_cookie,
NtpClientCookie([0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01])
);
assert_eq!(parsed.receive_timestamp, NtpTimestamp::from_fixed_int(0x0));
assert_eq!(parsed.transmit_timestamp, NtpTimestamp::from_fixed_int(0x0));
let mut buffer: [u8; 48] = [0u8; 48];
let cursor = Cursor::new(buffer.as_mut_slice());
parsed.serialize(cursor).unwrap();
assert_eq!(data, buffer);
}
#[test]
fn parse_response() {
#[expect(clippy::unusual_byte_groupings)] #[rustfmt::skip]
let data = [
0b_00_101_100,
0x04,
0x05,
0x06,
0x10, 0x00, 0x00, 0x00,
0x20, 0x00, 0x00, 0x00,
0x01,
0x07,
0x00,
0b0000_00_1_1,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
];
let (parsed, len) = NtpHeaderV5::deserialize(&data).unwrap();
assert_eq!(len, 48);
assert_eq!(parsed.leap, NtpLeapIndicator::NoWarning);
assert!(parsed.mode.is_response());
assert_eq!(parsed.stratum, 4);
assert_eq!(parsed.poll, PollInterval::from_byte(5));
assert_eq!(parsed.precision, 6);
assert_eq!(parsed.timescale, NtpTimescale::Tai);
assert_eq!(parsed.era, NtpEra(7));
assert!(parsed.flags.interleaved_mode);
assert!(parsed.flags.synchronized);
assert!(parsed.flags.interleaved_mode);
assert_eq!(parsed.root_delay, NtpDuration::from_seconds(1.0));
assert_eq!(parsed.root_dispersion, NtpDuration::from_seconds(2.0));
assert_eq!(
parsed.server_cookie,
NtpServerCookie([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08])
);
assert_eq!(
parsed.client_cookie,
NtpClientCookie([0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01])
);
assert_eq!(
parsed.receive_timestamp,
NtpTimestamp::from_fixed_int(0x1111111111111111)
);
assert_eq!(
parsed.transmit_timestamp,
NtpTimestamp::from_fixed_int(0x2222222222222222)
);
let mut buffer: [u8; 48] = [0u8; 48];
let cursor = Cursor::new(buffer.as_mut_slice());
parsed.serialize(cursor).unwrap();
assert_eq!(data, buffer);
}
#[test]
fn parse_response_unsynchronized() {
#[expect(clippy::unusual_byte_groupings)] #[rustfmt::skip]
let data = [
0b_00_101_100,
0x04,
0x05,
0x06,
0x10, 0x00, 0x00, 0x00,
0x20, 0x00, 0x00, 0x00,
0x01,
0x07,
0x00,
0b0000_00_1_0,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
];
let (parsed, len) = NtpHeaderV5::deserialize(&data).unwrap();
assert_eq!(len, 48);
assert_eq!(parsed.leap, NtpLeapIndicator::Unsynchronized);
assert!(parsed.mode.is_response());
assert_eq!(parsed.stratum, 4);
assert_eq!(parsed.poll, PollInterval::from_byte(5));
assert_eq!(parsed.precision, 6);
assert_eq!(parsed.timescale, NtpTimescale::Tai);
assert_eq!(parsed.era, NtpEra(7));
assert!(parsed.flags.interleaved_mode);
assert!(!parsed.flags.synchronized);
assert!(parsed.flags.interleaved_mode);
assert_eq!(parsed.root_delay, NtpDuration::from_seconds(1.0));
assert_eq!(parsed.root_dispersion, NtpDuration::from_seconds(2.0));
assert_eq!(
parsed.server_cookie,
NtpServerCookie([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08])
);
assert_eq!(
parsed.client_cookie,
NtpClientCookie([0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01])
);
assert_eq!(
parsed.receive_timestamp,
NtpTimestamp::from_fixed_int(0x1111111111111111)
);
assert_eq!(
parsed.transmit_timestamp,
NtpTimestamp::from_fixed_int(0x2222222222222222)
);
}
#[test]
fn test_encode_decode_roundtrip() {
for i in 0..=u8::MAX {
let header = NtpHeaderV5 {
leap: NtpLeapIndicator::from_bits(i % 4),
mode: NtpMode::from_bits(3 + (i % 2)).unwrap(),
stratum: i.wrapping_add(1),
poll: PollInterval::from_byte(i.wrapping_add(3)),
precision: i.wrapping_add(4) as i8,
timescale: NtpTimescale::from_bits(i % 4).unwrap(),
era: NtpEra(i.wrapping_add(6)),
flags: NtpFlags {
synchronized: true,
interleaved_mode: i % 4 == 0,
authnak: i % 5 == 0,
},
root_delay: NtpDuration::from_bits_time32([i; 4]),
root_dispersion: NtpDuration::from_bits_time32([i.wrapping_add(1); 4]),
server_cookie: NtpServerCookie([i.wrapping_add(2); 8]),
client_cookie: NtpClientCookie([i.wrapping_add(3); 8]),
receive_timestamp: NtpTimestamp::from_bits([i.wrapping_add(4); 8]),
transmit_timestamp: NtpTimestamp::from_bits([i.wrapping_add(5); 8]),
}
.fix_leap_indicator();
let mut buffer: [u8; 48] = [0u8; 48];
let mut cursor = Cursor::new(buffer.as_mut_slice());
header.serialize(&mut cursor).unwrap();
let (parsed, _) = NtpHeaderV5::deserialize(&buffer).unwrap();
assert_eq!(header, parsed);
}
}
#[test]
fn fail_on_incorrect_length() {
let data: [u8; 47] = [0u8; 47];
assert!(matches!(
NtpHeaderV5::deserialize(&data),
Err(ParsingError::IncorrectLength)
));
}
#[test]
#[expect(clippy::unusual_byte_groupings)] fn fail_on_incorrect_version() {
let mut data: [u8; 48] = [0u8; 48];
data[0] = 0b_00_111_100;
assert!(matches!(
NtpHeaderV5::deserialize(&data),
Err(ParsingError::InvalidVersion(7))
));
}
}