1use std::os::unix::net::UnixDatagram;
43use std::path::{Path, PathBuf};
44
45pub const SOCK_MAGIC: i32 = 0x534f_434b;
47pub const SAMPLE_LEN: usize = 40;
49const TV_SEC_AT: usize = 0;
50const TV_USEC_AT: usize = 8;
51const OFFSET_AT: usize = 16;
52const PULSE_AT: usize = 24;
53const LEAP_AT: usize = 28;
54const PAD_AT: usize = 32;
55pub const MAGIC_AT: usize = 36;
57
58pub fn encode_sample(
60 tv_sec: i64,
61 tv_usec: i64,
62 offset_s: f64,
63 pulse: i32,
64 leap: i32,
65) -> [u8; SAMPLE_LEN] {
66 let mut b = [0u8; SAMPLE_LEN];
67 b[TV_SEC_AT..TV_USEC_AT].copy_from_slice(&tv_sec.to_ne_bytes());
68 b[TV_USEC_AT..OFFSET_AT].copy_from_slice(&tv_usec.to_ne_bytes());
69 b[OFFSET_AT..PULSE_AT].copy_from_slice(&offset_s.to_ne_bytes());
70 b[PULSE_AT..LEAP_AT].copy_from_slice(&pulse.to_ne_bytes());
71 b[LEAP_AT..PAD_AT].copy_from_slice(&leap.to_ne_bytes());
72 b[MAGIC_AT..SAMPLE_LEN].copy_from_slice(&SOCK_MAGIC.to_ne_bytes());
73 b
74}
75
76pub fn pulse_sample(host_ns: u64, utc_ns: i64, realtime_minus_mono_ns: i64) -> [u8; SAMPLE_LEN] {
81 const NS: i64 = 1_000_000_000;
82 let tv_ns = host_ns as i64 + realtime_minus_mono_ns;
83 let sec = tv_ns.div_euclid(NS);
84 let usec = tv_ns.rem_euclid(NS) / 1_000;
85 let sent_ns = sec * NS + usec * 1_000;
86 let offset_s = (utc_ns - sent_ns) as f64 / 1e9;
87 encode_sample(sec, usec, offset_s, 0, 0)
88}
89
90#[derive(Debug)]
93pub struct ChronySink {
94 path: PathBuf,
95 sock: Option<UnixDatagram>,
96 sent: u64,
97}
98
99impl ChronySink {
100 pub fn new(path: impl Into<PathBuf>) -> Self {
103 ChronySink {
104 path: path.into(),
105 sock: None,
106 sent: 0,
107 }
108 }
109
110 pub fn path(&self) -> &Path {
112 &self.path
113 }
114
115 pub fn sent(&self) -> u64 {
117 self.sent
118 }
119
120 pub fn send(&mut self, sample: &[u8; SAMPLE_LEN]) -> bool {
123 if self.sock.is_none() {
124 match UnixDatagram::unbound().and_then(|s| s.set_nonblocking(true).map(|()| s)) {
125 Ok(s) => self.sock = Some(s),
126 Err(e) => {
127 tracing::debug!(error = %e, "chrony socket create failed");
128 return false;
129 }
130 }
131 }
132 let Some(sock) = &self.sock else {
133 return false;
134 };
135 match sock.send_to(sample, &self.path) {
136 Ok(_) => {
137 self.sent += 1;
138 true
139 }
140 Err(e) => {
141 tracing::debug!(error = %e, path = %self.path.display(), "chrony sample not sent");
142 false
143 }
144 }
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 fn i32_at(b: &[u8], at: usize) -> i32 {
153 i32::from_ne_bytes(b[at..at + 4].try_into().unwrap())
154 }
155
156 fn tv(b: &[u8]) -> (i64, i64) {
157 (
158 i64::from_ne_bytes(b[0..8].try_into().unwrap()),
159 i64::from_ne_bytes(b[8..16].try_into().unwrap()),
160 )
161 }
162
163 fn offset(b: &[u8]) -> f64 {
164 f64::from_ne_bytes(b[16..24].try_into().unwrap())
165 }
166
167 #[test]
170 fn layout_is_chronys_64_bit_timeval_on_every_target() {
171 let b = encode_sample(1_760_860_000, 123_456, -0.25, 1, 2);
172 let mut want = Vec::with_capacity(40);
173 want.extend(1_760_860_000i64.to_ne_bytes());
174 want.extend(123_456i64.to_ne_bytes());
175 want.extend((-0.25f64).to_ne_bytes());
176 want.extend(1i32.to_ne_bytes());
177 want.extend(2i32.to_ne_bytes());
178 want.extend(0i32.to_ne_bytes());
179 want.extend(0x534f_434bi32.to_ne_bytes());
180 assert_eq!(b.as_slice(), want.as_slice());
181 assert_eq!(SAMPLE_LEN, 40);
182 assert_eq!(MAGIC_AT, 36);
183 #[repr(C)]
185 struct Sample64 {
186 tv_sec: i64,
187 tv_usec: i64,
188 offset: f64,
189 pulse: i32,
190 leap: i32,
191 pad: i32,
192 magic: i32,
193 }
194 assert_eq!(std::mem::size_of::<Sample64>(), SAMPLE_LEN);
195 assert_eq!(std::mem::offset_of!(Sample64, magic), MAGIC_AT);
196 let s = Sample64 {
197 tv_sec: 0,
198 tv_usec: 0,
199 offset: 0.0,
200 pulse: 0,
201 leap: 0,
202 pad: 0,
203 magic: 0,
204 };
205 let _ = (
206 s.tv_sec, s.tv_usec, s.offset, s.pulse, s.leap, s.pad, s.magic,
207 );
208 }
209
210 #[test]
211 fn pulse_maps_onto_the_system_clock() {
212 let host = 100_000_000_500u64;
215 let delta = 1_760_000_000_000_000_000i64;
216 let utc = host as i64 + delta + 2_000_000;
217 let b = pulse_sample(host, utc, delta);
218 assert_eq!(tv(&b), (1_760_000_100, 0));
219 assert!((offset(&b) - 0.002_000_5).abs() < 1e-9, "{}", offset(&b));
221 assert_eq!(i32_at(&b, PULSE_AT), 0);
222 assert_eq!(i32_at(&b, LEAP_AT), 0);
223 }
224
225 #[test]
226 fn delivers_to_a_bound_socket_and_skips_an_absent_one() {
227 let dir = tempfile::tempdir().unwrap();
228 let path = dir.path().join("chrony.sock");
229 let mut sink = ChronySink::new(&path);
230 assert!(!sink.send(&encode_sample(1, 2, 0.0, 0, 0)));
231 let rx = UnixDatagram::bind(&path).unwrap();
232 assert!(sink.send(&encode_sample(1, 2, 0.5, 0, 0)));
233 let mut buf = [0u8; 64];
234 let n = rx.recv(&mut buf).unwrap();
235 assert_eq!(n, SAMPLE_LEN);
236 assert_eq!(i32_at(&buf, MAGIC_AT), SOCK_MAGIC);
237 assert_eq!(sink.sent(), 1);
238 }
239}