Files
qc2-reader/src/ntp.zig
spotdemo4 99dff7586e
Some checks failed
check / check (push) Failing after 22s
bump / bump (push) Failing after 27s
vulnerable / flake (push) Failing after 32s
vulnerable / actions (push) Failing after 28s
update / renovate (push) Failing after 35s
init
2026-03-26 16:34:22 -04:00

104 lines
3.4 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Minimal SNTP client (RFC 4330).
//!
//! Performs a single round-trip to an NTP server and computes the offset
//! between the local system clock and UTC so that reported timestamps can
//! be corrected before being sent to a server.
//!
//! Typical usage:
//! const offset = ntp.queryOffset(allocator, "pool.ntp.org") catch |err| blk: {
//! std.log.warn("NTP query failed ({s}), timestamps may be inaccurate", .{@errorName(err)});
//! break :blk 0;
//! };
//! // corrected_us = std.time.microTimestamp() + offset
const std = @import("std");
const ntp_port: u16 = 123;
/// NTP epoch is 1 Jan 1900; Unix epoch is 1 Jan 1970.
/// Difference = 70 years = 2 208 988 800 seconds.
const ntp_to_unix_s: i64 = 2_208_988_800;
/// Decode a 64-bit NTP timestamp starting at `buf[off]` into microseconds
/// since the Unix epoch.
fn ntpToUs(buf: []const u8, off: usize) i64 {
const secs = std.mem.readInt(u32, buf[off..][0..4], .big);
const frac = std.mem.readInt(u32, buf[off + 4 ..][0..4], .big);
const unix_s: i64 = @as(i64, secs) - ntp_to_unix_s;
// frac / 2^32 * 1_000_000 µs (no overflow: frac < 2^32, result < 1_000_000)
const us_frac: i64 = @intCast((@as(u64, frac) * 1_000_000) >> 32);
return unix_s * 1_000_000 + us_frac;
}
/// Query `server` (hostname or IP) via SNTP and return the estimated clock
/// offset in microseconds.
///
/// Return value semantics:
/// offset_us = ntp_time_us - local_time_us
///
/// To get a corrected UTC timestamp:
/// corrected_us = std.time.microTimestamp() + offset_us
///
/// Uses the standard SNTP offset formula:
/// offset = ((T2 - T1) + (T3 - T4)) / 2
/// where T1 = local send time, T2 = server receive, T3 = server transmit,
/// T4 = local receive time.
pub fn queryOffset(allocator: std.mem.Allocator, server: []const u8) !i64 {
// Resolve hostname.
const addr_list = try std.net.getAddressList(allocator, server, ntp_port);
defer addr_list.deinit();
if (addr_list.addrs.len == 0) return error.NoAddress;
const addr = addr_list.addrs[0];
// Open a UDP socket.
const sock = try std.posix.socket(
addr.any.family,
std.posix.SOCK.DGRAM,
std.posix.IPPROTO.UDP,
);
defer std.posix.close(sock);
// 2 second receive timeout.
const tv = std.posix.timeval{ .sec = 2, .usec = 0 };
try std.posix.setsockopt(
sock,
std.posix.SOL.SOCKET,
std.posix.SO.RCVTIMEO,
std.mem.asBytes(&tv),
);
// 48-byte SNTP request: only the flags byte is non-zero.
// LI = 0 (no leap second warning)
// VN = 4 (NTP version 4)
// Mode = 3 (client)
// Packed: 0b00_100_011 = 0x23
var req = std.mem.zeroes([48]u8);
req[0] = 0x23;
// T1: local clock immediately before send.
const t1: i64 = std.time.microTimestamp();
const sent = try std.posix.sendto(
sock,
&req,
0,
&addr.any,
addr.getOsSockLen(),
);
if (sent != 48) return error.SendFailed;
var resp: [48]u8 = undefined;
const n = try std.posix.recvfrom(sock, &resp, 0, null, null);
if (n < 48) return error.ShortResponse;
// T4: local clock immediately after receive.
const t4: i64 = std.time.microTimestamp();
// T2 = server receive timestamp (bytes 3239)
// T3 = server transmit timestamp (bytes 4047)
const t2 = ntpToUs(&resp, 32);
const t3 = ntpToUs(&resp, 40);
return @divTrunc((t2 - t1) + (t3 - t4), 2);
}