From bf0d65c741fa925c1229319c95590409226b63cb Mon Sep 17 00:00:00 2001 From: FObersteiner Date: Sun, 4 Aug 2024 20:12:31 +0200 Subject: [PATCH] update to v0.0.17 --- README.md | 37 ++++----- build.zig | 2 +- build.zig.zon | 10 +-- docs/change.log | 6 ++ src/cmd.zig | 25 +++--- src/main.zig | 181 ++++++++++++++++++++++++++------------------ src/ntp.zig | 31 ++++---- src/test_ntplib.zig | 8 +- 8 files changed, 175 insertions(+), 125 deletions(-) diff --git a/README.md b/README.md index 14461c1..46f72a0 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,14 @@ Usage: ntp_client [options] Options: -s, --server NTP server to query (default: pool.ntp.org) - -p, --port UDP port to use for NTP query (default: 123) -v, --protocol-version NTP protocol version, 3 or 4 (default: 4) - -a, --all Query all IP addresses found for a given server URL (default: false / stop after first) - --src-ip IP address to use for sending the query (default: 0.0.0.0 / auto-select) + --src-ip IP address to use for sending the query (default: 0::0 / IPv6 auto-select) --src-port UDP port to use for sending the query (default: 0 / any port) - -z, --timezone Timezone to use in results display (default: UTC) - -j, --json Print result as JSON + --dst-port UDP port of destination server (default: 123) + -z, --timezone Timezone to use in console output (default: UTC) + -j, --json Print result in JSON + -i, --interval Interval for repeated queries in seconds (default: null / one-shot operation) + -a, --all Query all IP addresses found for a given server URL (default: false / stop after first) -h, --help Show this help and exit ``` @@ -47,21 +48,23 @@ zig build run -Dexe -- -z local ```text ---***--- Server name: "pool.ntp.org" -Server address: "178.63.52.31:123" +Server address: "167.235.69.67:123" --- -LI=0 VN=4 Mode=4 Stratum=2 Poll=0 (0 s) Precision=-24 (59 ns) -ID: 0xDE03BC83 -Server root dispersion: 30914 us, root delay: 8392 us +LI=0 VN=4 Mode=4 Stratum=3 Poll=0 (0 s) Precision=-25 (29 ns) +ID: 0xE0928157 +Server root dispersion: 1388 us, root delay: 8224 us --- -Server last synced : 2024-07-09T08:54:05.515336464+02:00 -T1, packet created : 2024-07-09T09:06:56.690365786+02:00 -T2, server received : 2024-07-09T09:06:56.697508476+02:00 -T3, server replied : 2024-07-09T09:06:56.697553604+02:00 -T4, reply received : 2024-07-09T09:06:56.700540800+02:00 +Server last synced : 2024-07-17T21:24:24.462370132+02:00 +T1, packet created : 2024-07-17T21:31:16.304413052+02:00 +T2, server received : 2024-07-17T21:31:16.296820666+02:00 +T3, server replied : 2024-07-17T21:31:16.296837443+02:00 +T4, reply received : 2024-07-17T21:31:16.323274023+02:00 (timezone displayed: Europe/Berlin) --- -Offset to timserver: 0.002 s (2077 us) -Round-trip delay: 0.010 s (10129 us) +Offset to timserver: -0.017 s (-17015 us) +Round-trip delay: 0.019 s (18844 us) +--- +Result flags: 0 (OK) ---***--- ``` @@ -71,7 +74,7 @@ Developed & tested on Linux (Debian, on an x86 machine). Windows worked last tim ## Zig version -This package is developed with Zig `0.14.0-dev` (master), might not compile with older versions. As of 2024-06-15, Zig-0.12 and Zig-0.13 (both stable) should work. +This package is developed with Zig `0.14.0-dev` (master), might not compile with older versions. As of 2024-08-04, Zig-0.12 and Zig-0.13 (both stable) should work. ## Dependencies diff --git a/build.zig b/build.zig index e4978b3..e1ddeca 100644 --- a/build.zig +++ b/build.zig @@ -1,6 +1,6 @@ const std = @import("std"); const log = std.log.scoped(.ntp_client_build); -const client_version = std.SemanticVersion{ .major = 0, .minor = 0, .patch = 16 }; +const client_version = std.SemanticVersion{ .major = 0, .minor = 0, .patch = 17 }; pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); diff --git a/build.zig.zon b/build.zig.zon index da7ad1b..787f2ef 100755 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,15 +1,15 @@ .{ .name = "ntp_client", - .version = "0.0.16", + .version = "0.0.17", .dependencies = .{ .zdt = .{ - .url = "https://codeberg.org/FObersteiner/zdt/archive/v0.1.5.tar.gz", - .hash = "1220b8b17cfe26a282faf637450cd657fb2808ab7f62aadc115c2f4b96f0f8dd021b", + .url = "https://codeberg.org/FObersteiner/zdt/archive/179df6487230bbcbd2f388da8c86460df0321bb7.tar.gz", + .hash = "122068fa994abe66babb2f6b6bf708944f82586f9e56ae64a305870a3285909ca78a", .lazy = true, }, .flags = .{ - .url = "https://github.com/n0s4/flags/archive/refs/tags/v0.5.0.tar.gz", - .hash = "12202e9d5de187569b77064c66a4972e8a824488295fab2f5c8cb48331eab9877257", + .url = "https://github.com/n0s4/flags/archive/refs/tags/v0.6.0.tar.gz", + .hash = "122059ec2d5696493f89cb2931f53f9cb052795890a8aac992d8e08e0229536ffa02", .lazy = true, }, }, diff --git a/docs/change.log b/docs/change.log index ccba3fa..2cbdaeb 100644 --- a/docs/change.log +++ b/docs/change.log @@ -1,5 +1,11 @@ # Changelog +## 2024-08-04, v0.0.17 + +- refactor main +- enforce match of source and target address family. An IPv6 server should only be reachable if an IPv6 source address is used. +- use 'flags' package v0.6.0 + ## 2024-07-12, v0.0.16 - result validation / flagging diff --git a/src/cmd.zig b/src/cmd.zig index 462ba52..1b25f25 100644 --- a/src/cmd.zig +++ b/src/cmd.zig @@ -5,30 +5,35 @@ pub const name = "ntp_client"; // defaults: server: []const u8 = "pool.ntp.org", -port: u16 = 123, protocol_version: u8 = 4, -all: bool = false, -src_ip: []const u8 = "0.0.0.0", // TODO : should this be 0::0 / IPv6 by default? +ipv4: bool = false, +src_ip: []const u8 = "0::0", src_port: u16 = 0, +dst_port: u16 = 123, timezone: []const u8 = "UTC", json: bool = false, +interval: ?u64 = null, +all: bool = false, pub const descriptions = .{ .server = "NTP server to query (default: pool.ntp.org)", - .port = "UDP port to use for NTP query (default: 123)", .protocol_version = "NTP protocol version, 3 or 4 (default: 4)", - .all = "Query all IP addresses found for a given server URL (default: false / stop after first)", - .src_ip = "IP address to use for sending the query (default: 0.0.0.0 / auto-select)", + .ipv4 = "use IPv4 instead of the default IPv6", + .src_ip = "IP address to use for sending the query (default: 0::0 / IPv6 auto-select)", .src_port = "UDP port to use for sending the query (default: 0 / any port)", - .timezone = "Timezone to use in results display (default: UTC)", - .json = "Print result as JSON", + .dst_port = "UDP port of destination server (default: 123)", + .timezone = "Timezone to use in console output (default: UTC)", + .json = "Print result in JSON", + .interval = "Interval for repeated queries in seconds (default: null / one-shot operation)", + .all = "Query all IP addresses found for a given server URL (default: false / stop after first)", }; pub const switches = .{ .server = 's', - .port = 'p', .protocol_version = 'v', - .all = 'a', .timezone = 'z', .json = 'j', + .interval = 'i', + .all = 'a', + // .ipv4 = '4', }; diff --git a/src/main.zig b/src/main.zig index 93b1c44..300a2e6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -18,9 +18,14 @@ const pprint = @import("prettyprint.zig"); // ------------------------------------------------------------------------------------ const timeout_sec: isize = 5; // wait-for-reply timeout const mtu: usize = 1024; // buffer size for transmission +const ip_default = "0::0"; // ------------------------------------------------------------------------------------ pub fn main() !void { + // var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + // const allocator = gpa.allocator(); + // defer _ = gpa.deinit(); + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -29,22 +34,28 @@ pub fn main() !void { var args = try std.process.argsWithAllocator(allocator); defer args.deinit(); - const cli = flags.parse(&args, Cmd); + const cmd = flags.parse(&args, Cmd, .{}); - const port: u16 = cli.flags.port; - const proto_vers: u8 = cli.flags.protocol_version; + const proto_vers: u8 = cmd.protocol_version; if (proto_vers < 3 or proto_vers > 4) { return errprintln("invalid protocol version: {d}", .{proto_vers}); } + if (cmd.interval != null and cmd.all) { + return errprintln("cannot query all servers repeatedly", .{}); + } + if (cmd.interval) |interval| { + if (interval > 60 * 60 * 4 or interval < 2) + return errprintln("interval must be in range [2s..4h], got {d}s", .{interval}); + } var tz: Timezone = Timezone.UTC; defer tz.deinit(); - if (!std.mem.eql(u8, cli.flags.timezone, "UTC")) { - if (std.mem.eql(u8, cli.flags.timezone, "local")) { + if (!std.mem.eql(u8, cmd.timezone, "UTC")) { + if (std.mem.eql(u8, cmd.timezone, "local")) { tz = try Timezone.tzLocal(allocator); } else { tz = Timezone.runtimeFromTzfile( - cli.flags.timezone, + cmd.timezone, Timezone.tzdb_prefix, allocator, ) catch Timezone.UTC; @@ -54,23 +65,29 @@ pub fn main() !void { // --- prepare connection --------------------------------------------------------- // resolve hostname - const addrlist = net.getAddressList(allocator, cli.flags.server, port) catch { - return errprintln("invalid hostname '{s}'", .{cli.flags.server}); + const addrlist = net.getAddressList(allocator, cmd.server, cmd.dst_port) catch { + return errprintln("invalid hostname '{s}'", .{cmd.server}); }; defer addrlist.deinit(); + // only use default IPv4 if user specified to use IPv4 without setting a specific src IP: + const src_ip = if (cmd.ipv4 and std.mem.eql(u8, ip_default, cmd.src_ip)) + "0.0.0.0" // any available IPv4 + else + cmd.src_ip; + // from where to send the query. // Zig std docs: to handle IPv6 link-local unix addresses, // it is recommended to use `resolveIp` instead. - const addr_src = try std.net.Address.parseIp(cli.flags.src_ip, cli.flags.src_port); + const src_addr = try std.net.Address.parseIp(src_ip, cmd.src_port); const sock = try posix.socket( - addr_src.any.family, + src_addr.any.family, // CLOEXEC not strictly needed here; see open(2) man page. posix.SOCK.DGRAM | posix.SOCK.CLOEXEC, posix.IPPROTO.UDP, ); - try posix.bind(sock, &addr_src.any, addr_src.getOsSockLen()); + try posix.bind(sock, &src_addr.any, src_addr.getOsSockLen()); defer posix.close(sock); if (timeout_sec != 0) { // make this configurable ? ...0 would mean no timeout @@ -78,82 +95,98 @@ pub fn main() !void { sock, posix.SOL.SOCKET, posix.SO.RCVTIMEO, - &std.mem.toBytes(posix.timespec{ .tv_sec = timeout_sec, .tv_nsec = 0 }), + &std.mem.toBytes(posix.timespec{ .sec = timeout_sec, .nsec = 0 }), ); } // --- query server(s) ------------------------------------------------------------ - // TODO : get number of hops in the connection ?! - var buf: [mtu]u8 = std.mem.zeroes([mtu]u8); - iter_addrs: for (addrlist.addrs, 0..) |dst, i| { - var dst_addr_sock: posix.sockaddr = undefined; // must not use dst.any - var dst_addr_len: posix.socklen_t = dst.getOsSockLen(); - - ntp.Packet.initToBuffer(proto_vers, &buf); - - // packet created! - const T1: ntp.Time = ntp.Time.fromUnixNanos(@as(u64, @intCast(std.time.nanoTimestamp()))); - - _ = posix.sendto( - sock, - buf[0..ntp.packet_len], - 0, - &dst.any, - dst_addr_len, - ) catch |err| switch (err) { - error.AddressFamilyNotSupported => { - if (dst.any.family == posix.AF.INET6) { - errprintln("IPv6 error, try next server.", .{}); - continue :iter_addrs; - } - return err; - }, - else => |e| return e, - }; - - const n_recv: usize = posix.recvfrom( - sock, - buf[0..], - 0, - &dst_addr_sock, - &dst_addr_len, - ) catch |err| switch (err) { - error.WouldBlock => { - errprintln("Error: connection timed out", .{}); - if (i < addrlist.addrs.len - 1) errprintln("Try next server.", .{}); - continue :iter_addrs; - }, - else => |e| return e, - }; - - // reply received! - const T4: ntp.Time = ntp.Time.fromUnixNanos(@as(u64, @intCast(std.time.nanoTimestamp()))); - - if (n_recv != ntp.packet_len) { - errprintln("Error: invalid reply length", .{}); - if (i < addrlist.addrs.len - 1) errprintln("Try next server.", .{}); - continue :iter_addrs; - } + repeat: while (true) { + iter_addrs: for (addrlist.addrs, 0..) |dst, i| { + const result: ntp.Result = sample_ntp( + &sock, + &src_addr, + &dst, + &buf, + proto_vers, + ) catch |err| switch (err) { + error.AddressFamilyMismatch => { + errprintln( + "Error: IP address family mismatch for server at {any} (src: {s}, dst: {s})", + .{ dst, inet_family(src_addr.any.family), inet_family(dst.any.family) }, + ); + if (i < addrlist.addrs.len - 1) errprintln("Try next server...", .{}); + continue :iter_addrs; // continue to iterate addresses, even if -a is not set + }, + error.WouldBlock => { + errprintln("Error: connection timed out", .{}); + if (i < addrlist.addrs.len - 1) errprintln("Try next server...", .{}); + continue :iter_addrs; // continue to iterate addresses, even if -a is not set + }, + else => |e| return e, + }; + + if (cmd.json) { + try pprint.json(io.getStdOut().writer(), result, cmd.server, dst); + } else { + try pprint.humanfriendly(io.getStdOut().writer(), result, &tz, cmd.server, dst); + } + + if (!cmd.all) break :iter_addrs; + } // end loop 'iter_addrs' + if (cmd.interval) |interval| { + std.time.sleep(interval * std.time.ns_per_s); + } else break :repeat; + } // end loop 'repeat' +} - const p_result: ntp.Packet = ntp.Packet.parse(buf[0..ntp.packet_len].*); - const result: ntp.Result = ntp.Result.fromPacket(p_result, T1, T4); +// --- helpers ------------------------------------------------------------------------ - if (cli.flags.json) { - try pprint.json(io.getStdOut().writer(), result, cli.flags.server, dst); - } else { - try pprint.humanfriendly(io.getStdOut().writer(), result, &tz, cli.flags.server, dst); - } +/// Sample an NTP server at 'dst' from given socket and source address. +/// Result gets written to the buffer 'buf'. +fn sample_ntp(sock: *const posix.socket_t, src: *const net.Address, dst: *const net.Address, buf: []u8, protocol_version: u8) !ntp.Result { + + // Check src and dst addr if families match (both posix.AF.INET/v4 or posix.AF.INET6/v6). + if (src.any.family != dst.any.family) return error.AddressFamilyMismatch; + + var dst_addr_sock: posix.sockaddr = undefined; // must not use dst.any + var dst_addr_len: posix.socklen_t = dst.getOsSockLen(); + ntp.Packet.initToBuffer(protocol_version, buf); + const T1: ntp.Time = ntp.Time.fromUnixNanos(@as(u64, @intCast(std.time.nanoTimestamp()))); + _ = try posix.sendto( + sock.*, + buf[0..ntp.packet_len], + 0, + &dst.any, + dst_addr_len, + ); + const n_recv: usize = try posix.recvfrom( + sock.*, + buf[0..], + 0, + &dst_addr_sock, + &dst_addr_len, + ); + const T4: ntp.Time = ntp.Time.fromUnixNanos(@as(u64, @intCast(std.time.nanoTimestamp()))); + if (n_recv != ntp.packet_len) return error.invalidLength; - if (!cli.flags.all) break :iter_addrs; - } + return ntp.Result.fromPacket(ntp.Packet.parse(buf[0..ntp.packet_len].*), T1, T4); } -// --- helpers ------------------------------------------------------------------------ - +/// Print an error to stderr. fn errprintln(comptime fmt: []const u8, args: anytype) void { const stderr = io.getStdErr().writer(); nosuspend stderr.print(fmt ++ "\n", args) catch return; } + +/// Turn AF flags into an appropriate text representation. +fn inet_family(family: u16) []const u8 { + const result = switch (family) { + posix.AF.INET => "IPv4", + posix.AF.INET6 => "IPv6", + else => "unknown", + }; + return result; +} diff --git a/src/ntp.zig b/src/ntp.zig index 60cdc98..798d749 100644 --- a/src/ntp.zig +++ b/src/ntp.zig @@ -249,6 +249,8 @@ pub const Result = struct { offset: i64 = 0, /// round-trip delay in ns (network) delay: i64 = 0, + // TODO : inter-arrival jitter (RFC 3550) + ia_jitter: f64 = 0, /// dispersion / clock error estimate in ns disp: u64 = 0, @@ -322,6 +324,7 @@ pub const Result = struct { return true; } + /// NTP query result flagging /// bit | meaning /// ----|------------------ /// 0 | there is an unsynchronized leapsecond @@ -334,7 +337,7 @@ pub const Result = struct { /// 7 | client send time after client receive time /// 8 | server send time after server receive time /// 9 | round-trip time must be positive - pub const flag_descr = enum(u32) { + pub const result_flag = enum(u32) { OK = 0, unsynchronized_leapsecond = 1, incorrect_version = (1 << 1), @@ -354,7 +357,7 @@ pub const Result = struct { return; } var idx: usize = 0; - for (std.enums.values(flag_descr)) |v| { + for (std.enums.values(result_flag)) |v| { const prefix = if (idx > 0) ", " else ""; if ((@intFromEnum(v) & flags) > 0) { const s = try std.fmt.bufPrint(buf[idx..], "{s}{s}", .{ prefix, @tagName(v) }); @@ -365,57 +368,57 @@ pub const Result = struct { /// Validate result from an NTP query. Returns a set of flags as a u32. /// A result of zero means OK. If a bit is set, something is wrong. - /// See 'flag_descr'. + /// See 'result_flag'. pub fn validate(result: Result) u32 { - var flags: u32 = @intFromEnum(flag_descr.OK); + var flags: u32 = @intFromEnum(result_flag.OK); // # 0 - unsynchronized leapsecond if (result.leap_indicator == 3) - flags |= @intFromEnum(flag_descr.unsynchronized_leapsecond); + flags |= @intFromEnum(result_flag.unsynchronized_leapsecond); // # 1 - version not 3 or 4 if (result.version > 4 or result.version < 3) - flags |= @intFromEnum(flag_descr.incorrect_version); + flags |= @intFromEnum(result_flag.incorrect_version); // # 2 - mode not server-mode if (result.mode != server_mode) - flags |= @intFromEnum(flag_descr.incorrect_mode); + flags |= @intFromEnum(result_flag.incorrect_mode); // # 3 - stratum > max_stratum if (result.stratum > max_stratum) - flags |= @intFromEnum(flag_descr.stratum_too_large); + flags |= @intFromEnum(result_flag.stratum_too_large); // # 4 - incorrect_poll_freq = (1 << 4), // Note: RFC5905 specifies a min poll of 4, we ignore this deliberately if (result.poll > max_poll) - flags |= @intFromEnum(flag_descr.incorrect_poll_freq); + flags |= @intFromEnum(result_flag.incorrect_poll_freq); // # 5 - sync distance of the server; // Note: root_dispersion and _delay as found in the NTP packet only refer to the // server. To get the actual root distance, the client's delay / dispersion // to the root would have to be used. if ((result.root_dispersion +| result.root_delay / 2) > max_dist * ns_per_s) - flags |= @intFromEnum(flag_descr.server_sync_dist_too_large); + flags |= @intFromEnum(result_flag.server_sync_dist_too_large); // # 6 - server_sync_outdated = (1 << 6), if (result.T2.sub(result.Tref) > 1024 * ns_per_s) - flags |= @intFromEnum(flag_descr.server_sync_outdated); + flags |= @intFromEnum(result_flag.server_sync_outdated); // # 8 - T1>T4: cannot receive before send // Note: #1 this is incorrect across an NTP era boundary // #2 this might be incorrect due to poor clock resolution / accuracy if (result.T1.decode() > result.T4.decode()) - flags |= @intFromEnum(flag_descr.client_send_after_receive); + flags |= @intFromEnum(result_flag.client_send_after_receive); // # 9 - T2>T3: cannot receive before send // Note: #1 this is incorrect across an NTP era boundary // #2 this might be incorrect due to poor clock resolution / accuracy if (result.T2.decode() > result.T3.decode()) - flags |= @intFromEnum(flag_descr.server_send_after_receive); + flags |= @intFromEnum(result_flag.server_send_after_receive); // # 10 - round-trip time must not be negative if (result.delay < 0) - flags |= @intFromEnum(flag_descr.negative_rtt); + flags |= @intFromEnum(result_flag.negative_rtt); // TODO : ? // pub const max_dispersion: u64 = 16; // [s] diff --git a/src/test_ntplib.zig b/src/test_ntplib.zig index ba47745..01d984b 100644 --- a/src/test_ntplib.zig +++ b/src/test_ntplib.zig @@ -233,21 +233,21 @@ test "Result - validate / flags" { var buf: [256]u8 = std.mem.zeroes([256]u8); var flags = res.validate(); // stratum 1 is good - try testing.expectEqual(@intFromEnum(ntp.Result.flag_descr.OK), flags); + try testing.expectEqual(@intFromEnum(ntp.Result.result_flag.OK), flags); _ = try ntp.Result.printFlags(flags, &buf); try testing.expectEqualStrings("0 (OK)", std.mem.sliceTo(buf[0..], 0)); p.stratum = 17; res = ntp.Result.fromPacket(p, T1, T4); flags = res.validate(); - try testing.expectEqual(@intFromEnum(ntp.Result.flag_descr.stratum_too_large), flags); + try testing.expectEqual(@intFromEnum(ntp.Result.result_flag.stratum_too_large), flags); p.stratum = 1; // v---- client ! p.li_vers_mode = 0 << 6 | 3 << 3 | 3; res = ntp.Result.fromPacket(p, T1, T4); flags = res.validate(); - try testing.expectEqual(@intFromEnum(ntp.Result.flag_descr.incorrect_mode), flags); + try testing.expectEqual(@intFromEnum(ntp.Result.result_flag.incorrect_mode), flags); _ = try ntp.Result.printFlags(flags, &buf); try testing.expectEqualStrings("incorrect_mode", std.mem.sliceTo(buf[0..], 0)); @@ -263,5 +263,5 @@ test "Result - validate / flags" { p.poll = 18; res = ntp.Result.fromPacket(p, T1, T4); flags = res.validate(); - try testing.expect(flags & @intFromEnum(ntp.Result.flag_descr.incorrect_poll_freq) > 0); + try testing.expect(flags & @intFromEnum(ntp.Result.result_flag.incorrect_poll_freq) > 0); }