use std::net::Ipv4Addr;
pub const REF_TIMESTAMP_EPOCH: u32 = 2_208_988_800;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct NtpMessage {
pub leap: NtpLeapIndicator,
pub version: u8,
pub mode: NtpMode,
pub stratum: u8,
pub poll: i8,
pub precision: i8,
pub root_delay_raw: u32,
pub root_dispersion_raw: u32,
pub ref_id: [u8; 4],
pub ref_timestamp: NtpTimestamp,
pub origin_timestamp: NtpTimestamp,
pub recv_timestamp: NtpTimestamp,
pub transmit_timestamp: NtpTimestamp,
}
impl NtpMessage {
#[inline]
pub fn is_amplification_risk(&self) -> bool {
matches!(self.mode, NtpMode::Control | NtpMode::Private)
}
#[inline]
pub fn ref_id_as_ipv4(&self) -> Option<Ipv4Addr> {
if self.stratum >= 2 {
Some(Ipv4Addr::new(
self.ref_id[0],
self.ref_id[1],
self.ref_id[2],
self.ref_id[3],
))
} else {
None
}
}
pub fn ref_id_as_str(&self) -> Option<&str> {
if self.stratum > 1 {
return None;
}
let end = self
.ref_id
.iter()
.position(|&b| b == 0)
.unwrap_or(self.ref_id.len());
std::str::from_utf8(&self.ref_id[..end]).ok()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(tag = "kind", content = "value", rename_all = "snake_case")
)]
#[non_exhaustive]
pub enum NtpLeapIndicator {
NoWarning,
AddSecond,
DeleteSecond,
Unsynchronised,
}
impl NtpLeapIndicator {
pub fn from_raw(b: u8) -> Self {
match b & 0x03 {
0 => NtpLeapIndicator::NoWarning,
1 => NtpLeapIndicator::AddSecond,
2 => NtpLeapIndicator::DeleteSecond,
_ => NtpLeapIndicator::Unsynchronised,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
feature = "serde",
serde(tag = "kind", content = "value", rename_all = "snake_case")
)]
#[non_exhaustive]
pub enum NtpMode {
Reserved,
SymmetricActive,
SymmetricPassive,
Client,
Server,
Broadcast,
Control,
Private,
}
impl NtpMode {
pub fn from_raw(b: u8) -> Self {
match b & 0x07 {
0 => NtpMode::Reserved,
1 => NtpMode::SymmetricActive,
2 => NtpMode::SymmetricPassive,
3 => NtpMode::Client,
4 => NtpMode::Server,
5 => NtpMode::Broadcast,
6 => NtpMode::Control,
7 => NtpMode::Private,
_ => unreachable!("u8 & 0x07 fits in 0..8"),
}
}
pub fn as_u8(self) -> u8 {
match self {
NtpMode::Reserved => 0,
NtpMode::SymmetricActive => 1,
NtpMode::SymmetricPassive => 2,
NtpMode::Client => 3,
NtpMode::Server => 4,
NtpMode::Broadcast => 5,
NtpMode::Control => 6,
NtpMode::Private => 7,
}
}
pub fn as_str(self) -> &'static str {
match self {
NtpMode::Reserved => "reserved",
NtpMode::SymmetricActive => "symmetric_active",
NtpMode::SymmetricPassive => "symmetric_passive",
NtpMode::Client => "client",
NtpMode::Server => "server",
NtpMode::Broadcast => "broadcast",
NtpMode::Control => "control",
NtpMode::Private => "private",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NtpTimestamp {
pub seconds: u32,
pub fraction: u32,
}
impl NtpTimestamp {
#[inline]
pub fn is_zero(self) -> bool {
self.seconds == 0 && self.fraction == 0
}
pub fn to_unix_f64(self) -> Option<f64> {
if self.is_zero() {
return None;
}
let secs = self.seconds.checked_sub(REF_TIMESTAMP_EPOCH)?;
let frac = (self.fraction as f64) / (u32::MAX as f64 + 1.0);
Some(secs as f64 + frac)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn leap_indicator_decoding() {
assert_eq!(NtpLeapIndicator::from_raw(0), NtpLeapIndicator::NoWarning);
assert_eq!(NtpLeapIndicator::from_raw(1), NtpLeapIndicator::AddSecond);
assert_eq!(
NtpLeapIndicator::from_raw(2),
NtpLeapIndicator::DeleteSecond
);
assert_eq!(
NtpLeapIndicator::from_raw(3),
NtpLeapIndicator::Unsynchronised
);
assert_eq!(
NtpLeapIndicator::from_raw(0xfc | 1),
NtpLeapIndicator::AddSecond
);
}
#[test]
fn mode_round_trip_u8() {
for n in 0..=7u8 {
let mode = NtpMode::from_raw(n);
assert_eq!(mode.as_u8(), n);
}
}
#[test]
fn mode_slugs_locked() {
assert_eq!(NtpMode::Client.as_str(), "client");
assert_eq!(NtpMode::Server.as_str(), "server");
assert_eq!(NtpMode::Control.as_str(), "control");
assert_eq!(NtpMode::Private.as_str(), "private");
}
#[test]
fn amplification_predicate_fires_on_modes_6_and_7() {
let mut m = NtpMessage {
leap: NtpLeapIndicator::NoWarning,
version: 4,
mode: NtpMode::Client,
stratum: 0,
poll: 0,
precision: 0,
root_delay_raw: 0,
root_dispersion_raw: 0,
ref_id: [0; 4],
ref_timestamp: NtpTimestamp::default(),
origin_timestamp: NtpTimestamp::default(),
recv_timestamp: NtpTimestamp::default(),
transmit_timestamp: NtpTimestamp::default(),
};
assert!(!m.is_amplification_risk());
m.mode = NtpMode::Control;
assert!(m.is_amplification_risk());
m.mode = NtpMode::Private;
assert!(m.is_amplification_risk());
}
#[test]
fn ref_id_decodes_as_ipv4_when_stratum_high() {
let m = NtpMessage {
stratum: 3,
ref_id: [10, 0, 0, 1],
..nz()
};
assert_eq!(m.ref_id_as_ipv4(), Some(Ipv4Addr::new(10, 0, 0, 1)));
assert_eq!(m.ref_id_as_str(), None);
}
#[test]
fn ref_id_decodes_as_string_at_stratum_one() {
let m = NtpMessage {
stratum: 1,
ref_id: *b"GPS\0",
..nz()
};
assert_eq!(m.ref_id_as_str(), Some("GPS"));
assert_eq!(m.ref_id_as_ipv4(), None);
}
#[test]
fn ntp_timestamp_unix_epoch_offset() {
let ts = NtpTimestamp {
seconds: REF_TIMESTAMP_EPOCH,
fraction: 0,
};
assert_eq!(ts.to_unix_f64(), Some(0.0));
let ts = NtpTimestamp {
seconds: REF_TIMESTAMP_EPOCH + 3600,
fraction: 0,
};
assert_eq!(ts.to_unix_f64(), Some(3600.0));
}
#[test]
fn ntp_timestamp_zero_returns_none() {
let ts = NtpTimestamp::default();
assert!(ts.is_zero());
assert_eq!(ts.to_unix_f64(), None);
}
fn nz() -> NtpMessage {
NtpMessage {
leap: NtpLeapIndicator::NoWarning,
version: 4,
mode: NtpMode::Client,
stratum: 0,
poll: 0,
precision: 0,
root_delay_raw: 0,
root_dispersion_raw: 0,
ref_id: [0; 4],
ref_timestamp: NtpTimestamp::default(),
origin_timestamp: NtpTimestamp::default(),
recv_timestamp: NtpTimestamp::default(),
transmit_timestamp: NtpTimestamp::default(),
}
}
}