use rtime_core::clock::LeapIndicator;
use rtime_core::timestamp::NtpTimestamp;
use crate::packet::{NTP_VERSION, NtpMode, NtpPacket};
#[derive(Debug, Clone)]
pub struct ServerState {
pub stratum: u8,
pub reference_id: u32,
pub reference_ts: NtpTimestamp,
pub root_delay: u32,
pub root_dispersion: u32,
pub leap_indicator: LeapIndicator,
pub precision: i8,
}
impl Default for ServerState {
fn default() -> Self {
Self {
stratum: 16, reference_id: 0,
reference_ts: NtpTimestamp::ZERO,
root_delay: 0,
root_dispersion: 0,
leap_indicator: LeapIndicator::AlarmUnsynchronized,
precision: -20,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ServerError {
#[error("request mode is not Client (mode={0:?})")]
NotClientRequest(NtpMode),
#[error("unsupported NTP version: {0}")]
UnsupportedVersion(u8),
#[error("transmit timestamp is zero")]
ZeroTransmit,
}
pub fn validate_request(packet: &NtpPacket) -> Result<(), ServerError> {
if packet.mode != NtpMode::Client {
return Err(ServerError::NotClientRequest(packet.mode));
}
if !(3..=4).contains(&packet.version) {
return Err(ServerError::UnsupportedVersion(packet.version));
}
if packet.transmit_ts == NtpTimestamp::ZERO {
return Err(ServerError::ZeroTransmit);
}
Ok(())
}
pub fn build_response(
request: &NtpPacket,
receive_ts: NtpTimestamp,
transmit_ts: NtpTimestamp,
server_state: &ServerState,
) -> NtpPacket {
NtpPacket {
leap_indicator: server_state.leap_indicator,
version: NTP_VERSION,
mode: NtpMode::Server,
stratum: server_state.stratum,
poll: request.poll,
precision: server_state.precision,
root_delay: server_state.root_delay,
root_dispersion: server_state.root_dispersion,
reference_id: server_state.reference_id,
reference_ts: server_state.reference_ts,
origin_ts: request.transmit_ts,
receive_ts,
transmit_ts,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client;
fn test_server_state() -> ServerState {
ServerState {
stratum: 1,
reference_id: u32::from_be_bytes(*b"GPS\0"),
reference_ts: NtpTimestamp::new(3_900_000_000, 0),
root_delay: 0,
root_dispersion: 0x0001_0000, leap_indicator: LeapIndicator::NoWarning,
precision: -20,
}
}
#[test]
fn roundtrip_client_server() {
let state = test_server_state();
let t1 = NtpTimestamp::new(3_900_000_100, 0);
let cookie = t1;
let request = client::build_request(cookie);
validate_request(&request).expect("valid client request");
let t2 = NtpTimestamp::new(3_900_000_100, 100_000);
let t3 = NtpTimestamp::new(3_900_000_100, 110_000);
let response = build_response(&request, t2, t3, &state);
assert_eq!(response.mode, NtpMode::Server);
assert_eq!(response.version, NTP_VERSION);
assert_eq!(response.stratum, state.stratum);
assert_eq!(response.origin_ts, cookie);
assert_eq!(response.receive_ts, t2);
assert_eq!(response.transmit_ts, t3);
assert_eq!(response.reference_id, state.reference_id);
assert_eq!(response.leap_indicator, LeapIndicator::NoWarning);
let wire = response.serialize();
let parsed = NtpPacket::parse(&wire).expect("parse response");
assert_eq!(parsed, response);
let t4 = NtpTimestamp::new(3_900_000_100, 210_000);
let result = client::process_response(&parsed, t1, t4, cookie).expect("process response");
assert_eq!(result.stratum, 1);
assert_eq!(result.leap_indicator, LeapIndicator::NoWarning);
assert!(result.delay.to_nanos() >= 0, "delay should be non-negative");
}
#[test]
fn validate_rejects_wrong_mode() {
let mut pkt = NtpPacket::new_client_request(NtpTimestamp::new(1000, 1));
pkt.mode = NtpMode::Server;
let err = validate_request(&pkt).unwrap_err();
assert!(
matches!(err, ServerError::NotClientRequest(NtpMode::Server)),
"expected NotClientRequest, got: {err:?}"
);
}
#[test]
fn validate_rejects_zero_transmit() {
let mut pkt = NtpPacket::new_client_request(NtpTimestamp::ZERO);
pkt.transmit_ts = NtpTimestamp::ZERO;
let err = validate_request(&pkt).unwrap_err();
assert!(
matches!(err, ServerError::ZeroTransmit),
"expected ZeroTransmit, got: {err:?}"
);
}
#[test]
fn validate_rejects_unsupported_version() {
let mut pkt = NtpPacket::new_client_request(NtpTimestamp::new(1000, 1));
pkt.version = 2;
let err = validate_request(&pkt).unwrap_err();
assert!(
matches!(err, ServerError::UnsupportedVersion(2)),
"expected UnsupportedVersion(2), got: {err:?}"
);
}
#[test]
fn validate_accepts_version_3() {
let mut pkt = NtpPacket::new_client_request(NtpTimestamp::new(1000, 1));
pkt.version = 3;
validate_request(&pkt).expect("version 3 should be accepted");
}
#[test]
fn validate_accepts_version_4() {
let pkt = NtpPacket::new_client_request(NtpTimestamp::new(1000, 1));
assert_eq!(pkt.version, 4);
validate_request(&pkt).expect("version 4 should be accepted");
}
#[test]
fn response_copies_client_poll() {
let state = test_server_state();
let mut request = NtpPacket::new_client_request(NtpTimestamp::new(1000, 1));
request.poll = 10;
let response = build_response(
&request,
NtpTimestamp::new(1000, 100),
NtpTimestamp::new(1000, 200),
&state,
);
assert_eq!(response.poll, 10);
}
}