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)]
71pub enum LeapIndicator {
72 NoWarning,
73 LastMinute61,
74 LastMinute59,
75 Unsynchronized,
76}
77
78impl LeapIndicator {
79 fn from_bits(b: u8) -> Self {
80 match b & 0b11 {
81 0 => LeapIndicator::NoWarning,
82 1 => LeapIndicator::LastMinute61,
83 2 => LeapIndicator::LastMinute59,
84 _ => LeapIndicator::Unsynchronized,
85 }
86 }
87
88 fn bits(self) -> u8 {
89 match self {
90 LeapIndicator::NoWarning => 0,
91 LeapIndicator::LastMinute61 => 1,
92 LeapIndicator::LastMinute59 => 2,
93 LeapIndicator::Unsynchronized => 3,
94 }
95 }
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub enum Mode {
100 Reserved,
101 SymmetricActive,
102 SymmetricPassive,
103 Client,
104 Server,
105 Broadcast,
106 Control,
107 Private,
108}
109
110impl Mode {
111 fn from_bits(b: u8) -> Self {
112 match b & 0b111 {
113 1 => Mode::SymmetricActive,
114 2 => Mode::SymmetricPassive,
115 3 => Mode::Client,
116 4 => Mode::Server,
117 5 => Mode::Broadcast,
118 6 => Mode::Control,
119 7 => Mode::Private,
120 _ => Mode::Reserved,
121 }
122 }
123
124 fn bits(self) -> u8 {
125 match self {
126 Mode::Reserved => 0,
127 Mode::SymmetricActive => 1,
128 Mode::SymmetricPassive => 2,
129 Mode::Client => 3,
130 Mode::Server => 4,
131 Mode::Broadcast => 5,
132 Mode::Control => 6,
133 Mode::Private => 7,
134 }
135 }
136}
137
138#[derive(Clone, Copy, Debug, PartialEq, Eq)]
139pub enum ParseError {
140 TooShort { len: usize },
142 BadVersion { version: u8 },
144}
145
146impl fmt::Display for ParseError {
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 match self {
149 ParseError::TooShort { len } => {
150 write!(f, "packet is {len} bytes; NTP header needs {HEADER_LEN}")
151 }
152 ParseError::BadVersion { version } => {
153 write!(f, "unsupported NTP version {version} (expected 3 or 4)")
154 }
155 }
156 }
157}
158
159impl std::error::Error for ParseError {}
160
161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
163pub struct NtpPacket {
164 pub leap: LeapIndicator,
165 pub version: u8,
166 pub mode: Mode,
167 pub stratum: u8,
168 pub poll: i8,
169 pub precision: i8,
170 pub root_delay: NtpShort,
171 pub root_dispersion: NtpShort,
172 pub reference_id: [u8; 4],
173 pub reference_ts: NtpTimestamp,
174 pub origin_ts: NtpTimestamp,
175 pub receive_ts: NtpTimestamp,
176 pub transmit_ts: NtpTimestamp,
177}
178
179fn read_u32(buf: &[u8], at: usize) -> u32 {
180 let mut b = [0u8; 4];
181 if let Some(s) = buf.get(at..at + 4) {
182 b.copy_from_slice(s);
183 }
184 u32::from_be_bytes(b)
185}
186
187fn read_u64(buf: &[u8], at: usize) -> u64 {
188 let mut b = [0u8; 8];
189 if let Some(s) = buf.get(at..at + 8) {
190 b.copy_from_slice(s);
191 }
192 u64::from_be_bytes(b)
193}
194
195impl NtpPacket {
196 pub fn client_request(version: u8, transmit_ts: NtpTimestamp) -> Self {
200 NtpPacket {
201 leap: LeapIndicator::NoWarning,
202 version,
203 mode: Mode::Client,
204 stratum: 0,
205 poll: 0,
206 precision: 0x20u8 as i8,
207 root_delay: NtpShort(0),
208 root_dispersion: NtpShort(0),
209 reference_id: [0; 4],
210 reference_ts: NtpTimestamp::ZERO,
211 origin_ts: NtpTimestamp::ZERO,
212 receive_ts: NtpTimestamp::ZERO,
213 transmit_ts,
214 }
215 }
216
217 pub fn parse(buf: &[u8]) -> Result<NtpPacket, ParseError> {
220 if buf.len() < HEADER_LEN {
221 return Err(ParseError::TooShort { len: buf.len() });
222 }
223 let b0 = buf[0];
224 let version = (b0 >> 3) & 0b111;
225 if !(3..=4).contains(&version) {
226 return Err(ParseError::BadVersion { version });
227 }
228 let mut reference_id = [0u8; 4];
229 reference_id.copy_from_slice(&buf[12..16]);
230 Ok(NtpPacket {
231 leap: LeapIndicator::from_bits(b0 >> 6),
232 version,
233 mode: Mode::from_bits(b0),
234 stratum: buf[1],
235 poll: buf[2] as i8,
236 precision: buf[3] as i8,
237 root_delay: NtpShort(read_u32(buf, 4)),
238 root_dispersion: NtpShort(read_u32(buf, 8)),
239 reference_id,
240 reference_ts: NtpTimestamp(read_u64(buf, 16)),
241 origin_ts: NtpTimestamp(read_u64(buf, 24)),
242 receive_ts: NtpTimestamp(read_u64(buf, 32)),
243 transmit_ts: NtpTimestamp(read_u64(buf, 40)),
244 })
245 }
246
247 pub fn write(&self, buf: &mut [u8; HEADER_LEN]) {
249 buf[0] = (self.leap.bits() << 6) | ((self.version & 0b111) << 3) | self.mode.bits();
250 buf[1] = self.stratum;
251 buf[2] = self.poll as u8;
252 buf[3] = self.precision as u8;
253 buf[4..8].copy_from_slice(&self.root_delay.0.to_be_bytes());
254 buf[8..12].copy_from_slice(&self.root_dispersion.0.to_be_bytes());
255 buf[12..16].copy_from_slice(&self.reference_id);
256 buf[16..24].copy_from_slice(&self.reference_ts.0.to_be_bytes());
257 buf[24..32].copy_from_slice(&self.origin_ts.0.to_be_bytes());
259 buf[32..40].copy_from_slice(&self.receive_ts.0.to_be_bytes());
260 buf[40..48].copy_from_slice(&self.transmit_ts.0.to_be_bytes());
261 }
262
263 pub fn to_bytes(&self) -> [u8; HEADER_LEN] {
264 let mut buf = [0u8; HEADER_LEN];
265 self.write(&mut buf);
266 buf
267 }
268}
269
270#[derive(Clone, Copy, Debug, PartialEq, Eq)]
272pub struct ExtensionField<'a> {
273 pub field_type: u16,
274 pub value: &'a [u8],
275}
276
277#[derive(Clone, Copy, Debug, PartialEq, Eq)]
279pub enum Trailer<'a> {
280 Extension(ExtensionField<'a>),
282 Opaque(&'a [u8]),
285}
286
287pub fn extension_fields(packet: &[u8]) -> ExtensionIter<'_> {
291 let rest = packet.get(HEADER_LEN..).unwrap_or(&[]);
292 ExtensionIter { rest }
293}
294
295pub struct ExtensionIter<'a> {
296 rest: &'a [u8],
297}
298
299impl<'a> Iterator for ExtensionIter<'a> {
300 type Item = Trailer<'a>;
301
302 fn next(&mut self) -> Option<Trailer<'a>> {
303 if self.rest.is_empty() {
304 return None;
305 }
306 if self.rest.len() >= 4 {
307 let field_type = u16::from_be_bytes([self.rest[0], self.rest[1]]);
308 let len = u16::from_be_bytes([self.rest[2], self.rest[3]]) as usize;
309 if len >= 16 && len.is_multiple_of(4) && len <= self.rest.len() {
312 let value = &self.rest[4..len];
313 self.rest = &self.rest[len..];
314 return Some(Trailer::Extension(ExtensionField { field_type, value }));
315 }
316 }
317 let opaque = self.rest;
318 self.rest = &[];
319 Some(Trailer::Opaque(opaque))
320 }
321}
322
323pub fn offset_delay(t1: f64, t2: f64, t3: f64, t4: f64) -> (f64, f64) {
327 let offset = ((t2 - t1) + (t3 - t4)) / 2.0;
328 let delay = (t4 - t1) - (t3 - t2);
329 (offset, delay)
330}
331
332#[cfg(test)]
333mod precision_tests {
334 use super::*;
335
336 const TICK: f64 = 1.0 / 4_294_967_296.0;
354
355 #[test]
356 fn differences_keep_sub_nanosecond_resolution() {
357 let secs = 1_787_856_000i64;
359 let a = NtpTimestamp::from_unix(secs, 0);
360 let b = NtpTimestamp::from_unix(secs, 1);
361
362 let exact = b.seconds_since(a);
364 assert!(
365 exact > 0.0,
366 "a 1 ns step vanished entirely in the fixed-point difference"
367 );
368 assert!(
369 (exact - 1e-9).abs() <= TICK,
370 "fixed-point difference gave {exact} s for a 1 ns step"
371 );
372
373 let ulp = (secs as f64).next_up() - secs as f64;
375 assert!(
376 ulp > 200e-9,
377 "this test assumes an f64 at the Unix epoch is coarse; ULP is {ulp} s"
378 );
379 }
380
381 #[test]
383 fn the_2038_exponent_step_does_not_reach_the_difference() {
384 let secs = 2_147_500_000i64;
386 let a = NtpTimestamp::from_unix(secs, 0);
387 let b = NtpTimestamp::from_unix(secs, 100);
388 let exact = b.seconds_since(a);
389 assert!(
390 (exact - 100e-9).abs() <= TICK,
391 "a 100 ns step past 2038 measured as {exact} s"
392 );
393
394 let ulp = (secs as f64).next_up() - secs as f64;
396 assert!(
397 ulp > 400e-9,
398 "expected the post-2038 f64 gap to exceed 400 ns, got {ulp} s"
399 );
400 }
401
402 #[test]
406 fn a_difference_spans_the_era_boundary() {
407 let before = NtpTimestamp::from_unix(2_085_978_495, 0);
409 let after = NtpTimestamp::from_unix(2_085_978_497, 0);
410 let delta = after.seconds_since(before);
411 assert!(
412 (delta - 2.0).abs() <= TICK,
413 "two seconds across the era wrap measured as {delta}"
414 );
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[test]
423 fn roundtrip_header() {
424 let p = NtpPacket {
425 leap: LeapIndicator::NoWarning,
426 version: 4,
427 mode: Mode::Server,
428 stratum: 2,
429 poll: 6,
430 precision: -20,
431 root_delay: NtpShort::from_seconds(0.015),
432 root_dispersion: NtpShort::from_seconds(0.002),
433 reference_id: *b"GPS\0",
434 reference_ts: NtpTimestamp::from_unix(1_756_200_000, 0),
435 origin_ts: NtpTimestamp(0x0102_0304_0506_0708),
436 receive_ts: NtpTimestamp::from_unix(1_756_200_100, 500_000_000),
437 transmit_ts: NtpTimestamp::from_unix(1_756_200_100, 500_100_000),
438 };
439 let bytes = p.to_bytes();
440 let q = NtpPacket::parse(&bytes).expect("parse back");
441 assert_eq!(p, q);
442 }
443
444 #[test]
445 fn field_offsets_match_rfc5905() {
446 let mut p = NtpPacket::client_request(4, NtpTimestamp(0xAABB_CCDD_EEFF_0011));
448 p.origin_ts = NtpTimestamp(0x1111_1111_1111_1111);
449 p.receive_ts = NtpTimestamp(0x2222_2222_2222_2222);
450 p.reference_ts = NtpTimestamp(0x3333_3333_3333_3333);
451 let b = p.to_bytes();
452 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()); }
458
459 #[test]
460 fn short_and_bad_version_are_errors() {
461 assert_eq!(
462 NtpPacket::parse(&[0u8; 20]),
463 Err(ParseError::TooShort { len: 20 })
464 );
465 let mut b = [0u8; 48];
466 b[0] = 2 << 3; assert_eq!(
468 NtpPacket::parse(&b),
469 Err(ParseError::BadVersion { version: 2 })
470 );
471 }
472
473 #[test]
474 fn timestamp_wraparound_diff() {
475 let before = NtpTimestamp(u64::MAX - (1u64 << 31)); let after = NtpTimestamp(1u64 << 31); let d = after.seconds_since(before);
479 assert!((d - 1.0).abs() < 1e-9, "got {d}");
480 }
481
482 #[test]
483 fn exchange_math() {
484 let t1 = 10.000; let t2 = 10.125; let t3 = 10.126;
488 let t4 = 10.051; let (offset, delay) = offset_delay(t1, t2, t3, t4);
490 assert!((offset - 0.100).abs() < 1e-9, "offset {offset}");
491 assert!((delay - 0.050).abs() < 1e-9, "delay {delay}");
492 }
493
494 #[test]
495 fn extension_iteration_handles_garbage() {
496 let mut buf = vec![0u8; 48];
498 buf[0] = (4 << 3) | 3;
499 buf.extend_from_slice(&0x0104u16.to_be_bytes()); buf.extend_from_slice(&16u16.to_be_bytes()); buf.extend_from_slice(&[0xAB; 12]); buf.extend_from_slice(&[1, 2, 3]); let items: Vec<_> = extension_fields(&buf).collect();
504 assert_eq!(items.len(), 2);
505 match items[0] {
506 Trailer::Extension(ef) => {
507 assert_eq!(ef.field_type, 0x0104);
508 assert_eq!(ef.value, &[0xAB; 12][..]);
509 }
510 _ => panic!("expected extension"),
511 }
512 assert!(matches!(items[1], Trailer::Opaque(&[1, 2, 3])));
513 }
514}