Skip to main content

ados_clock/
chrony.rs

1//! GNSS PPS samples for chrony's `SOCK` refclock driver.
2//!
3//! chrony reads `struct sock_sample` datagrams on the socket it binds for a
4//! `refclock SOCK <path>` line (for example
5//! `refclock SOCK /run/chrony.sock refid PPS`). Each datagram says: at
6//! system time `tv` the true time was `tv + offset`. chrony's definition
7//! (`refclock_sock.c`):
8//!
9//! ```c
10//! struct sock_sample {
11//!     struct timeval tv;   /* time_t tv_sec; suseconds_t tv_usec */
12//!     double offset;       /* seconds */
13//!     int pulse;
14//!     int leap;
15//!     int _pad;
16//!     int magic;           /* 0x534f434b */
17//! };
18//! ```
19//!
20//! chrony drops a datagram whose length is not `sizeof(struct sock_sample)`
21//! as *its* build sees it, so the layout to emit is the one chrony was
22//! compiled with, not this binary's libc. Every 64-bit Linux and every 32-bit
23//! distro built with 64-bit `time_t` (glibc `_TIME_BITS=64`: Debian 13 and
24//! Ubuntu 24.04 armhf after the t64 transition, where `struct timeval` becomes
25//! `__time64_t` + `__suseconds64_t`) gives the same 40 bytes, native endian:
26//!
27//! | offset | type | field     |
28//! |--------|------|-----------|
29//! | 0      | i64  | tv_sec    |
30//! | 8      | i64  | tv_usec   |
31//! | 16     | f64  | offset, s |
32//! | 24     | i32  | pulse     |
33//! | 28     | i32  | leap      |
34//! | 32     | i32  | _pad      |
35//! | 36     | i32  | magic     |
36//!
37//! That layout is written explicitly, whatever the target's own `time_t`. A
38//! chrony built with 32-bit `time_t` (pre-t64 armhf, e.g. Debian 12) expects
39//! 32 bytes and rejects these samples; pair this sink with a chrony built
40//! for 64-bit `time_t`.
41
42use std::os::unix::net::UnixDatagram;
43use std::path::{Path, PathBuf};
44
45/// `SOCK_MAGIC` from chrony's `refclock_sock.c`.
46pub const SOCK_MAGIC: i32 = 0x534f_434b;
47/// `sizeof(struct sock_sample)` with a 64-bit `struct timeval`.
48pub 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;
55/// Byte offset of `magic`.
56pub const MAGIC_AT: usize = 36;
57
58/// Encode one `struct sock_sample` (padding zero).
59pub 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
76/// The sample for one pulse: at host monotonic `host_ns` the true UTC was
77/// `utc_ns`. `realtime_minus_mono_ns` (CLOCK_REALTIME − CLOCK_MONOTONIC, read
78/// now) maps the pulse onto the system clock; the offset is measured from the
79/// microsecond-truncated `tv` actually sent.
80pub 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/// Sends samples to chrony's socket. The socket is non-blocking, so a send
91/// never stalls the caller: an absent or full receiver drops the sample.
92#[derive(Debug)]
93pub struct ChronySink {
94    path: PathBuf,
95    sock: Option<UnixDatagram>,
96    sent: u64,
97}
98
99impl ChronySink {
100    /// A sink for the chrony socket at `path`. Nothing is opened until the
101    /// first [`send`](Self::send).
102    pub fn new(path: impl Into<PathBuf>) -> Self {
103        ChronySink {
104            path: path.into(),
105            sock: None,
106            sent: 0,
107        }
108    }
109
110    /// The chrony socket path samples are sent to.
111    pub fn path(&self) -> &Path {
112        &self.path
113    }
114
115    /// Samples delivered so far.
116    pub fn sent(&self) -> u64 {
117        self.sent
118    }
119
120    /// Send one sample. Returns false when chrony's socket is absent (chrony
121    /// not running, refclock not configured) or the send would block.
122    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    /// The exact bytes chrony's 64-bit-`time_t` build reads, identical on
168    /// every target (no dependence on this target's libc `timeval`).
169    #[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        // The C layout chrony builds on a 64-bit-time_t system, mirrored.
184        #[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        // Host mono 100.000_000_500 s, realtime runs 1.76e9 s ahead, and the
213        // system clock is 2 ms slow against the true UTC.
214        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        // 2 ms plus the 500 ns the microsecond truncation dropped.
220        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}