src.nth.io/

summaryrefslogtreecommitdiff
path: root/src/host.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/host.rs')
-rw-r--r--src/host.rs88
1 files changed, 88 insertions, 0 deletions
diff --git a/src/host.rs b/src/host.rs
new file mode 100644
index 0000000..feb5a34
--- /dev/null
+++ b/src/host.rs
@@ -0,0 +1,88 @@
+//! Host clock trust: the device must never be set from a clock we do not
+//! trust.
+//!
+//! The primary check measures the actual clock error with a single SNTP
+//! query (RFC 4330) and accepts the host when the offset is under a second.
+//! That is a direct measurement, platform-independent, and stronger than
+//! asking the OS whether it believes it is synchronized. When no NTP server
+//! is reachable, systemd-timesyncd's verdict (`timedatectl`, Linux) is the
+//! fallback; hosts with neither fail safe.
+
+use std::net::UdpSocket;
+use std::process::Command;
+use std::time::{Duration, SystemTime, UNIX_EPOCH};
+
+const NTP_SERVERS: [&str; 2] = ["time.apple.com:123", "pool.ntp.org:123"];
+/// Accept the host clock when it is within this many seconds of NTP time.
+/// Well inside the 5s sync read-back tolerance, far above network jitter.
+const MAX_OFFSET_SECONDS: f64 = 1.0;
+/// Seconds between the NTP era (1900) and the Unix epoch (1970).
+const NTP_UNIX_OFFSET: f64 = 2_208_988_800.0;
+/// The NTP fraction field is 32-bit fixed-point in units of 1/2^32 seconds;
+/// dividing by 2^32 converts it to seconds.
+const NTP_FRACTION_SCALE: f64 = (1u64 << 32) as f64;
+
+pub fn clock_is_ntp_synchronized() -> bool {
+ for server in NTP_SERVERS {
+ if let Some(offset) = sntp_offset(server) {
+ let ok = offset.abs() <= MAX_OFFSET_SECONDS;
+ if ok {
+ log::debug!("Host clock is {offset:+.3}s from {server}; trusted");
+ } else {
+ log::warn!("Host clock is {offset:+.3}s from {server}; not trusted");
+ }
+ return ok;
+ }
+ }
+ log::debug!("No NTP server reachable; falling back to timedatectl");
+ timedatectl_says_synchronized()
+}
+
+/// One SNTP client exchange: returns the approximate offset of the local
+/// clock relative to the server (positive = local clock ahead). Uses the
+/// request/response midpoint, so the error is bounded by half the round
+/// trip, which is milliseconds against a threshold of a second.
+fn sntp_offset(server: &str) -> Option<f64> {
+ let socket = UdpSocket::bind(("0.0.0.0", 0)).ok()?;
+ socket.set_read_timeout(Some(Duration::from_secs(2))).ok()?;
+ socket.connect(server).ok()?;
+
+ let mut request = [0u8; 48];
+ request[0] = 0b00_100_011; // LI 0, version 4, mode 3 (client)
+ let sent_at = unix_now();
+ socket.send(&request).ok()?;
+
+ let mut response = [0u8; 48];
+ let len = socket.recv(&mut response).ok()?;
+ let received_at = unix_now();
+ if len < 48 || response[0] & 0x07 != 4 {
+ // Not a server-mode reply.
+ return None;
+ }
+
+ // Transmit timestamp: seconds since 1900 plus a 32-bit binary fraction.
+ let seconds = u32::from_be_bytes(response[40..44].try_into().ok()?) as f64;
+ let fraction =
+ u32::from_be_bytes(response[44..48].try_into().ok()?) as f64 / NTP_FRACTION_SCALE;
+ let server_time = seconds + fraction - NTP_UNIX_OFFSET;
+ if server_time <= 0.0 {
+ return None;
+ }
+ Some((sent_at + received_at) / 2.0 - server_time)
+}
+
+fn unix_now() -> f64 {
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map(|d| d.as_secs_f64())
+ .unwrap_or(0.0)
+}
+
+/// systemd-timesyncd's opinion; false on hosts without timedatectl.
+fn timedatectl_says_synchronized() -> bool {
+ Command::new("timedatectl")
+ .args(["show", "-p", "NTPSynchronized", "--value"])
+ .output()
+ .map(|output| output.status.success() && output.stdout.trim_ascii() == b"yes")
+ .unwrap_or(false)
+}