init
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

This commit is contained in:
2026-03-26 16:34:22 -04:00
parent 24059f9660
commit 99dff7586e
156 changed files with 8152 additions and 45 deletions

103
src/ntp.zig Normal file
View File

@@ -0,0 +1,103 @@
//! 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);
}