use rtime_core::clock::LeapIndicator;
use rtime_core::timestamp::{NtpDuration, NtpTimestamp};
use crate::kiss_code::KissCode;
use crate::packet::{NtpMode, NtpPacket};
#[derive(Debug, Clone)]
pub struct NtpResult {
pub offset: NtpDuration,
pub delay: NtpDuration,
pub stratum: u8,
pub leap_indicator: LeapIndicator,
pub root_delay: NtpDuration,
pub root_dispersion: NtpDuration,
pub reference_id: u32,
pub precision: i8,
}
#[derive(Debug, thiserror::Error)]
pub enum ClientError {
#[error("response is not server mode")]
NotServerResponse,
#[error("origin timestamp mismatch (possible spoofing)")]
OriginMismatch,
#[error("server sent zero transmit timestamp")]
ZeroTransmit,
#[error("Kiss-o'-Death received: {0:?}")]
KissOfDeath(KissCode),
}
pub fn build_request(cookie: NtpTimestamp) -> NtpPacket {
NtpPacket::new_client_request(cookie)
}
pub fn process_response(
response: &NtpPacket,
t1: NtpTimestamp,
t4: NtpTimestamp,
origin_cookie: NtpTimestamp,
) -> Result<NtpResult, ClientError> {
if response.mode != NtpMode::Server {
return Err(ClientError::NotServerResponse);
}
if response.origin_ts != origin_cookie {
return Err(ClientError::OriginMismatch);
}
if response.transmit_ts == NtpTimestamp::ZERO {
return Err(ClientError::ZeroTransmit);
}
if response.stratum == 0 {
let kod = KissCode::from_reference_id(response.reference_id);
return Err(ClientError::KissOfDeath(kod));
}
let t2 = response.receive_ts;
let t3 = response.transmit_ts;
let a = NtpDuration::between(t1, t2);
let b = NtpDuration::between(t4, t3);
let offset = (a + b) / 2;
let round_trip = NtpDuration::between(t1, t4);
let server_processing = NtpDuration::between(t2, t3);
let delay = round_trip - server_processing;
Ok(NtpResult {
offset,
delay,
stratum: response.stratum,
leap_indicator: response.leap_indicator,
root_delay: NtpDuration::from_ntp_short(response.root_delay),
root_dispersion: NtpDuration::from_ntp_short(response.root_dispersion),
reference_id: response.reference_id,
precision: response.precision,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_request_basic() {
let cookie = NtpTimestamp::new(1000, 0);
let pkt = build_request(cookie);
assert_eq!(pkt.mode, NtpMode::Client);
assert_eq!(pkt.transmit_ts, cookie);
}
#[test]
fn process_response_computes_offset() {
let t1 = NtpTimestamp::new(1000, 0); let t2 = NtpTimestamp::new(1000, 429496730); let t3 = NtpTimestamp::new(1000, 429496730); let t4 = NtpTimestamp::new(1000, 0);
let cookie = t1;
let response = NtpPacket {
leap_indicator: LeapIndicator::NoWarning,
version: 4,
mode: NtpMode::Server,
stratum: 2,
poll: 6,
precision: -20,
root_delay: 0,
root_dispersion: 0,
reference_id: u32::from_be_bytes(*b"GPS\0"),
reference_ts: NtpTimestamp::new(999, 0),
origin_ts: cookie,
receive_ts: t2,
transmit_ts: t3,
};
let result = process_response(&response, t1, t4, cookie).unwrap();
let offset_ms = result.offset.to_millis_f64();
assert!(
offset_ms.abs() > 50.0 && offset_ms.abs() < 200.0,
"offset: {}ms",
offset_ms
);
}
#[test]
fn process_response_rejects_wrong_origin() {
let cookie = NtpTimestamp::new(1000, 0);
let response = NtpPacket {
leap_indicator: LeapIndicator::NoWarning,
version: 4,
mode: NtpMode::Server,
stratum: 2,
poll: 6,
precision: -20,
root_delay: 0,
root_dispersion: 0,
reference_id: 0,
reference_ts: NtpTimestamp::ZERO,
origin_ts: NtpTimestamp::new(9999, 0), receive_ts: NtpTimestamp::new(1000, 0),
transmit_ts: NtpTimestamp::new(1000, 0),
};
let result = process_response(
&response,
NtpTimestamp::new(1000, 0),
NtpTimestamp::new(1000, 0),
cookie,
);
assert!(matches!(result, Err(ClientError::OriginMismatch)));
}
#[test]
fn process_response_rejects_kod() {
let cookie = NtpTimestamp::new(1000, 0);
let response = NtpPacket {
leap_indicator: LeapIndicator::AlarmUnsynchronized,
version: 4,
mode: NtpMode::Server,
stratum: 0, poll: 6,
precision: -20,
root_delay: 0,
root_dispersion: 0,
reference_id: u32::from_be_bytes(*b"RATE"),
reference_ts: NtpTimestamp::ZERO,
origin_ts: cookie,
receive_ts: NtpTimestamp::ZERO,
transmit_ts: NtpTimestamp::new(1000, 0),
};
let result = process_response(
&response,
NtpTimestamp::new(1000, 0),
NtpTimestamp::new(1000, 0),
cookie,
);
assert!(matches!(
result,
Err(ClientError::KissOfDeath(KissCode::Rate))
));
}
}