init
This commit is contained in:
103
src/ntp.zig
Normal file
103
src/ntp.zig
Normal 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 32–39)
|
||||
// T3 = server transmit timestamp (bytes 40–47)
|
||||
const t2 = ntpToUs(&resp, 32);
|
||||
const t3 = ntpToUs(&resp, 40);
|
||||
|
||||
return @divTrunc((t2 - t1) + (t3 - t4), 2);
|
||||
}
|
||||
Reference in New Issue
Block a user