1use std::net::{SocketAddr, UdpSocket};
6use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
7
8use super::common::{map_io_err, system_time_to_us};
9use super::socket_opts::set_windows_ttl_udp;
10use crate::time_src::{OffsetMicros, TimeSource, TimeSourceError};
11
12pub struct NtpSource;
13
14const NTP_TO_UNIX: u64 = 2_208_988_800;
16
17impl TimeSource for NtpSource {
18 fn name(&self) -> &'static str {
19 "ntp"
20 }
21
22 fn fetch(
23 &self,
24 target: SocketAddr,
25 timeout: Duration,
26 ) -> Result<OffsetMicros, TimeSourceError> {
27 let ntp_addr: SocketAddr = (target.ip(), 123).into();
28 fetch_ntp(ntp_addr, timeout)
29 }
30}
31
32fn fetch_ntp(addr: SocketAddr, timeout: Duration) -> Result<OffsetMicros, TimeSourceError> {
33 let socket = UdpSocket::bind(if addr.is_ipv4() {
34 "0.0.0.0:0"
35 } else {
36 "[::]:0"
37 })
38 .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
39 socket
40 .set_read_timeout(Some(timeout))
41 .map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
42
43 let mut req = [0u8; 48];
45 req[0] = 0b00_100_011; let t1_sys = SystemTime::now();
49 let t1_ntp = system_time_to_ntp(t1_sys);
50 req[40..44].copy_from_slice(&t1_ntp.0.to_be_bytes());
51 req[44..48].copy_from_slice(&t1_ntp.1.to_be_bytes());
52
53 socket.connect(addr).map_err(|e| map_io_err(e, "connect"))?;
54 set_windows_ttl_udp(&socket).map_err(|e| TimeSourceError::Protocol(e.to_string()))?;
55
56 let t_send = Instant::now();
57 socket.send(&req).map_err(|e| map_io_err(e, "send"))?;
58
59 let mut buf = [0u8; 48];
60 let n = socket.recv(&mut buf).map_err(|e| map_io_err(e, "recv"))?;
61 let rtt = t_send.elapsed();
62
63 if n < 48 {
64 return Err(TimeSourceError::Parse(format!(
65 "short NTP response: {} bytes",
66 n
67 )));
68 }
69
70 let mode = buf[0] & 0x07;
71 if mode != 4 && mode != 5 {
72 return Err(TimeSourceError::Protocol(format!(
73 "unexpected NTP mode: {}",
74 mode
75 )));
76 }
77
78 let t2 = parse_ntp_timestamp(&buf[32..40])?;
80 let t3 = parse_ntp_timestamp(&buf[40..48])?;
82
83 let t4_us = system_time_to_us(t1_sys)? + rtt.as_micros() as i64;
85
86 let t1_us = system_time_to_us(t1_sys)?;
88 let offset_us = ((t2 - t1_us) + (t3 - t4_us)) / 2;
89
90 Ok(offset_us)
91}
92
93fn parse_ntp_timestamp(b: &[u8]) -> Result<i64, TimeSourceError> {
98 if b.len() < 8 {
99 return Err(TimeSourceError::Parse("NTP timestamp too short".into()));
100 }
101 let secs = u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as u64;
102 let frac = u32::from_be_bytes([b[4], b[5], b[6], b[7]]);
103
104 if secs < NTP_TO_UNIX {
105 return Err(TimeSourceError::Parse(format!(
106 "NTP seconds {} predates Unix epoch",
107 secs
108 )));
109 }
110 let unix_secs = secs - NTP_TO_UNIX;
111 let frac_us = (frac as u64 * 1_000_000) >> 32;
113 i64::try_from(unix_secs * 1_000_000 + frac_us)
114 .map_err(|_| TimeSourceError::Parse("NTP timestamp overflows i64 (post-2262)".into()))
115}
116
117fn system_time_to_ntp(t: SystemTime) -> (u32, u32) {
119 let dur = t.duration_since(UNIX_EPOCH).unwrap_or(Duration::ZERO);
120 let ntp_secs = (dur.as_secs() + NTP_TO_UNIX) as u32;
121 let frac = ((dur.subsec_nanos() as u64) << 32) / 1_000_000_000;
123 (ntp_secs, frac as u32)
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn parse_known_ntp_timestamp() {
132 let secs: u32 = 3_913_056_000;
135 let frac: u32 = 0;
136 let mut b = [0u8; 8];
137 b[0..4].copy_from_slice(&secs.to_be_bytes());
138 b[4..8].copy_from_slice(&frac.to_be_bytes());
139 let us = parse_ntp_timestamp(&b).unwrap();
140 assert_eq!(us, 1_704_067_200 * 1_000_000);
141 }
142
143 #[test]
144 fn parse_ntp_with_fraction() {
145 let secs: u32 = NTP_TO_UNIX as u32;
147 let frac: u32 = 1 << 31;
148 let mut b = [0u8; 8];
149 b[0..4].copy_from_slice(&secs.to_be_bytes());
150 b[4..8].copy_from_slice(&frac.to_be_bytes());
151 let us = parse_ntp_timestamp(&b).unwrap();
152 assert_eq!(us, 500_000);
153 }
154
155 #[test]
156 fn roundtrip_ntp_conversion() {
157 let now = SystemTime::now();
158 let (secs, frac) = system_time_to_ntp(now);
159 let mut b = [0u8; 8];
160 b[0..4].copy_from_slice(&secs.to_be_bytes());
161 b[4..8].copy_from_slice(&frac.to_be_bytes());
162 let us = parse_ntp_timestamp(&b).unwrap();
163 let expected = system_time_to_us(now).unwrap();
164 assert!(
166 (us - expected).abs() < 1000,
167 "roundtrip error: {}us",
168 us - expected
169 );
170 }
171
172 use proptest::prelude::*;
173
174 proptest! {
175 #[test]
176 fn parse_ntp_timestamp_never_panics(data in proptest::collection::vec(any::<u8>(), 0..16)) {
177 let _ = parse_ntp_timestamp(&data);
178 }
179 }
180}