1use core::fmt;
7
8pub const HEADER_LEN: usize = 48;
10
11pub const UNIX_EPOCH_OFFSET: u64 = 2_208_988_800;
13
14const FRAC: f64 = 4_294_967_296.0; #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
18pub struct NtpTimestamp(pub u64);
19
20impl NtpTimestamp {
21 pub const ZERO: NtpTimestamp = NtpTimestamp(0);
22
23 pub fn from_parts(seconds: u32, fraction: u32) -> Self {
24 NtpTimestamp(((seconds as u64) << 32) | fraction as u64)
25 }
26
27 pub fn seconds(self) -> u32 {
28 (self.0 >> 32) as u32
29 }
30
31 pub fn fraction(self) -> u32 {
32 self.0 as u32
33 }
34
35 pub fn from_unix(secs: i64, nanos: u32) -> Self {
38 let ntp_secs = (secs.wrapping_add(UNIX_EPOCH_OFFSET as i64)) as u64;
39 let frac = ((nanos as u64) << 32) / 1_000_000_000;
40 NtpTimestamp(((ntp_secs & 0xFFFF_FFFF) << 32) | (frac & 0xFFFF_FFFF))
41 }
42
43 pub fn seconds_since(self, earlier: NtpTimestamp) -> f64 {
46 let diff = self.0.wrapping_sub(earlier.0) as i64;
47 diff as f64 / FRAC
48 }
49
50 pub fn is_zero(self) -> bool {
51 self.0 == 0
52 }
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
57pub struct NtpShort(pub u32);
58
59impl NtpShort {
60 pub fn to_seconds(self) -> f64 {
61 self.0 as f64 / 65_536.0
62 }
63
64 pub fn from_seconds(s: f64) -> Self {
65 let clamped = s.clamp(0.0, 65_535.999);
66 NtpShort((clamped * 65_536.0) as u32)
67 }
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74#[repr(u8)]
75pub enum LeapIndicator {
76 NoWarning = 0,
77 LastMinute61 = 1,
78 LastMinute59 = 2,
79 Unsynchronized = 3,
80}
81
82impl LeapIndicator {
83 #[inline(always)]
90 fn from_bits(b: u8) -> Self {
91 match b & 0b11 {
92 0 => LeapIndicator::NoWarning,
93 1 => LeapIndicator::LastMinute61,
94 2 => LeapIndicator::LastMinute59,
95 _ => LeapIndicator::Unsynchronized,
96 }
97 }
98
99 #[inline(always)]
102 fn bits(self) -> u8 {
103 self as u8
104 }
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
110#[repr(u8)]
111pub enum Mode {
112 Reserved = 0,
113 SymmetricActive = 1,
114 SymmetricPassive = 2,
115 Client = 3,
116 Server = 4,
117 Broadcast = 5,
118 Control = 6,
119 Private = 7,
120}
121
122impl Mode {
123 #[inline(always)]
126 fn from_bits(b: u8) -> Self {
127 match b & 0b111 {
128 1 => Mode::SymmetricActive,
129 2 => Mode::SymmetricPassive,
130 3 => Mode::Client,
131 4 => Mode::Server,
132 5 => Mode::Broadcast,
133 6 => Mode::Control,
134 7 => Mode::Private,
135 _ => Mode::Reserved,
136 }
137 }
138
139 #[inline(always)]
140 fn bits(self) -> u8 {
141 self as u8
142 }
143}
144
145#[derive(Clone, Copy, Debug, PartialEq, Eq)]
146pub enum ParseError {
147 TooShort { len: usize },
149 BadVersion { version: u8 },
151}
152
153impl fmt::Display for ParseError {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 match self {
156 ParseError::TooShort { len } => {
157 write!(f, "packet is {len} bytes; NTP header needs {HEADER_LEN}")
158 }
159 ParseError::BadVersion { version } => {
160 write!(f, "unsupported NTP version {version} (expected 3 or 4)")
161 }
162 }
163 }
164}
165
166impl core::error::Error for ParseError {}
167
168#[derive(Clone, Copy, Debug, PartialEq, Eq)]
170pub struct NtpPacket {
171 pub leap: LeapIndicator,
172 pub version: u8,
173 pub mode: Mode,
174 pub stratum: u8,
175 pub poll: i8,
176 pub precision: i8,
177 pub root_delay: NtpShort,
178 pub root_dispersion: NtpShort,
179 pub reference_id: [u8; 4],
180 pub reference_ts: NtpTimestamp,
181 pub origin_ts: NtpTimestamp,
182 pub receive_ts: NtpTimestamp,
183 pub transmit_ts: NtpTimestamp,
184}
185
186#[inline(always)]
200fn be_u32<const AT: usize>(h: &[u8; HEADER_LEN]) -> u32 {
201 u32::from_be_bytes([h[AT], h[AT + 1], h[AT + 2], h[AT + 3]])
202}
203
204#[inline(always)]
206fn be_u64<const AT: usize>(h: &[u8; HEADER_LEN]) -> u64 {
207 u64::from_be_bytes([
208 h[AT],
209 h[AT + 1],
210 h[AT + 2],
211 h[AT + 3],
212 h[AT + 4],
213 h[AT + 5],
214 h[AT + 6],
215 h[AT + 7],
216 ])
217}
218
219impl NtpPacket {
220 pub fn client_request(version: u8, transmit_ts: NtpTimestamp) -> Self {
224 NtpPacket {
225 leap: LeapIndicator::NoWarning,
226 version,
227 mode: Mode::Client,
228 stratum: 0,
229 poll: 0,
230 precision: 0x20u8 as i8,
231 root_delay: NtpShort(0),
232 root_dispersion: NtpShort(0),
233 reference_id: [0; 4],
234 reference_ts: NtpTimestamp::ZERO,
235 origin_ts: NtpTimestamp::ZERO,
236 receive_ts: NtpTimestamp::ZERO,
237 transmit_ts,
238 }
239 }
240
241 pub fn parse(buf: &[u8]) -> Result<NtpPacket, ParseError> {
244 let Some(h) = buf.first_chunk::<HEADER_LEN>() else {
252 return Err(ParseError::TooShort { len: buf.len() });
253 };
254 let b0 = h[0];
255 let version = (b0 >> 3) & 0b111;
256 if !(3..=4).contains(&version) {
257 return Err(ParseError::BadVersion { version });
258 }
259 Ok(NtpPacket {
260 leap: LeapIndicator::from_bits(b0 >> 6),
261 version,
262 mode: Mode::from_bits(b0),
263 stratum: h[1],
264 poll: h[2] as i8,
265 precision: h[3] as i8,
266 root_delay: NtpShort(be_u32::<4>(h)),
267 root_dispersion: NtpShort(be_u32::<8>(h)),
268 reference_id: [h[12], h[13], h[14], h[15]],
269 reference_ts: NtpTimestamp(be_u64::<16>(h)),
270 origin_ts: NtpTimestamp(be_u64::<24>(h)),
271 receive_ts: NtpTimestamp(be_u64::<32>(h)),
272 transmit_ts: NtpTimestamp(be_u64::<40>(h)),
273 })
274 }
275
276 pub fn write(&self, buf: &mut [u8; HEADER_LEN]) {
278 buf[0] = (self.leap.bits() << 6) | ((self.version & 0b111) << 3) | self.mode.bits();
279 buf[1] = self.stratum;
280 buf[2] = self.poll as u8;
281 buf[3] = self.precision as u8;
282 buf[4..8].copy_from_slice(&self.root_delay.0.to_be_bytes());
283 buf[8..12].copy_from_slice(&self.root_dispersion.0.to_be_bytes());
284 buf[12..16].copy_from_slice(&self.reference_id);
285 buf[16..24].copy_from_slice(&self.reference_ts.0.to_be_bytes());
286 buf[24..32].copy_from_slice(&self.origin_ts.0.to_be_bytes());
288 buf[32..40].copy_from_slice(&self.receive_ts.0.to_be_bytes());
289 buf[40..48].copy_from_slice(&self.transmit_ts.0.to_be_bytes());
290 }
291
292 pub fn to_bytes(&self) -> [u8; HEADER_LEN] {
293 let mut buf = [0u8; HEADER_LEN];
294 self.write(&mut buf);
295 buf
296 }
297}
298
299#[derive(Clone, Copy, Debug, PartialEq, Eq)]
301pub struct ExtensionField<'a> {
302 pub field_type: u16,
303 pub value: &'a [u8],
304}
305
306#[derive(Clone, Copy, Debug, PartialEq, Eq)]
308pub enum Trailer<'a> {
309 Extension(ExtensionField<'a>),
311 Opaque(&'a [u8]),
314}
315
316pub fn extension_fields(packet: &[u8]) -> ExtensionIter<'_> {
320 let rest = packet.get(HEADER_LEN..).unwrap_or(&[]);
321 ExtensionIter { rest }
322}
323
324pub struct ExtensionIter<'a> {
325 rest: &'a [u8],
326}
327
328impl<'a> Iterator for ExtensionIter<'a> {
329 type Item = Trailer<'a>;
330
331 fn next(&mut self) -> Option<Trailer<'a>> {
332 if self.rest.is_empty() {
333 return None;
334 }
335 if self.rest.len() >= 4 {
336 let field_type = u16::from_be_bytes([self.rest[0], self.rest[1]]);
337 let len = u16::from_be_bytes([self.rest[2], self.rest[3]]) as usize;
338 if len >= 16 && len.is_multiple_of(4) && len <= self.rest.len() {
341 let value = &self.rest[4..len];
342 self.rest = &self.rest[len..];
343 return Some(Trailer::Extension(ExtensionField { field_type, value }));
344 }
345 }
346 let opaque = self.rest;
347 self.rest = &[];
348 Some(Trailer::Opaque(opaque))
349 }
350}
351
352pub fn offset_delay(t1: f64, t2: f64, t3: f64, t4: f64) -> (f64, f64) {
356 let offset = ((t2 - t1) + (t3 - t4)) / 2.0;
357 let delay = (t4 - t1) - (t3 - t2);
358 (offset, delay)
359}
360
361#[cfg(test)]
362mod precision_tests {
363 use super::*;
364
365 const TICK: f64 = 1.0 / 4_294_967_296.0;
383
384 #[test]
385 fn differences_keep_sub_nanosecond_resolution() {
386 let secs = 1_787_856_000i64;
388 let a = NtpTimestamp::from_unix(secs, 0);
389 let b = NtpTimestamp::from_unix(secs, 1);
390
391 let exact = b.seconds_since(a);
393 assert!(
394 exact > 0.0,
395 "a 1 ns step vanished entirely in the fixed-point difference"
396 );
397 assert!(
398 (exact - 1e-9).abs() <= TICK,
399 "fixed-point difference gave {exact} s for a 1 ns step"
400 );
401
402 let ulp = (secs as f64).next_up() - secs as f64;
404 assert!(
405 ulp > 200e-9,
406 "this test assumes an f64 at the Unix epoch is coarse; ULP is {ulp} s"
407 );
408 }
409
410 #[test]
412 fn the_2038_exponent_step_does_not_reach_the_difference() {
413 let secs = 2_147_500_000i64;
415 let a = NtpTimestamp::from_unix(secs, 0);
416 let b = NtpTimestamp::from_unix(secs, 100);
417 let exact = b.seconds_since(a);
418 assert!(
419 (exact - 100e-9).abs() <= TICK,
420 "a 100 ns step past 2038 measured as {exact} s"
421 );
422
423 let ulp = (secs as f64).next_up() - secs as f64;
425 assert!(
426 ulp > 400e-9,
427 "expected the post-2038 f64 gap to exceed 400 ns, got {ulp} s"
428 );
429 }
430
431 #[test]
435 fn a_difference_spans_the_era_boundary() {
436 let before = NtpTimestamp::from_unix(2_085_978_495, 0);
438 let after = NtpTimestamp::from_unix(2_085_978_497, 0);
439 let delta = after.seconds_since(before);
440 assert!(
441 (delta - 2.0).abs() <= TICK,
442 "two seconds across the era wrap measured as {delta}"
443 );
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450
451 #[test]
452 fn roundtrip_header() {
453 let p = NtpPacket {
454 leap: LeapIndicator::NoWarning,
455 version: 4,
456 mode: Mode::Server,
457 stratum: 2,
458 poll: 6,
459 precision: -20,
460 root_delay: NtpShort::from_seconds(0.015),
461 root_dispersion: NtpShort::from_seconds(0.002),
462 reference_id: *b"GPS\0",
463 reference_ts: NtpTimestamp::from_unix(1_756_200_000, 0),
464 origin_ts: NtpTimestamp(0x0102_0304_0506_0708),
465 receive_ts: NtpTimestamp::from_unix(1_756_200_100, 500_000_000),
466 transmit_ts: NtpTimestamp::from_unix(1_756_200_100, 500_100_000),
467 };
468 let bytes = p.to_bytes();
469 let q = NtpPacket::parse(&bytes).expect("parse back");
470 assert_eq!(p, q);
471 }
472
473 #[test]
474 fn field_offsets_match_rfc5905() {
475 let mut p = NtpPacket::client_request(4, NtpTimestamp(0xAABB_CCDD_EEFF_0011));
477 p.origin_ts = NtpTimestamp(0x1111_1111_1111_1111);
478 p.receive_ts = NtpTimestamp(0x2222_2222_2222_2222);
479 p.reference_ts = NtpTimestamp(0x3333_3333_3333_3333);
480 let b = p.to_bytes();
481 assert_eq!(b[0], 0b00_100_011); assert_eq!(&b[16..24], &[0x33; 8]); assert_eq!(&b[24..32], &[0x11; 8]); assert_eq!(&b[32..40], &[0x22; 8]); assert_eq!(&b[40..48], &0xAABB_CCDD_EEFF_0011u64.to_be_bytes()); }
487
488 #[test]
489 fn short_and_bad_version_are_errors() {
490 assert_eq!(
491 NtpPacket::parse(&[0u8; 20]),
492 Err(ParseError::TooShort { len: 20 })
493 );
494 let mut b = [0u8; 48];
495 b[0] = 2 << 3; assert_eq!(
497 NtpPacket::parse(&b),
498 Err(ParseError::BadVersion { version: 2 })
499 );
500 }
501
502 #[test]
503 fn timestamp_wraparound_diff() {
504 let before = NtpTimestamp(u64::MAX - (1u64 << 31)); let after = NtpTimestamp(1u64 << 31); let d = after.seconds_since(before);
508 assert!((d - 1.0).abs() < 1e-9, "got {d}");
509 }
510
511 #[test]
512 fn exchange_math() {
513 let t1 = 10.000; let t2 = 10.125; let t3 = 10.126;
517 let t4 = 10.051; let (offset, delay) = offset_delay(t1, t2, t3, t4);
519 assert!((offset - 0.100).abs() < 1e-9, "offset {offset}");
520 assert!((delay - 0.050).abs() < 1e-9, "delay {delay}");
521 }
522
523 #[test]
529 fn extension_iteration_handles_garbage() {
530 let mut buf = [0u8; HEADER_LEN + 16 + 3];
531 buf[0] = (4 << 3) | 3;
532 buf[48..50].copy_from_slice(&0x0104u16.to_be_bytes()); buf[50..52].copy_from_slice(&16u16.to_be_bytes()); buf[52..64].copy_from_slice(&[0xAB; 12]); buf[64..67].copy_from_slice(&[1, 2, 3]); let mut it = extension_fields(&buf);
538 match it.next() {
539 Some(Trailer::Extension(ef)) => {
540 assert_eq!(ef.field_type, 0x0104);
541 assert_eq!(ef.value, &[0xAB; 12][..]);
542 }
543 other => panic!("expected an extension field, got {other:?}"),
544 }
545 assert!(matches!(it.next(), Some(Trailer::Opaque(&[1, 2, 3]))));
546 assert!(it.next().is_none(), "iteration ends after the opaque tail");
547 }
548
549 #[test]
557 fn client_request_round_trips_in_48_bytes() {
558 let nonce = NtpTimestamp(0x1234_5678_9ABC_DEF0);
559 let req = NtpPacket::client_request(4, nonce);
560
561 let wire = req.to_bytes();
562 assert_eq!(wire.len(), HEADER_LEN, "an SNTP request is 48 bytes");
563
564 let back = NtpPacket::parse(&wire).expect("our own request must parse");
565 assert_eq!(req, back, "round trip changed the packet");
566
567 assert_eq!(back.mode, Mode::Client);
571 assert_eq!(back.version, 4);
572 assert_eq!(back.transmit_ts, nonce);
573 assert!(back.origin_ts.is_zero() && back.receive_ts.is_zero());
574 }
575}