//! 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); }