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 = "0.pool.ntp.org: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#![forbid(unsafe_code)]
24#![warn(missing_docs)]
25
26use log::debug;
27use protocol::{ConstPackedSizeBytes, ReadBytes, WriteBytes};
28use std::io;
29use std::net::{SocketAddr, ToSocketAddrs, UdpSocket};
30use std::ops::Deref;
31use std::time::Duration;
32
33/// Select the appropriate bind address based on the target address family.
34///
35/// Returns `"0.0.0.0:0"` for IPv4 targets and `"[::]:0"` for IPv6 targets.
36pub(crate) fn bind_addr_for(target: &SocketAddr) -> &'static str {
37 match target {
38 SocketAddr::V4(_) => "0.0.0.0:0",
39 SocketAddr::V6(_) => "[::]:0",
40 }
41}
42
43pub mod protocol;
44/// Unix time conversion utilities for NTP timestamps.
45///
46/// Provides the `Instant` type for converting between NTP timestamps
47/// (seconds since 1900-01-01) and Unix timestamps (seconds since 1970-01-01).
48pub mod unix_time;
49/// NTP extension field parsing and NTS extension types.
50///
51/// Provides types for parsing and serializing NTP extension fields (RFC 7822)
52/// and NTS-specific extension field types (RFC 8915).
53pub mod extension;
54
55/// Clock sample filtering for the continuous NTP client.
56///
57/// Implements a simplified version of the RFC 5905 Section 10 clock filter
58/// algorithm.
59#[cfg(feature = "tokio")]
60pub mod filter;
61
62/// Continuous NTP client with adaptive poll interval management and interleaved mode.
63///
64/// Enable with the `tokio` feature flag:
65///
66/// ```toml
67/// [dependencies]
68/// ntp_usg = { version = "0.9", features = ["tokio"] }
69/// ```
70#[cfg(feature = "tokio")]
71pub mod client;
72
73/// Network Time Security (NTS) client (RFC 8915).
74///
75/// Provides authenticated NTP using TLS 1.3 key establishment and AEAD
76/// per-packet authentication. Enable with the `nts` feature flag:
77///
78/// ```toml
79/// [dependencies]
80/// ntp_usg = { version = "0.9", features = ["nts"] }
81/// ```
82#[cfg(feature = "nts")]
83pub mod nts;
84
85/// Async NTP client functions using the Tokio runtime.
86///
87/// Enable with the `tokio` feature flag:
88///
89/// ```toml
90/// [dependencies]
91/// ntp_usg = { version = "0.9", features = ["tokio"] }
92/// ```
93///
94/// See [`async_ntp::request`] and [`async_ntp::request_with_timeout`] for details.
95#[cfg(feature = "tokio")]
96pub mod async_ntp;
97
98/// Error returned when the server responds with a Kiss-o'-Death (KoD) packet.
99///
100/// Per RFC 5905 Section 7.4, recipients of kiss codes MUST inspect them and take
101/// the described actions. This error is returned as the inner error of an
102/// [`io::Error`] with kind [`io::ErrorKind::ConnectionRefused`], and can be
103/// extracted via [`io::Error::get_ref`] and `downcast_ref`.
104///
105/// # Caller Responsibilities
106///
107/// - **DENY / RSTR**: The caller MUST stop sending packets to this server.
108/// - **RATE**: The caller MUST reduce its polling interval before retrying.
109///
110/// # Examples
111///
112/// ```no_run
113/// # use std::error::Error;
114/// # fn main() -> Result<(), Box<dyn Error>> {
115/// match ntp::request("pool.ntp.org:123") {
116/// Ok(result) => println!("Offset: {:.6}s", result.offset_seconds),
117/// Err(e) => {
118/// if let Some(kod) = e.get_ref().and_then(|inner| inner.downcast_ref::<ntp::KissOfDeathError>()) {
119/// eprintln!("Kiss-o'-Death: {:?}", kod.code);
120/// }
121/// }
122/// }
123/// # Ok(())
124/// # }
125/// ```
126#[derive(Clone, Copy, Debug)]
127pub struct KissOfDeathError {
128 /// The specific kiss code received from the server.
129 pub code: protocol::KissOfDeath,
130}
131
132impl std::fmt::Display for KissOfDeathError {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 match self.code {
135 protocol::KissOfDeath::Deny => {
136 write!(f, "server sent Kiss-o'-Death DENY: access denied, stop querying this server")
137 }
138 protocol::KissOfDeath::Rstr => {
139 write!(f, "server sent Kiss-o'-Death RSTR: access restricted, stop querying this server")
140 }
141 protocol::KissOfDeath::Rate => {
142 write!(f, "server sent Kiss-o'-Death RATE: reduce polling interval")
143 }
144 }
145 }
146}
147
148impl std::error::Error for KissOfDeathError {}
149
150/// The result of an NTP request, containing the server's response packet
151/// along with computed timing information.
152///
153/// This struct implements `Deref<Target = protocol::Packet>`, so all packet
154/// fields can be accessed directly (e.g., `result.transmit_timestamp`).
155#[derive(Clone, Copy, Debug)]
156pub struct NtpResult {
157 /// The parsed NTP response packet from the server.
158 pub packet: protocol::Packet,
159 /// The destination timestamp (T4): local time when the response was received.
160 ///
161 /// Expressed as an NTP `TimestampFormat` for consistency with the packet timestamps.
162 pub destination_timestamp: protocol::TimestampFormat,
163 /// Clock offset: the estimated difference between the local clock and the server clock.
164 ///
165 /// Computed as `((T2 - T1) + (T3 - T4)) / 2` per RFC 5905 Section 8, where:
166 /// - T1 = origin timestamp (client transmit time)
167 /// - T2 = receive timestamp (server receive time)
168 /// - T3 = transmit timestamp (server transmit time)
169 /// - T4 = destination timestamp (client receive time)
170 ///
171 /// A positive value means the local clock is behind the server.
172 /// A negative value means the local clock is ahead of the server.
173 pub offset_seconds: f64,
174 /// Round-trip delay between the client and server.
175 ///
176 /// Computed as `(T4 - T1) - (T3 - T2)` per RFC 5905 Section 8.
177 pub delay_seconds: f64,
178}
179
180impl Deref for NtpResult {
181 type Target = protocol::Packet;
182 fn deref(&self) -> &Self::Target {
183 &self.packet
184 }
185}
186
187/// Convert a Unix `Instant` to seconds as f64 (relative to Unix epoch).
188fn instant_to_f64(instant: &unix_time::Instant) -> f64 {
189 instant.secs() as f64 + (instant.subsec_nanos() as f64 / 1e9)
190}
191
192/// Compute clock offset and round-trip delay from the four NTP timestamps
193/// using era-aware `Instant` values.
194pub(crate) fn compute_offset_delay(
195 t1: &unix_time::Instant,
196 t2: &unix_time::Instant,
197 t3: &unix_time::Instant,
198 t4: &unix_time::Instant,
199) -> (f64, f64) {
200 let t1 = instant_to_f64(t1);
201 let t2 = instant_to_f64(t2);
202 let t3 = instant_to_f64(t3);
203 let t4 = instant_to_f64(t4);
204 let offset = ((t2 - t1) + (t3 - t4)) / 2.0;
205 let delay = (t4 - t1) - (t3 - t2);
206 (offset, delay)
207}
208
209/// Build an NTP client request packet and serialize it.
210///
211/// Returns the serialized buffer and the origin timestamp (T1).
212pub(crate) fn build_request_packet(
213) -> io::Result<([u8; protocol::Packet::PACKED_SIZE_BYTES], protocol::TimestampFormat)> {
214 let packet = protocol::Packet {
215 leap_indicator: protocol::LeapIndicator::default(),
216 version: protocol::Version::V4,
217 mode: protocol::Mode::Client,
218 stratum: protocol::Stratum::UNSPECIFIED,
219 poll: 0,
220 precision: 0,
221 root_delay: protocol::ShortFormat::default(),
222 root_dispersion: protocol::ShortFormat::default(),
223 reference_id: protocol::ReferenceIdentifier::PrimarySource(protocol::PrimarySource::Null),
224 reference_timestamp: protocol::TimestampFormat::default(),
225 origin_timestamp: protocol::TimestampFormat::default(),
226 receive_timestamp: protocol::TimestampFormat::default(),
227 transmit_timestamp: unix_time::Instant::now().into(),
228 };
229 let t1 = packet.transmit_timestamp;
230 let mut send_buf = [0u8; protocol::Packet::PACKED_SIZE_BYTES];
231 (&mut send_buf[..]).write_bytes(packet)?;
232 Ok((send_buf, t1))
233}
234
235/// Parse and validate an NTP server response, performing all checks except
236/// origin timestamp verification.
237///
238/// Records T4 (destination timestamp) immediately, then validates source IP,
239/// packet size, mode, Kiss-o'-Death codes, transmit timestamp, and
240/// unsynchronized clock status.
241///
242/// Returns the parsed packet and the destination timestamp (T4). This is used
243/// by both the one-shot [`validate_response`] and the continuous client (which
244/// does its own origin timestamp handling for interleaved mode support).
245pub(crate) fn parse_and_validate_response(
246 recv_buf: &[u8],
247 recv_len: usize,
248 src_addr: SocketAddr,
249 resolved_addrs: &[SocketAddr],
250) -> io::Result<(protocol::Packet, protocol::TimestampFormat)> {
251 // Record T4 (destination timestamp) immediately.
252 let t4_instant = unix_time::Instant::now();
253 let t4: protocol::TimestampFormat = t4_instant.into();
254
255 // Verify the response came from one of the resolved addresses (IP only, port may differ).
256 if !resolved_addrs.iter().any(|a| a.ip() == src_addr.ip()) {
257 return Err(io::Error::new(
258 io::ErrorKind::InvalidData,
259 "response from unexpected source address",
260 ));
261 }
262
263 // Verify minimum packet size.
264 if recv_len < protocol::Packet::PACKED_SIZE_BYTES {
265 return Err(io::Error::new(
266 io::ErrorKind::InvalidData,
267 "NTP response too short",
268 ));
269 }
270
271 // Parse the first 48 bytes as an NTP packet (ignoring extension fields/MAC).
272 let response: protocol::Packet =
273 (&recv_buf[..protocol::Packet::PACKED_SIZE_BYTES]).read_bytes()?;
274
275 // Validate server mode (RFC 5905 Section 8).
276 if response.mode != protocol::Mode::Server {
277 return Err(io::Error::new(
278 io::ErrorKind::InvalidData,
279 "unexpected response mode (expected Server)",
280 ));
281 }
282
283 // Enforce Kiss-o'-Death codes (RFC 5905 Section 7.4).
284 if let protocol::ReferenceIdentifier::KissOfDeath(kod) = response.reference_id {
285 return Err(io::Error::new(
286 io::ErrorKind::ConnectionRefused,
287 KissOfDeathError { code: kod },
288 ));
289 }
290
291 // Validate that the server's transmit timestamp is non-zero.
292 if response.transmit_timestamp.seconds == 0 && response.transmit_timestamp.fraction == 0 {
293 return Err(io::Error::new(
294 io::ErrorKind::InvalidData,
295 "server transmit timestamp is zero",
296 ));
297 }
298
299 // Reject unsynchronized servers (LI=Unknown with non-zero stratum).
300 if response.leap_indicator == protocol::LeapIndicator::Unknown
301 && response.stratum != protocol::Stratum::UNSPECIFIED
302 {
303 return Err(io::Error::new(
304 io::ErrorKind::InvalidData,
305 "server reports unsynchronized clock",
306 ));
307 }
308
309 Ok((response, t4))
310}
311
312/// Validate and parse an NTP server response (one-shot API).
313///
314/// Delegates to [`parse_and_validate_response`] for common checks, then
315/// verifies the origin timestamp (anti-replay) and computes clock offset
316/// and round-trip delay.
317pub(crate) fn validate_response(
318 recv_buf: &[u8],
319 recv_len: usize,
320 src_addr: SocketAddr,
321 resolved_addrs: &[SocketAddr],
322 t1: &protocol::TimestampFormat,
323) -> io::Result<NtpResult> {
324 let (response, t4) =
325 parse_and_validate_response(recv_buf, recv_len, src_addr, resolved_addrs)?;
326
327 // Validate origin timestamp matches what we sent (anti-replay, RFC 5905 Section 8).
328 if response.origin_timestamp != *t1 {
329 return Err(io::Error::new(
330 io::ErrorKind::InvalidData,
331 "origin timestamp mismatch: response does not match our request",
332 ));
333 }
334
335 // Convert all four timestamps to Instant for era-aware offset/delay computation.
336 let t4_instant = unix_time::Instant::from(t4);
337 let t1_instant = unix_time::timestamp_to_instant(*t1, &t4_instant);
338 let t2_instant = unix_time::timestamp_to_instant(response.receive_timestamp, &t4_instant);
339 let t3_instant = unix_time::timestamp_to_instant(response.transmit_timestamp, &t4_instant);
340
341 let (offset_seconds, delay_seconds) =
342 compute_offset_delay(&t1_instant, &t2_instant, &t3_instant, &t4_instant);
343
344 Ok(NtpResult {
345 packet: response,
346 destination_timestamp: t4,
347 offset_seconds,
348 delay_seconds,
349 })
350}
351
352/// Send a blocking request to an NTP server with a hardcoded 5 second timeout.
353///
354/// This is a convenience wrapper around [`request_with_timeout`] with a 5 second timeout.
355///
356/// # Arguments
357///
358/// * `addr` - Any valid socket address (e.g., `"pool.ntp.org:123"` or `"192.168.1.1:123"`)
359///
360/// # Returns
361///
362/// Returns an [`NtpResult`] containing the server's response packet and computed timing
363/// information, or an error if the server cannot be reached or the response is invalid.
364///
365/// # Examples
366///
367/// ```no_run
368/// # use std::error::Error;
369/// # fn main() -> Result<(), Box<dyn Error>> {
370/// // Request time from NTP pool
371/// let result = ntp::request("pool.ntp.org:123")?;
372///
373/// // Access packet fields directly via Deref
374/// println!("Server time: {:?}", result.transmit_timestamp);
375/// println!("Stratum: {:?}", result.stratum);
376///
377/// // Access computed timing information
378/// println!("Offset: {:.6} seconds", result.offset_seconds);
379/// println!("Delay: {:.6} seconds", result.delay_seconds);
380/// # Ok(())
381/// # }
382/// ```
383///
384/// # Errors
385///
386/// Returns `io::Error` if:
387/// - Cannot bind to local UDP socket
388/// - Network timeout (5 seconds for read/write)
389/// - Invalid NTP packet response
390/// - DNS resolution fails
391/// - Response fails validation (wrong mode, origin timestamp mismatch, etc.)
392/// - Server sent a Kiss-o'-Death packet (see [`KissOfDeathError`])
393pub fn request<A: ToSocketAddrs>(addr: A) -> io::Result<NtpResult> {
394 request_with_timeout(addr, Duration::from_secs(5))
395}
396
397/// Send a blocking request to an NTP server with a configurable timeout.
398///
399/// Constructs an NTPv4 client-mode packet, sends it to the specified server, and validates
400/// the response per RFC 5905. Returns the parsed response along with computed clock offset
401/// and round-trip delay.
402///
403/// # Arguments
404///
405/// * `addr` - Any valid socket address (e.g., `"pool.ntp.org:123"` or `"192.168.1.1:123"`)
406/// * `timeout` - Maximum duration to wait for both sending and receiving the NTP packet
407///
408/// # Returns
409///
410/// Returns an [`NtpResult`] containing the server's response packet and computed timing
411/// information, or an error if the server cannot be reached or the response is invalid.
412///
413/// # Examples
414///
415/// ```no_run
416/// # use std::error::Error;
417/// # use std::time::Duration;
418/// # fn main() -> Result<(), Box<dyn Error>> {
419/// // Request time with a 10 second timeout
420/// let result = ntp::request_with_timeout("pool.ntp.org:123", Duration::from_secs(10))?;
421/// println!("Offset: {:.6} seconds", result.offset_seconds);
422/// println!("Delay: {:.6} seconds", result.delay_seconds);
423/// # Ok(())
424/// # }
425/// ```
426///
427/// # Errors
428///
429/// Returns `io::Error` if:
430/// - Cannot bind to local UDP socket
431/// - Network timeout (specified duration exceeded)
432/// - Invalid NTP packet response
433/// - DNS resolution fails
434/// - Response source address does not match the target server
435/// - Response origin timestamp does not match our request (anti-replay)
436/// - Server responds with unexpected mode or zero transmit timestamp
437/// - Server reports unsynchronized clock (LI=Unknown with non-zero stratum)
438/// - Server sent a Kiss-o'-Death packet (see [`KissOfDeathError`])
439pub fn request_with_timeout<A: ToSocketAddrs>(
440 addr: A,
441 timeout: Duration,
442) -> io::Result<NtpResult> {
443 // Resolve the target address eagerly so we can verify the response source.
444 let resolved_addrs: Vec<SocketAddr> = addr.to_socket_addrs()?.collect();
445 if resolved_addrs.is_empty() {
446 return Err(io::Error::new(
447 io::ErrorKind::InvalidInput,
448 "address resolved to no socket addresses",
449 ));
450 }
451 let target_addr = resolved_addrs[0];
452
453 // Build the request packet (shared with async path).
454 let (send_buf, t1) = build_request_packet()?;
455
456 // Create the socket from which we will send the packet.
457 let sock = UdpSocket::bind(bind_addr_for(&target_addr))?;
458 sock.set_read_timeout(Some(timeout))?;
459 sock.set_write_timeout(Some(timeout))?;
460
461 // Send the data.
462 let sz = sock.send_to(&send_buf, target_addr)?;
463 debug!("{:?}", sock.local_addr());
464 debug!("sent: {}", sz);
465
466 // Receive the response into a larger buffer to accommodate extension fields.
467 let mut recv_buf = [0u8; 1024];
468 let (recv_len, src_addr) = sock.recv_from(&mut recv_buf[..])?;
469 debug!("recv: {} bytes from {:?}", recv_len, src_addr);
470
471 // Validate and parse the response (shared with async path).
472 validate_response(&recv_buf, recv_len, src_addr, &resolved_addrs, &t1)
473}
474
475#[test]
476fn test_request_ntp_org() {
477 let res = request("0.pool.ntp.org:123");
478 let _ = res.expect("Failed to get a ntp packet from ntp.org");
479}
480
481#[test]
482fn test_request_google() {
483 let res = request("time.google.com:123");
484 let _ = res.expect("Failed to get a ntp packet from time.google.com");
485}