Skip to main content

ntp/
lib.rs

1/*!
2# Example
3Shows how to use the ntp library to fetch the current time according
4to the requested ntp server.
5
6```rust,no_run
7extern crate chrono;
8extern crate ntp;
9
10use chrono::TimeZone;
11
12fn main() {
13    let address = "time.nist.gov:123";
14    let result = ntp::request(address).unwrap();
15    let unix_time = ntp::unix_time::Instant::from(result.transmit_timestamp);
16    let local_time = chrono::Local.timestamp_opt(unix_time.secs(), unix_time.subsec_nanos() as _).unwrap();
17    println!("{}", local_time);
18    println!("Offset: {:.6} seconds", result.offset_seconds);
19}
20```
21*/
22
23#![cfg_attr(not(feature = "std"), no_std)]
24#![deny(unsafe_code)]
25#![warn(missing_docs)]
26#![warn(unreachable_pub)]
27
28#[cfg(feature = "alloc")]
29extern crate alloc;
30
31/// Custom error types for buffer-based NTP packet parsing and serialization.
32pub mod error;
33/// NTP extension field parsing and NTS extension types.
34///
35/// Provides types for parsing and serializing NTP extension fields (RFC 7822)
36/// and NTS-specific extension field types (RFC 8915).
37pub mod extension;
38pub mod protocol;
39/// Unix time conversion utilities for NTP timestamps.
40///
41/// Provides the `Instant` type for converting between NTP timestamps
42/// (seconds since 1900-01-01) and Unix timestamps (seconds since 1970-01-01).
43pub mod unix_time;
44
45/// Clock sample filtering for the continuous NTP client.
46///
47/// Implements a simplified version of the RFC 5905 Section 10 clock filter
48/// algorithm.
49#[cfg(any(feature = "tokio", feature = "smol-runtime"))]
50pub mod filter;
51
52/// Shared types and logic for the continuous NTP client.
53#[cfg(any(feature = "tokio", feature = "smol-runtime"))]
54pub mod client_common;
55
56/// Shared NTS logic used by both tokio and smol NTS modules.
57#[cfg(any(feature = "nts", feature = "nts-smol"))]
58pub(crate) mod nts_common;
59
60/// Continuous NTP client with adaptive poll interval management and interleaved mode.
61///
62/// Enable with the `tokio` feature flag:
63///
64/// ```toml
65/// [dependencies]
66/// ntp_usg = { version = "2.0", features = ["tokio"] }
67/// ```
68#[cfg(feature = "tokio")]
69pub mod client;
70
71/// Network Time Security (NTS) client (RFC 8915).
72///
73/// Provides authenticated NTP using TLS 1.3 key establishment and AEAD
74/// per-packet authentication. Enable with the `nts` feature flag:
75///
76/// ```toml
77/// [dependencies]
78/// ntp_usg = { version = "2.0", features = ["nts"] }
79/// ```
80#[cfg(feature = "nts")]
81pub mod nts;
82
83/// System clock adjustment utilities for applying NTP corrections.
84///
85/// Provides platform-specific functions for slewing (gradual) and stepping
86/// (immediate) the system clock. Requires elevated privileges (root/admin).
87///
88/// Enable with the `clock` feature flag:
89///
90/// ```toml
91/// [dependencies]
92/// ntp_usg = { version = "2.0", features = ["clock"] }
93/// ```
94#[cfg(feature = "clock")]
95pub mod clock;
96
97/// Async NTP client functions using the Tokio runtime.
98///
99/// Enable with the `tokio` feature flag:
100///
101/// ```toml
102/// [dependencies]
103/// ntp_usg = { version = "2.0", features = ["tokio"] }
104/// ```
105///
106/// See [`async_ntp::request`] and [`async_ntp::request_with_timeout`] for details.
107#[cfg(feature = "tokio")]
108pub mod async_ntp;
109
110/// Async NTP client functions using the smol runtime.
111///
112/// Enable with the `smol-runtime` feature flag:
113///
114/// ```toml
115/// [dependencies]
116/// ntp_usg = { version = "2.0", features = ["smol-runtime"] }
117/// ```
118///
119/// See [`smol_ntp::request`] and [`smol_ntp::request_with_timeout`] for details.
120#[cfg(feature = "smol-runtime")]
121pub mod smol_ntp;
122
123/// Continuous NTP client using the smol runtime.
124///
125/// Enable with the `smol-runtime` feature flag:
126///
127/// ```toml
128/// [dependencies]
129/// ntp_usg = { version = "2.0", features = ["smol-runtime"] }
130/// ```
131#[cfg(feature = "smol-runtime")]
132pub mod smol_client;
133
134/// Network Time Security (NTS) client using the smol runtime (RFC 8915).
135///
136/// Provides the same NTS functionality as [`nts`] but using smol
137/// and futures-rustls instead of tokio and tokio-rustls.
138///
139/// Enable with the `nts-smol` feature flag:
140///
141/// ```toml
142/// [dependencies]
143/// ntp_usg = { version = "2.0", features = ["nts-smol"] }
144/// ```
145#[cfg(feature = "nts-smol")]
146pub mod smol_nts;
147
148// ============================================================================
149// Everything below requires std (networking, blocking I/O, etc.)
150// ============================================================================
151
152#[cfg(feature = "std")]
153use log::debug;
154#[cfg(feature = "std")]
155use protocol::{ConstPackedSizeBytes, ReadBytes, WriteBytes};
156#[cfg(feature = "std")]
157use std::io;
158#[cfg(feature = "std")]
159use std::net::{SocketAddr, ToSocketAddrs, UdpSocket};
160#[cfg(feature = "std")]
161use std::ops::Deref;
162#[cfg(feature = "std")]
163use std::time::Duration;
164
165/// Select the appropriate bind address based on the target address family.
166///
167/// Returns `"0.0.0.0:0"` for IPv4 targets and `"[::]:0"` for IPv6 targets.
168#[cfg(feature = "std")]
169pub(crate) fn bind_addr_for(target: &SocketAddr) -> &'static str {
170    match target {
171        SocketAddr::V4(_) => "0.0.0.0:0",
172        SocketAddr::V6(_) => "[::]:0",
173    }
174}
175
176/// Error returned when the server responds with a Kiss-o'-Death (KoD) packet.
177///
178/// Per RFC 5905 Section 7.4, recipients of kiss codes MUST inspect them and take
179/// the described actions. This error is returned as the inner error of an
180/// [`io::Error`] with kind [`io::ErrorKind::ConnectionRefused`], and can be
181/// extracted via [`io::Error::get_ref`] and `downcast_ref`.
182///
183/// # Caller Responsibilities
184///
185/// - **DENY / RSTR**: The caller MUST stop sending packets to this server.
186/// - **RATE**: The caller MUST reduce its polling interval before retrying.
187///
188/// # Examples
189///
190/// ```no_run
191/// # use std::error::Error;
192/// # fn main() -> Result<(), Box<dyn Error>> {
193/// match ntp::request("time.nist.gov:123") {
194///     Ok(result) => println!("Offset: {:.6}s", result.offset_seconds),
195///     Err(e) => {
196///         if let Some(kod) = e.get_ref().and_then(|inner| inner.downcast_ref::<ntp::KissOfDeathError>()) {
197///             eprintln!("Kiss-o'-Death: {:?}", kod.code);
198///         }
199///     }
200/// }
201/// # Ok(())
202/// # }
203/// ```
204#[cfg(feature = "std")]
205#[derive(Clone, Copy, Debug)]
206pub struct KissOfDeathError {
207    /// The specific kiss code received from the server.
208    pub code: protocol::KissOfDeath,
209}
210
211#[cfg(feature = "std")]
212impl std::fmt::Display for KissOfDeathError {
213    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214        match self.code {
215            protocol::KissOfDeath::Deny => {
216                write!(
217                    f,
218                    "server sent Kiss-o'-Death DENY: access denied, stop querying this server"
219                )
220            }
221            protocol::KissOfDeath::Rstr => {
222                write!(
223                    f,
224                    "server sent Kiss-o'-Death RSTR: access restricted, stop querying this server"
225                )
226            }
227            protocol::KissOfDeath::Rate => {
228                write!(f, "server sent Kiss-o'-Death RATE: reduce polling interval")
229            }
230        }
231    }
232}
233
234#[cfg(feature = "std")]
235impl std::error::Error for KissOfDeathError {}
236
237/// The result of an NTP request, containing the server's response packet
238/// along with computed timing information.
239///
240/// This struct implements `Deref<Target = protocol::Packet>`, so all packet
241/// fields can be accessed directly (e.g., `result.transmit_timestamp`).
242#[cfg(feature = "std")]
243#[derive(Clone, Copy, Debug)]
244pub struct NtpResult {
245    /// The parsed NTP response packet from the server.
246    pub packet: protocol::Packet,
247    /// The destination timestamp (T4): local time when the response was received.
248    ///
249    /// Expressed as an NTP `TimestampFormat` for consistency with the packet timestamps.
250    pub destination_timestamp: protocol::TimestampFormat,
251    /// Clock offset: the estimated difference between the local clock and the server clock.
252    ///
253    /// Computed as `((T2 - T1) + (T3 - T4)) / 2` per RFC 5905 Section 8, where:
254    /// - T1 = origin timestamp (client transmit time)
255    /// - T2 = receive timestamp (server receive time)
256    /// - T3 = transmit timestamp (server transmit time)
257    /// - T4 = destination timestamp (client receive time)
258    ///
259    /// A positive value means the local clock is behind the server.
260    /// A negative value means the local clock is ahead of the server.
261    pub offset_seconds: f64,
262    /// Round-trip delay between the client and server.
263    ///
264    /// Computed as `(T4 - T1) - (T3 - T2)` per RFC 5905 Section 8.
265    pub delay_seconds: f64,
266}
267
268#[cfg(feature = "std")]
269impl Deref for NtpResult {
270    type Target = protocol::Packet;
271    fn deref(&self) -> &Self::Target {
272        &self.packet
273    }
274}
275
276/// Convert a Unix `Instant` to seconds as f64 (relative to Unix epoch).
277#[cfg(feature = "std")]
278fn instant_to_f64(instant: &unix_time::Instant) -> f64 {
279    instant.secs() as f64 + (instant.subsec_nanos() as f64 / 1e9)
280}
281
282/// Compute clock offset and round-trip delay from the four NTP timestamps
283/// using era-aware `Instant` values.
284#[cfg(feature = "std")]
285pub(crate) fn compute_offset_delay(
286    t1: &unix_time::Instant,
287    t2: &unix_time::Instant,
288    t3: &unix_time::Instant,
289    t4: &unix_time::Instant,
290) -> (f64, f64) {
291    let t1 = instant_to_f64(t1);
292    let t2 = instant_to_f64(t2);
293    let t3 = instant_to_f64(t3);
294    let t4 = instant_to_f64(t4);
295    let offset = ((t2 - t1) + (t3 - t4)) / 2.0;
296    let delay = (t4 - t1) - (t3 - t2);
297    (offset, delay)
298}
299
300/// Build an NTP client request packet and serialize it.
301///
302/// Returns the serialized buffer and the origin timestamp (T1).
303#[cfg(feature = "std")]
304pub(crate) fn build_request_packet() -> io::Result<(
305    [u8; protocol::Packet::PACKED_SIZE_BYTES],
306    protocol::TimestampFormat,
307)> {
308    let packet = protocol::Packet {
309        leap_indicator: protocol::LeapIndicator::default(),
310        version: protocol::Version::V4,
311        mode: protocol::Mode::Client,
312        stratum: protocol::Stratum::UNSPECIFIED,
313        poll: 0,
314        precision: 0,
315        root_delay: protocol::ShortFormat::default(),
316        root_dispersion: protocol::ShortFormat::default(),
317        reference_id: protocol::ReferenceIdentifier::PrimarySource(protocol::PrimarySource::Null),
318        reference_timestamp: protocol::TimestampFormat::default(),
319        origin_timestamp: protocol::TimestampFormat::default(),
320        receive_timestamp: protocol::TimestampFormat::default(),
321        transmit_timestamp: unix_time::Instant::now().into(),
322    };
323    let t1 = packet.transmit_timestamp;
324    let mut send_buf = [0u8; protocol::Packet::PACKED_SIZE_BYTES];
325    (&mut send_buf[..]).write_bytes(packet)?;
326    Ok((send_buf, t1))
327}
328
329/// Parse and validate an NTP server response, performing all checks except
330/// origin timestamp verification.
331///
332/// Records T4 (destination timestamp) immediately, then validates source IP,
333/// packet size, mode, Kiss-o'-Death codes, transmit timestamp, and
334/// unsynchronized clock status.
335///
336/// Returns the parsed packet and the destination timestamp (T4). This is used
337/// by both the one-shot [`validate_response`] and the continuous client (which
338/// does its own origin timestamp handling for interleaved mode support).
339#[cfg(feature = "std")]
340pub(crate) fn parse_and_validate_response(
341    recv_buf: &[u8],
342    recv_len: usize,
343    src_addr: SocketAddr,
344    resolved_addrs: &[SocketAddr],
345) -> io::Result<(protocol::Packet, protocol::TimestampFormat)> {
346    // Record T4 (destination timestamp) immediately.
347    let t4_instant = unix_time::Instant::now();
348    let t4: protocol::TimestampFormat = t4_instant.into();
349
350    // Verify the response came from one of the resolved addresses (IP only, port may differ).
351    if !resolved_addrs.iter().any(|a| a.ip() == src_addr.ip()) {
352        return Err(io::Error::new(
353            io::ErrorKind::InvalidData,
354            "response from unexpected source address",
355        ));
356    }
357
358    // Verify minimum packet size.
359    if recv_len < protocol::Packet::PACKED_SIZE_BYTES {
360        return Err(io::Error::new(
361            io::ErrorKind::InvalidData,
362            "NTP response too short",
363        ));
364    }
365
366    // Parse the first 48 bytes as an NTP packet (ignoring extension fields/MAC).
367    let response: protocol::Packet =
368        (&recv_buf[..protocol::Packet::PACKED_SIZE_BYTES]).read_bytes()?;
369
370    // Validate server mode (RFC 5905 Section 8).
371    if response.mode != protocol::Mode::Server {
372        return Err(io::Error::new(
373            io::ErrorKind::InvalidData,
374            "unexpected response mode (expected Server)",
375        ));
376    }
377
378    // Enforce Kiss-o'-Death codes (RFC 5905 Section 7.4).
379    if let protocol::ReferenceIdentifier::KissOfDeath(kod) = response.reference_id {
380        return Err(io::Error::new(
381            io::ErrorKind::ConnectionRefused,
382            KissOfDeathError { code: kod },
383        ));
384    }
385
386    // Validate that the server's transmit timestamp is non-zero.
387    if response.transmit_timestamp.seconds == 0 && response.transmit_timestamp.fraction == 0 {
388        return Err(io::Error::new(
389            io::ErrorKind::InvalidData,
390            "server transmit timestamp is zero",
391        ));
392    }
393
394    // Reject unsynchronized servers (LI=Unknown with non-zero stratum).
395    if response.leap_indicator == protocol::LeapIndicator::Unknown
396        && response.stratum != protocol::Stratum::UNSPECIFIED
397    {
398        return Err(io::Error::new(
399            io::ErrorKind::InvalidData,
400            "server reports unsynchronized clock",
401        ));
402    }
403
404    Ok((response, t4))
405}
406
407/// Validate and parse an NTP server response (one-shot API).
408///
409/// Delegates to [`parse_and_validate_response`] for common checks, then
410/// verifies the origin timestamp (anti-replay) and computes clock offset
411/// and round-trip delay.
412#[cfg(feature = "std")]
413pub(crate) fn validate_response(
414    recv_buf: &[u8],
415    recv_len: usize,
416    src_addr: SocketAddr,
417    resolved_addrs: &[SocketAddr],
418    t1: &protocol::TimestampFormat,
419) -> io::Result<NtpResult> {
420    let (response, t4) = parse_and_validate_response(recv_buf, recv_len, src_addr, resolved_addrs)?;
421
422    // Validate origin timestamp matches what we sent (anti-replay, RFC 5905 Section 8).
423    if response.origin_timestamp != *t1 {
424        return Err(io::Error::new(
425            io::ErrorKind::InvalidData,
426            "origin timestamp mismatch: response does not match our request",
427        ));
428    }
429
430    // Convert all four timestamps to Instant for era-aware offset/delay computation.
431    let t4_instant = unix_time::Instant::from(t4);
432    let t1_instant = unix_time::timestamp_to_instant(*t1, &t4_instant);
433    let t2_instant = unix_time::timestamp_to_instant(response.receive_timestamp, &t4_instant);
434    let t3_instant = unix_time::timestamp_to_instant(response.transmit_timestamp, &t4_instant);
435
436    let (offset_seconds, delay_seconds) =
437        compute_offset_delay(&t1_instant, &t2_instant, &t3_instant, &t4_instant);
438
439    Ok(NtpResult {
440        packet: response,
441        destination_timestamp: t4,
442        offset_seconds,
443        delay_seconds,
444    })
445}
446
447/// Send a blocking request to an NTP server with a hardcoded 5 second timeout.
448///
449/// This is a convenience wrapper around [`request_with_timeout`] with a 5 second timeout.
450///
451/// # Arguments
452///
453/// * `addr` - Any valid socket address (e.g., `"time.nist.gov:123"` or `"192.168.1.1:123"`)
454///
455/// # Returns
456///
457/// Returns an [`NtpResult`] containing the server's response packet and computed timing
458/// information, or an error if the server cannot be reached or the response is invalid.
459///
460/// # Examples
461///
462/// ```no_run
463/// # use std::error::Error;
464/// # fn main() -> Result<(), Box<dyn Error>> {
465/// // Request time from NTP server
466/// let result = ntp::request("time.nist.gov:123")?;
467///
468/// // Access packet fields directly via Deref
469/// println!("Server time: {:?}", result.transmit_timestamp);
470/// println!("Stratum: {:?}", result.stratum);
471///
472/// // Access computed timing information
473/// println!("Offset: {:.6} seconds", result.offset_seconds);
474/// println!("Delay: {:.6} seconds", result.delay_seconds);
475/// # Ok(())
476/// # }
477/// ```
478///
479/// # Errors
480///
481/// Returns `io::Error` if:
482/// - Cannot bind to local UDP socket
483/// - Network timeout (5 seconds for read/write)
484/// - Invalid NTP packet response
485/// - DNS resolution fails
486/// - Response fails validation (wrong mode, origin timestamp mismatch, etc.)
487/// - Server sent a Kiss-o'-Death packet (see [`KissOfDeathError`])
488#[cfg(feature = "std")]
489pub fn request<A: ToSocketAddrs>(addr: A) -> io::Result<NtpResult> {
490    request_with_timeout(addr, Duration::from_secs(5))
491}
492
493/// Send a blocking request to an NTP server with a configurable timeout.
494///
495/// Constructs an NTPv4 client-mode packet, sends it to the specified server, and validates
496/// the response per RFC 5905. Returns the parsed response along with computed clock offset
497/// and round-trip delay.
498///
499/// # Arguments
500///
501/// * `addr` - Any valid socket address (e.g., `"time.nist.gov:123"` or `"192.168.1.1:123"`)
502/// * `timeout` - Maximum duration to wait for both sending and receiving the NTP packet
503///
504/// # Returns
505///
506/// Returns an [`NtpResult`] containing the server's response packet and computed timing
507/// information, or an error if the server cannot be reached or the response is invalid.
508///
509/// # Examples
510///
511/// ```no_run
512/// # use std::error::Error;
513/// # use std::time::Duration;
514/// # fn main() -> Result<(), Box<dyn Error>> {
515/// // Request time with a 10 second timeout
516/// let result = ntp::request_with_timeout("time.nist.gov:123", Duration::from_secs(10))?;
517/// println!("Offset: {:.6} seconds", result.offset_seconds);
518/// println!("Delay: {:.6} seconds", result.delay_seconds);
519/// # Ok(())
520/// # }
521/// ```
522///
523/// # Errors
524///
525/// Returns `io::Error` if:
526/// - Cannot bind to local UDP socket
527/// - Network timeout (specified duration exceeded)
528/// - Invalid NTP packet response
529/// - DNS resolution fails
530/// - Response source address does not match the target server
531/// - Response origin timestamp does not match our request (anti-replay)
532/// - Server responds with unexpected mode or zero transmit timestamp
533/// - Server reports unsynchronized clock (LI=Unknown with non-zero stratum)
534/// - Server sent a Kiss-o'-Death packet (see [`KissOfDeathError`])
535#[cfg(feature = "std")]
536pub fn request_with_timeout<A: ToSocketAddrs>(addr: A, timeout: Duration) -> io::Result<NtpResult> {
537    // Resolve the target address eagerly so we can verify the response source.
538    let resolved_addrs: Vec<SocketAddr> = addr.to_socket_addrs()?.collect();
539    if resolved_addrs.is_empty() {
540        return Err(io::Error::new(
541            io::ErrorKind::InvalidInput,
542            "address resolved to no socket addresses",
543        ));
544    }
545    let target_addr = resolved_addrs[0];
546
547    // Build the request packet (shared with async path).
548    let (send_buf, t1) = build_request_packet()?;
549
550    // Create the socket from which we will send the packet.
551    let sock = UdpSocket::bind(bind_addr_for(&target_addr))?;
552    sock.set_read_timeout(Some(timeout))?;
553    sock.set_write_timeout(Some(timeout))?;
554
555    // Send the data.
556    let sz = sock.send_to(&send_buf, target_addr)?;
557    debug!("{:?}", sock.local_addr());
558    debug!("sent: {}", sz);
559
560    // Receive the response into a larger buffer to accommodate extension fields.
561    let mut recv_buf = [0u8; 1024];
562    let (recv_len, src_addr) = sock.recv_from(&mut recv_buf[..])?;
563    debug!("recv: {} bytes from {:?}", recv_len, src_addr);
564
565    // Validate and parse the response (shared with async path).
566    validate_response(&recv_buf, recv_len, src_addr, &resolved_addrs, &t1)
567}
568
569#[cfg(all(test, feature = "std"))]
570#[test]
571fn test_request_nist() {
572    match request_with_timeout("time.nist.gov:123", Duration::from_secs(10)) {
573        Ok(_) => {}
574        Err(e) if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::TimedOut => {
575            eprintln!("skipping test_request_nist: NTP port unreachable ({e})");
576        }
577        Err(e) => panic!("unexpected error from time.nist.gov: {e}"),
578    }
579}
580
581#[cfg(all(test, feature = "std"))]
582#[test]
583fn test_request_nist_alt() {
584    match request_with_timeout("time-a-g.nist.gov:123", Duration::from_secs(10)) {
585        Ok(_) => {}
586        Err(e) if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::TimedOut => {
587            eprintln!("skipping test_request_nist_alt: NTP port unreachable ({e})");
588        }
589        Err(e) => panic!("unexpected error from time-a-g.nist.gov: {e}"),
590    }
591}
592
593#[cfg(all(test, feature = "std"))]
594mod tests {
595    use super::*;
596
597    // ── compute_offset_delay ──────────────────────────────────────
598
599    #[test]
600    fn test_offset_delay_symmetric() {
601        // T1=0, T2=0.5, T3=0.5, T4=1.0
602        // offset = ((0.5-0)+(0.5-1))/2 = (0.5+(-0.5))/2 = 0
603        // delay = (1-0)-(0.5-0.5) = 1.0
604        let t1 = unix_time::Instant::new(0, 0);
605        let t2 = unix_time::Instant::new(0, 500_000_000);
606        let t3 = unix_time::Instant::new(0, 500_000_000);
607        let t4 = unix_time::Instant::new(1, 0);
608        let (offset, delay) = compute_offset_delay(&t1, &t2, &t3, &t4);
609        assert!(offset.abs() < 1e-9, "expected ~0 offset, got {offset}");
610        assert!(
611            (delay - 1.0).abs() < 1e-9,
612            "expected 1.0 delay, got {delay}"
613        );
614    }
615
616    #[test]
617    fn test_offset_delay_local_behind() {
618        // Client behind by 1s: T1=0, T2=1.5, T3=1.5, T4=1.0
619        // offset = ((1.5-0)+(1.5-1))/2 = (1.5+0.5)/2 = 1.0
620        // delay = (1-0)-(1.5-1.5) = 1.0
621        let t1 = unix_time::Instant::new(0, 0);
622        let t2 = unix_time::Instant::new(1, 500_000_000);
623        let t3 = unix_time::Instant::new(1, 500_000_000);
624        let t4 = unix_time::Instant::new(1, 0);
625        let (offset, delay) = compute_offset_delay(&t1, &t2, &t3, &t4);
626        assert!(
627            (offset - 1.0).abs() < 1e-9,
628            "expected 1.0 offset, got {offset}"
629        );
630        assert!(
631            (delay - 1.0).abs() < 1e-9,
632            "expected 1.0 delay, got {delay}"
633        );
634    }
635
636    #[test]
637    fn test_offset_delay_local_ahead() {
638        // Client ahead by 1s: T1=10, T2=9.25, T3=9.75, T4=11
639        // offset = ((9.25-10)+(9.75-11))/2 = (-0.75+(-1.25))/2 = -1.0
640        // delay = (11-10)-(9.75-9.25) = 1.0 - 0.5 = 0.5
641        let t1 = unix_time::Instant::new(10, 0);
642        let t2 = unix_time::Instant::new(9, 250_000_000);
643        let t3 = unix_time::Instant::new(9, 750_000_000);
644        let t4 = unix_time::Instant::new(11, 0);
645        let (offset, delay) = compute_offset_delay(&t1, &t2, &t3, &t4);
646        assert!(
647            (offset - (-1.0)).abs() < 1e-9,
648            "expected -1.0 offset, got {offset}"
649        );
650        assert!(
651            (delay - 0.5).abs() < 1e-9,
652            "expected 0.5 delay, got {delay}"
653        );
654    }
655
656    #[test]
657    fn test_offset_delay_zero_processing_time() {
658        // Server processes instantly, RTT=0.1s: T1=0, T2=0.05, T3=0.05, T4=0.1
659        // offset = ((0.05-0)+(0.05-0.1))/2 = (0.05+(-0.05))/2 = 0
660        // delay = (0.1-0)-(0.05-0.05) = 0.1
661        let t1 = unix_time::Instant::new(0, 0);
662        let t2 = unix_time::Instant::new(0, 50_000_000);
663        let t3 = unix_time::Instant::new(0, 50_000_000);
664        let t4 = unix_time::Instant::new(0, 100_000_000);
665        let (offset, delay) = compute_offset_delay(&t1, &t2, &t3, &t4);
666        assert!(offset.abs() < 1e-9, "expected ~0 offset, got {offset}");
667        assert!(
668            (delay - 0.1).abs() < 1e-9,
669            "expected 0.1 delay, got {delay}"
670        );
671    }
672
673    // ── build_request_packet ──────────────────────────────────────
674
675    #[test]
676    fn test_build_request_packet_structure() {
677        let (buf, t1) = build_request_packet().unwrap();
678
679        // Deserialize and verify fields.
680        let pkt: protocol::Packet = (&buf[..protocol::Packet::PACKED_SIZE_BYTES])
681            .read_bytes()
682            .unwrap();
683        assert_eq!(pkt.version, protocol::Version::V4);
684        assert_eq!(pkt.mode, protocol::Mode::Client);
685        assert_eq!(pkt.stratum, protocol::Stratum::UNSPECIFIED);
686        assert_eq!(pkt.transmit_timestamp, t1);
687        // T1 should be non-zero (set to current time).
688        assert!(t1.seconds != 0 || t1.fraction != 0);
689    }
690
691    #[test]
692    fn test_build_request_packet_size() {
693        let (buf, _) = build_request_packet().unwrap();
694        assert_eq!(buf.len(), protocol::Packet::PACKED_SIZE_BYTES);
695        assert_eq!(buf.len(), 48);
696    }
697
698    // ── parse_and_validate_response ───────────────────────────────
699
700    /// Helper: build a valid 48-byte server response buffer.
701    fn make_server_response(
702        mode: protocol::Mode,
703        li: protocol::LeapIndicator,
704        stratum: protocol::Stratum,
705        ref_id: protocol::ReferenceIdentifier,
706        transmit_secs: u32,
707    ) -> [u8; 48] {
708        let pkt = protocol::Packet {
709            leap_indicator: li,
710            version: protocol::Version::V4,
711            mode,
712            stratum,
713            poll: 6,
714            precision: -20,
715            root_delay: protocol::ShortFormat::default(),
716            root_dispersion: protocol::ShortFormat::default(),
717            reference_id: ref_id,
718            reference_timestamp: protocol::TimestampFormat::default(),
719            origin_timestamp: protocol::TimestampFormat {
720                seconds: 100,
721                fraction: 0,
722            },
723            receive_timestamp: protocol::TimestampFormat {
724                seconds: 3_913_056_000,
725                fraction: 0,
726            },
727            transmit_timestamp: protocol::TimestampFormat {
728                seconds: transmit_secs,
729                fraction: 1,
730            },
731        };
732        let mut buf = [0u8; 48];
733        (&mut buf[..]).write_bytes(pkt).unwrap();
734        buf
735    }
736
737    fn valid_server_buf() -> [u8; 48] {
738        make_server_response(
739            protocol::Mode::Server,
740            protocol::LeapIndicator::NoWarning,
741            protocol::Stratum(2),
742            protocol::ReferenceIdentifier::SecondaryOrClient([127, 0, 0, 1]),
743            3_913_056_001,
744        )
745    }
746
747    fn src_addr() -> SocketAddr {
748        "127.0.0.1:123".parse().unwrap()
749    }
750
751    #[test]
752    fn test_validate_accepts_valid_response() {
753        let buf = valid_server_buf();
754        let addrs = vec![src_addr()];
755        let result = parse_and_validate_response(&buf, 48, src_addr(), &addrs);
756        assert!(result.is_ok());
757        let (pkt, _t4) = result.unwrap();
758        assert_eq!(pkt.mode, protocol::Mode::Server);
759    }
760
761    #[test]
762    fn test_validate_rejects_wrong_source_ip() {
763        let buf = valid_server_buf();
764        let addrs = vec!["10.0.0.1:123".parse().unwrap()];
765        let result = parse_and_validate_response(&buf, 48, src_addr(), &addrs);
766        assert!(result.is_err());
767        assert!(
768            result
769                .unwrap_err()
770                .to_string()
771                .contains("unexpected source")
772        );
773    }
774
775    #[test]
776    fn test_validate_rejects_short_packet() {
777        let buf = valid_server_buf();
778        let addrs = vec![src_addr()];
779        let result = parse_and_validate_response(&buf, 47, src_addr(), &addrs);
780        assert!(result.is_err());
781        assert!(result.unwrap_err().to_string().contains("too short"));
782    }
783
784    #[test]
785    fn test_validate_rejects_client_mode() {
786        let buf = make_server_response(
787            protocol::Mode::Client,
788            protocol::LeapIndicator::NoWarning,
789            protocol::Stratum(2),
790            protocol::ReferenceIdentifier::SecondaryOrClient([127, 0, 0, 1]),
791            3_913_056_001,
792        );
793        let addrs = vec![src_addr()];
794        let result = parse_and_validate_response(&buf, 48, src_addr(), &addrs);
795        assert!(result.is_err());
796        assert!(
797            result
798                .unwrap_err()
799                .to_string()
800                .contains("unexpected response mode")
801        );
802    }
803
804    #[test]
805    fn test_validate_rejects_kiss_of_death() {
806        let buf = make_server_response(
807            protocol::Mode::Server,
808            protocol::LeapIndicator::NoWarning,
809            protocol::Stratum::UNSPECIFIED,
810            protocol::ReferenceIdentifier::KissOfDeath(protocol::KissOfDeath::Deny),
811            3_913_056_001,
812        );
813        let addrs = vec![src_addr()];
814        let result = parse_and_validate_response(&buf, 48, src_addr(), &addrs);
815        let err = result.unwrap_err();
816        assert_eq!(err.kind(), io::ErrorKind::ConnectionRefused);
817        let kod = err
818            .get_ref()
819            .unwrap()
820            .downcast_ref::<KissOfDeathError>()
821            .unwrap();
822        assert!(matches!(kod.code, protocol::KissOfDeath::Deny));
823    }
824
825    #[test]
826    fn test_validate_rejects_zero_transmit() {
827        // Build a packet with fully zero transmit timestamp.
828        let pkt = protocol::Packet {
829            leap_indicator: protocol::LeapIndicator::NoWarning,
830            version: protocol::Version::V4,
831            mode: protocol::Mode::Server,
832            stratum: protocol::Stratum(2),
833            poll: 6,
834            precision: -20,
835            root_delay: protocol::ShortFormat::default(),
836            root_dispersion: protocol::ShortFormat::default(),
837            reference_id: protocol::ReferenceIdentifier::SecondaryOrClient([127, 0, 0, 1]),
838            reference_timestamp: protocol::TimestampFormat::default(),
839            origin_timestamp: protocol::TimestampFormat::default(),
840            receive_timestamp: protocol::TimestampFormat::default(),
841            transmit_timestamp: protocol::TimestampFormat {
842                seconds: 0,
843                fraction: 0,
844            },
845        };
846        let mut raw = [0u8; 48];
847        (&mut raw[..]).write_bytes(pkt).unwrap();
848        let addrs = vec![src_addr()];
849        let result = parse_and_validate_response(&raw, 48, src_addr(), &addrs);
850        assert!(result.is_err());
851        assert!(
852            result
853                .unwrap_err()
854                .to_string()
855                .contains("transmit timestamp is zero")
856        );
857    }
858
859    #[test]
860    fn test_validate_rejects_unsynchronized() {
861        let buf = make_server_response(
862            protocol::Mode::Server,
863            protocol::LeapIndicator::Unknown,
864            protocol::Stratum(2), // non-zero stratum + LI=Unknown = unsynchronized
865            protocol::ReferenceIdentifier::SecondaryOrClient([127, 0, 0, 1]),
866            3_913_056_001,
867        );
868        let addrs = vec![src_addr()];
869        let result = parse_and_validate_response(&buf, 48, src_addr(), &addrs);
870        assert!(result.is_err());
871        assert!(result.unwrap_err().to_string().contains("unsynchronized"));
872    }
873
874    #[test]
875    fn test_validate_allows_li_unknown_stratum_zero() {
876        // LI=Unknown with stratum 0 (UNSPECIFIED) is OK — it's a KoD or reference clock.
877        // But stratum 0 with a non-KoD ref_id should pass the LI check.
878        let buf = make_server_response(
879            protocol::Mode::Server,
880            protocol::LeapIndicator::Unknown,
881            protocol::Stratum::UNSPECIFIED,
882            protocol::ReferenceIdentifier::PrimarySource(protocol::PrimarySource::Gps),
883            3_913_056_001,
884        );
885        let addrs = vec![src_addr()];
886        let result = parse_and_validate_response(&buf, 48, src_addr(), &addrs);
887        assert!(result.is_ok());
888    }
889
890    #[test]
891    fn test_validate_accepts_different_port() {
892        // Source port doesn't need to match — only IP.
893        let buf = valid_server_buf();
894        let addrs = vec!["127.0.0.1:456".parse().unwrap()];
895        let result = parse_and_validate_response(&buf, 48, src_addr(), &addrs);
896        assert!(result.is_ok());
897    }
898
899    // ── KissOfDeathError display ──────────────────────────────────
900
901    #[test]
902    fn test_kod_display_deny() {
903        let kod = KissOfDeathError {
904            code: protocol::KissOfDeath::Deny,
905        };
906        assert!(kod.to_string().contains("DENY"));
907    }
908
909    #[test]
910    fn test_kod_display_rstr() {
911        let kod = KissOfDeathError {
912            code: protocol::KissOfDeath::Rstr,
913        };
914        assert!(kod.to_string().contains("RSTR"));
915    }
916
917    #[test]
918    fn test_kod_display_rate() {
919        let kod = KissOfDeathError {
920            code: protocol::KissOfDeath::Rate,
921        };
922        assert!(kod.to_string().contains("RATE"));
923    }
924}