#![allow(
clippy::missing_errors_doc,
clippy::missing_panics_doc,
reason = "TODO: Shamik please add documentation."
)]
#![allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
reason = "TODO: Shamik please address various warning associated with casting."
)]
use std::fmt::Display;
use bon::Builder;
use nom::Parser;
use nom::number::{be_i8, be_u8};
use thiserror::Error;
mod extension;
mod header;
mod short;
mod timestamp;
pub use extension::{ExtensionField, Fec2V1Value};
pub use header::{LeapIndicator, Mode, Version};
pub use short::Short;
pub use timestamp::Timestamp;
use crate::daemon::event::{NtpData, Stratum, TryFromU8Error};
use crate::daemon::time::{Duration as ClockBoundDuration, Instant as ClockBoundInstant};
#[derive(Debug, Clone, PartialEq, Eq, Builder)]
pub struct Packet {
#[builder(default = LeapIndicator::NoWarning)]
pub leap_indicator: LeapIndicator,
#[builder(default = Version::V4)]
pub version: Version,
#[builder(default = Mode::Client)]
pub mode: Mode,
#[builder(default = 0)]
pub stratum: u8,
#[builder(default = 0)]
pub poll: u8,
#[builder(default = 0)]
pub precision: i8,
#[builder(default = Short::new(0))]
pub root_delay: Short,
#[builder(default = Short::new(0))]
pub root_dispersion: Short,
#[builder(default = [0u8; 4])]
pub reference_id: [u8; 4],
#[builder(default = Timestamp::new(0))]
pub reference_timestamp: Timestamp,
#[builder(default = Timestamp::new(0))]
pub origin_timestamp: Timestamp,
#[builder(default = Timestamp::new(0))]
pub receive_timestamp: Timestamp,
#[builder(default = Timestamp::new(0))]
pub transmit_timestamp: Timestamp,
#[builder(default = Vec::new())]
pub extensions: Vec<ExtensionField>,
}
impl Display for Packet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Packet")?;
writeln!(f, "leap indicator: {:?}", self.leap_indicator)?;
writeln!(f, "version: {:?}", self.version)?;
writeln!(f, "mode: {:?}", self.mode)?;
writeln!(f, "stratum: {}", self.stratum)?;
writeln!(f, "poll: {}", self.poll)?;
writeln!(f, "precision: {}", self.precision)?;
writeln!(f, "root_delay: {}", self.root_delay.to_time_delta())?;
writeln!(
f,
"root_dispersion: {}",
self.root_dispersion.to_time_delta()
)?;
writeln!(f, "reference_id: {:?}", self.reference_id)?;
writeln!(
f,
"reference_timestamp: {}",
self.reference_timestamp.to_utc_datetime()
)?;
writeln!(
f,
"origin_timestamp: {}",
self.origin_timestamp.to_utc_datetime()
)?;
writeln!(
f,
"receive_timestamp: {}",
self.receive_timestamp.to_utc_datetime()
)?;
writeln!(
f,
"transmit_timestamp: {}",
self.transmit_timestamp.to_utc_datetime()
)?;
Ok(())
}
}
impl Packet {
pub const MIN_SIZE: usize = 48;
pub const fn new_request(transmit_timestamp: u64) -> Self {
let transmit_timestamp = Timestamp::new(transmit_timestamp);
Self {
leap_indicator: LeapIndicator::NoWarning,
version: Version::V4,
mode: Mode::Client,
stratum: 0,
poll: 0,
precision: 0,
root_delay: Short::new(0),
root_dispersion: Short::new(0),
reference_id: [0u8; 4],
reference_timestamp: Timestamp::new(0),
origin_timestamp: Timestamp::new(0),
receive_timestamp: Timestamp::new(0),
transmit_timestamp,
extensions: Vec::new(),
}
}
pub fn emit_bytes(&self, mut buffer: &mut [u8]) {
use bytes::BufMut;
let total_size = self.total_size();
assert!(
buffer.len() >= total_size,
"Buffer too small: {} < {}",
buffer.len(),
total_size
);
let header: u8 =
self.mode.to_bits() | self.version.get() << 3 | self.leap_indicator.to_bits() << 6;
buffer.put_u8(header);
buffer.put_u8(self.stratum);
buffer.put_u8(self.poll);
buffer.put_i8(self.precision);
buffer.put_u32_ne(self.root_delay.into_network_endian());
buffer.put_u32_ne(self.root_dispersion.into_network_endian());
buffer.put(self.reference_id.as_slice());
buffer.put_u64_ne(self.reference_timestamp.into_network_endian());
buffer.put_u64_ne(self.origin_timestamp.into_network_endian());
buffer.put_u64_ne(self.receive_timestamp.into_network_endian());
buffer.put_u64_ne(self.transmit_timestamp.into_network_endian());
for extension in &self.extensions {
extension.emit_bytes(buffer);
}
}
pub fn total_size(&self) -> usize {
Self::MIN_SIZE
+ self
.extensions
.iter()
.map(|ext| ext.length() as usize)
.sum::<usize>()
}
pub fn parse_from_bytes(input: &[u8]) -> nom::IResult<&[u8], Packet> {
let (input, (leap_indicator, version, mode)) =
nom::bits::bits((LeapIndicator::parse, Version::parse, Mode::parse)).parse(input)?;
let (input, (stratum, poll, precision)) = (be_u8(), be_u8(), be_i8()).parse(input)?;
let (input, (root_delay, root_dispersion)) = (Short::parse, Short::parse).parse(input)?;
let (input, reference_id) = nom::bytes::take(4usize).parse(input)?;
let reference_id: [u8; 4] = reference_id.try_into().unwrap();
let (input, (reference_timestamp, origin_timestamp)) =
(Timestamp::parse, Timestamp::parse).parse(input)?;
let (input, (receive_timestamp, transmit_timestamp)) =
(Timestamp::parse, Timestamp::parse).parse(input)?;
let rv = Self {
leap_indicator,
version,
mode,
stratum,
poll,
precision,
root_delay,
root_dispersion,
reference_id,
reference_timestamp,
origin_timestamp,
receive_timestamp,
transmit_timestamp,
extensions: Vec::new(),
};
Ok((input, rv))
}
}
impl TryFrom<Packet> for NtpData {
type Error = TryFromPacketError;
fn try_from(value: Packet) -> Result<Self, Self::Error> {
match value {
Packet {
stratum: 0,
reference_id,
..
} => Err(Self::Error::KissoDeath(reference_id)),
Packet {
stratum: 16,
leap_indicator: LeapIndicator::Unknown,
..
} => Err(Self::Error::Unsynchronized),
Packet {
transmit_timestamp,
receive_timestamp,
..
} if receive_timestamp > transmit_timestamp => Err(Self::Error::BadTimestamps),
Packet {
root_delay,
root_dispersion,
receive_timestamp,
transmit_timestamp,
stratum,
..
} => {
let root_delay = ClockBoundDuration::from(root_delay);
let root_dispersion = ClockBoundDuration::from(root_dispersion);
let server_recv_time = ClockBoundInstant::from(receive_timestamp);
let server_send_time = ClockBoundInstant::from(transmit_timestamp);
let stratum = Stratum::try_from(stratum)?;
Ok(NtpData {
server_recv_time,
server_send_time,
root_delay,
root_dispersion,
stratum,
})
}
}
}
}
#[derive(Debug, Error, PartialEq)]
pub enum TryFromPacketError {
#[error("Could not parse stratum value.")]
ParsingStratum(#[from] TryFromU8Error),
#[error("Unspecified or invalid packet.")]
KissoDeath([u8; 4]),
#[error("The packet's timestamp indicates that it was received after it was transmitted.")]
BadTimestamps,
#[error("Server indicates that it is unsynchronized.")]
Unsynchronized,
}
#[cfg(test)]
mod test {
use chrono::{DateTime, TimeDelta};
use hex_literal::hex;
use rstest::rstest;
use crate::daemon::io::ntp::packet::extension::Fec2V1Value;
use super::*;
const NTP_PACKET: &[u8] = &hex!(
"
240103ee0000000200000001a9fea97aeb4529fe
000046f3eb4529feaffa7cbceb4529feb079bcc4
eb4529feb07b2c5b
"
);
#[test]
fn parse() {
let (leftover, packet) = Packet::parse_from_bytes(NTP_PACKET).unwrap();
assert!(leftover.is_empty());
assert_eq!(packet.leap_indicator, LeapIndicator::NoWarning);
assert_eq!(packet.version.get(), 4);
assert_eq!(packet.mode, Mode::Server);
assert_eq!(packet.stratum, 1);
assert_eq!(packet.poll, 3);
assert_eq!(packet.precision, -18); packet.root_delay.assert_within_error_bound(
TimeDelta::from_std(std::time::Duration::from_secs_f64(0.000031)).unwrap(),
);
packet.root_dispersion.assert_within_error_bound(
TimeDelta::from_std(std::time::Duration::from_secs_f64(0.000015)).unwrap(),
);
assert_eq!(packet.reference_id, [169, 254, 169, 122]);
packet
.reference_timestamp
.assert_within_error_bound(DateTime::from_timestamp(1738189694, 000004228).unwrap());
packet
.origin_timestamp
.assert_within_error_bound(DateTime::from_timestamp(1738189694, 687415882).unwrap());
packet
.receive_timestamp
.assert_within_error_bound(DateTime::from_timestamp(1738189694, 689357564).unwrap());
packet
.transmit_timestamp
.assert_within_error_bound(DateTime::from_timestamp(1738189694, 689379474).unwrap());
}
#[rstest]
#[case::no_extensions(Packet::builder().build(), 48)]
#[case::with_fec2v1(
Packet::builder()
.extensions(vec![ExtensionField::Fec2V1(Fec2V1Value {
major_version: 2,
minor_version: 100,
startup_id: 0xdeadbeef,
})])
.build(),
64
)]
fn packet_total_size(#[case] packet: Packet, #[case] expected_size: usize) {
assert_eq!(packet.total_size(), expected_size);
}
#[rstest]
#[case::no_extensions({
let (_, packet) = Packet::parse_from_bytes(NTP_PACKET).unwrap();
(packet, NTP_PACKET.to_vec())
})]
#[case::with_extension({
let (_, mut packet) = Packet::parse_from_bytes(NTP_PACKET).unwrap();
packet.extensions.push(ExtensionField::Fec2V1(Fec2V1Value {
major_version: 2,
minor_version: 100,
startup_id: 0xdeadbeef,
}));
let mut expected = NTP_PACKET.to_vec();
expected.extend_from_slice(&[0xFE, 0xC2, 0x00, 0x10]); // Extension header
expected.extend_from_slice(&[1, 2, 100, 0]); // FEC2V1 data
expected.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF]);
(packet, expected)
})]
fn emit(#[case] (packet, expected): (Packet, Vec<u8>)) {
let mut output = vec![0u8; packet.total_size()];
packet.emit_bytes(&mut output);
assert_eq!(output, expected);
}
#[test]
fn display_does_not_panic() {
let packet = Packet::new_request(0xd00f_d00f);
let _ = packet.to_string();
}
#[test]
fn conversion_packet_to_ntp_data() {
let (_, packet) = Packet::parse_from_bytes(NTP_PACKET).unwrap();
let ntp_data = NtpData::try_from(packet).unwrap();
assert!((ntp_data.root_delay.as_seconds_f64() - 0.000031).abs() < 1.0 / 65536.0);
assert!((ntp_data.root_dispersion.as_seconds_f64() - 0.000015).abs() < 1.0 / 65536.0);
assert_eq!(
ntp_data.server_recv_time,
ClockBoundInstant::from_time(1738189694, 689357564)
);
assert_eq!(
ntp_data.server_send_time,
ClockBoundInstant::from_time(1738189694, 689379474)
);
assert_eq!(ntp_data.stratum, Stratum::ONE);
}
#[test]
fn packet_builder() {
let default_packet = Packet::builder().build();
assert_eq!(default_packet.leap_indicator, LeapIndicator::NoWarning);
assert_eq!(default_packet.version, Version::V4);
assert_eq!(default_packet.mode, Mode::Client);
assert_eq!(default_packet.stratum, 0);
assert_eq!(default_packet.reference_id, [0u8; 4]);
assert!(default_packet.extensions.is_empty());
let custom_packet = Packet::builder()
.mode(Mode::Server)
.stratum(1)
.reference_id([0xa9, 0xfe, 0xa9, 0x7a])
.extensions(vec![ExtensionField::Fec2V1(Fec2V1Value {
major_version: 1,
minor_version: 0,
startup_id: 0xdeadbeef,
})])
.build();
assert_eq!(custom_packet.mode, Mode::Server);
assert_eq!(custom_packet.stratum, 1);
assert_eq!(custom_packet.reference_id, [0xa9, 0xfe, 0xa9, 0x7a]);
assert_eq!(custom_packet.extensions.len(), 1);
}
#[rstest]
#[case::kiss_o_death(
Packet::builder().build(),
Err(TryFromPacketError::KissoDeath([0, 0, 0,0]))
)]
#[case::healthy(
Packet::builder()
.stratum(16)
.leap_indicator(LeapIndicator::Unknown)
.build(),
Err(TryFromPacketError::Unsynchronized)
)]
#[case::healthy(
Packet::builder()
.stratum(1)
.receive_timestamp(Timestamp::new(10))
.transmit_timestamp(Timestamp::new(9))
.build(),
Err(TryFromPacketError::BadTimestamps)
)]
fn packet_is_unuseable(
#[case] packet: Packet,
#[case] ntp_data: Result<NtpData, TryFromPacketError>,
) {
assert_eq!(packet.try_into(), ntp_data);
}
}