diff --git a/README.md b/README.md index 3b21e47..bd1db62 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,12 @@ # NTP Client -CLI app to query an NTP server to verify your OS clock setting. - -The original repository is hosted [on Codeberg](https://codeberg.org/FObersteiner/ntp_client). +Command line app to query an NTP server, to verify your OS clock setting. The original repository is hosted [on Codeberg](https://codeberg.org/FObersteiner/ntp_client). ```text -Usage: ntp_client [options] - -Arguments: - Name of the NTP server to query. The default is "pool.ntp.org". +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). @@ -20,13 +16,15 @@ Options: -h, --help Show this help and exit ``` -## Compatibility +## Compatibility and Requirements + +Developed & tested on Linux. Currently does not work on Windows since uses socket instance from `std.posix`. Other operating systems? Mac OS might work but otherwise: no idea, give it a try! -Developed & tested on Linux. Currently does not work on Windows since uses socket instance from `std.posix`. Other operating systems? Mac OS might work but otherwise no idea, give it a try! +### Zig -## Requirements +Currently works with `0.12-stable` and `master`. -Zig: currently works with `0.12-stable` and `master`. Packages: +### Packages - [flags](https://github.com/n0s4/flags) for command line argument parsing - [zdt](https://codeberg.org/FObersteiner/zdt) to display timestamps as UTC or timezone-local datetimes diff --git a/build.zig b/build.zig index 002ed3c..dcdddef 100644 --- a/build.zig +++ b/build.zig @@ -23,7 +23,8 @@ pub fn build(b: *std.Build) void { b.installArtifact(exe); - // exe.linkLibC(); // needed for DNS query + // for Windows compatibility, link libc since used by zdt + exe.linkLibC(); exe.root_module.addImport("flags", flags_module); exe.root_module.addImport("zdt", zdt_module); diff --git a/build.zig.zon b/build.zig.zon index a1384de..11471fc 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,10 +1,10 @@ .{ .name = "ntp_client", - .version = "0.0.4", + .version = "0.0.6", .dependencies = .{ .zdt = .{ .url = "https://codeberg.org/FObersteiner/zdt/archive/main.tar.gz", - .hash = "1220d1d998e7aa634459f3041e111c22aae892d3b2a3d8270ebfc11ca21a3f9cd083", + .hash = "12205afa4f678b4176360ed45fb25e69c39f4a0b2e61e30745efb70ada3a96cc00b9", }, .flags = .{ .url = "https://github.com/n0s4/flags/archive/main.tar.gz", diff --git a/src/cmd.zig b/src/cmd.zig new file mode 100644 index 0000000..94b6520 --- /dev/null +++ b/src/cmd.zig @@ -0,0 +1,30 @@ +// config struct for the flags package argument parser +const Cmd = @This(); + +server: []const u8 = "pool.ntp.org", +port: u16 = 123, +protocol_version: u8 = 4, +all: bool = false, +src_ip: []const u8 = "0.0.0.0", +src_port: u16 = 0, +timezone: []const u8 = "UTC", + +pub const name = "ntp_client"; + +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).", + .src_port = "UDP port to use for sending the query (default: 0 / any port).", + .timezone = "Timezone to use in results display (default: UTC)", +}; + +pub const switches = .{ + .server = 's', + .port = 'p', + .protocol_version = 'v', + .all = 'a', + .timezone = 'z', +}; diff --git a/src/main.zig b/src/main.zig index 74d8a20..e787a56 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,7 +1,7 @@ // Copyright © 2024 Florian Obersteiner // License: see LICENSE in the root directory of the repo. // -// ~~~ NTP query CLI app ~~~ +// ~~~ NTP query CLI ~~~ // const std = @import("std"); const io = std.io; @@ -13,14 +13,14 @@ const zdt = @import("zdt"); const Datetime = zdt.Datetime; const Timezone = zdt.Timezone; const Resolution = zdt.Duration.Resolution; +const Cmd = @import("cmd.zig"); const ntp = @import("ntp.zig"); +const pprint = @import("prettyprint.zig").pprint_result; test { _ = ntp; } -//----------------------------------------------------------------------------- -const default_server: []const u8 = "pool.ntp.org"; //----------------------------------------------------------------------------- const mtu: usize = 1024; // buffer size for transmission const ms: u64 = 1_000_000; @@ -33,13 +33,11 @@ pub fn main() !void { defer _ = gpa.deinit(); const allocator = gpa.allocator(); - var args = std.process.args(); - const cli = flags.parse(&args, Cmd); + // for Windows compatibility: feed an allocator for args parsing + var args = try std.process.argsWithAllocator(allocator); + defer args.deinit(); - const server_url = if (cli.args.len >= 1) - cli.args[0] - else - default_server; + const cli = flags.parse(&args, Cmd); const port: u16 = cli.flags.port; const proto_vers: u8 = cli.flags.protocol_version; @@ -64,8 +62,8 @@ pub fn main() !void { // ------------------------------------------------------------------------ // resolve hostname - const addrlist = net.getAddressList(allocator, server_url, port) catch { - return errprintln("invalid hostname '{s}'", .{server_url}); + const addrlist = net.getAddressList(allocator, cli.flags.server, port) catch { + return errprintln("invalid hostname '{s}'", .{cli.flags.server}); }; defer addrlist.deinit(); if (addrlist.canon_name) |n| println("Query server: {s}", .{n}); @@ -132,31 +130,10 @@ pub fn main() !void { continue :iter_addrs; } - // TODO move the whole printing stuff to a pretty-printer method of the result - println("Server address: {any}", .{dst}); const result: ntp.Result = ntp.Packet.analyze(buf[0..ntp.packet_len].*); - println("\n{s}\n", .{result}); - println( - "Server last synced : {s}", - .{try Datetime.fromUnix(result.ts_ref, Resolution.nanosecond, tz)}, - ); - println( - "T1, packet created : {s}", - .{try Datetime.fromUnix(result.ts_org, Resolution.nanosecond, tz)}, - ); - println( - "T2, server received : {s}", - .{try Datetime.fromUnix(result.ts_rec, Resolution.nanosecond, tz)}, - ); - println( - "T3, server replied : {s}", - .{try Datetime.fromUnix(result.ts_xmt, Resolution.nanosecond, tz)}, - ); - println( - "T4, reply received : {s}", - .{try Datetime.fromUnix(result.ts_processed, Resolution.nanosecond, tz)}, - ); - if (!std.mem.eql(u8, tz.name(), "UTC")) println("Time zone displayed : {s}", .{tz.name()}); + println("Server address: {any}\n", .{dst}); + + try pprint(io.getStdOut().writer(), result, &tz); if (!cli.flags.all) break :iter_addrs; } @@ -175,35 +152,3 @@ fn errprintln(comptime fmt: []const u8, args: anytype) void { const stderr = io.getStdErr().writer(); nosuspend stderr.print(fmt ++ "\n", args) catch return; } - -// config struct for the flags package argument parser -const Cmd = struct { - pub const name = "ntp_client "; - port: u16 = 123, - protocol_version: u8 = 4, - all: bool = false, - src_ip: []const u8 = "0.0.0.0", - src_port: u16 = 0, - timezone: []const u8 = "UTC", - - pub const help = ( - \\Arguments: - \\ Name of the NTP server to query. The default is "pool.ntp.org". - ); - - pub const descriptions = .{ - .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).", - .src_port = "UDP port to use for sending the query (default: 0 / any port).", - .timezone = "Timezone to use in results display (default: UTC)", - }; - - pub const switches = .{ - .port = 'p', - .protocol_version = 'v', - .all = 'a', - .timezone = 'z', - }; -}; diff --git a/src/ntp.zig b/src/ntp.zig index 3725450..61e05f1 100644 --- a/src/ntp.zig +++ b/src/ntp.zig @@ -52,26 +52,30 @@ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // // NOTE : only fields up to and including Transmit Timestamp are used further on. -// Extensions are not supported. +// Extensions are not supported (yet). // const std = @import("std"); const print = std.debug.print; const testing = std.testing; const native_endian = @import("builtin").target.cpu.arch.endian(); +const ns_per_s: u64 = 1_000_000_000; +const client_mode: u8 = 3; + /// NTP packet has 48 bytes if extension and key / digest fields are excluded. pub const packet_len: usize = 48; -/// Clock error estimate; only applicable is client makes repeated calls to a server + +/// Clock error estimate; only applicable if client makes repeated calls to a server. pub const max_disp: f32 = 16.0; // [s] -/// Too far away + +/// Too far away. pub const max_stratum: u8 = 16; -/// offset between the Unix epoch and the NTP epoch in seconds +/// Offset between the Unix epoch and the NTP epoch in seconds. pub const epoch_offset: u32 = 2_208_988_800; -const ns_per_s: u64 = 1_000_000_000; -const client_mode: u8 = 3; - +/// struct equivalent of the NPT packet definition. +/// Byte order is big endian (network). pub const Packet = packed struct { li_vers_mode: u8, // 2 bits leap second indicator, 3 bits protocol version, 3 bits mode stratum: u8 = 0, @@ -133,6 +137,9 @@ test "packet" { try testing.expect(std.meta.eql(want, have)); } +/// NTP's representation of an epoch time. +/// 32 bits (uint) for the seconds since 1900-01-01 Z and 32 bits (uint) for the fractional part. +/// Byte order is big endian (network). pub const NtpTime = struct { seconds: u32, fraction: u32, @@ -141,6 +148,8 @@ pub const NtpTime = struct { return .{ .seconds = sec, .fraction = frac }; } + /// Convert nanoseconds since the Unix epoch to NTP time. + /// Accounts for native byte order; network is big endian while native might be little. pub fn fromUnixNanos(nanos: i128) NtpTime { // use i128 here since this is what std.time.nanoTimestamp gives use const _secs: i64 = @truncate(@divFloor(nanos, @as(i128, ns_per_s)) + epoch_offset); const secs: u32 = if (_secs < 0) 0 else @intCast(_secs); @@ -152,6 +161,8 @@ pub const NtpTime = struct { }; } + /// Convert NTP time to nanoseconds since the Unix epoch. + /// Accounts for native byte order; network is big endian while native might be little. pub fn toUnixNanos(self: NtpTime) i64 { const _seconds = if (native_endian == .big) self.seconds else @byteSwap(self.seconds); const _fraction = if (native_endian == .big) self.fraction else @byteSwap(self.fraction); @@ -161,6 +172,9 @@ pub const NtpTime = struct { return ns + nsec; } + /// 'time short' is NTP's representation of a duration; + /// 16 bits for seconds and 16 bits for a fractional part. + /// Accounts for native byte order; network is big endian while native might be little. pub fn timeShortToNanos(data: u32) u64 { const _data = if (native_endian == .big) data else @byteSwap(data); const nanos: u64 = (_data >> 16) * ns_per_s; @@ -169,6 +183,7 @@ pub const NtpTime = struct { return nanos + nsfrac; } + /// Parse NTP 'time short' duration to nanoseconds. pub fn precisionToNanos(data: i8) u64 { if (data > 0) return ns_per_s << @as(u6, @intCast(data)); if (data < 0) return ns_per_s >> @as(u6, @intCast(-data)); @@ -233,6 +248,7 @@ pub const Result = struct { ts_xmt: i64 = 0, /// T4, when the packet was received and processed ts_processed: i64 = 0, + /// syncronization distance; rood delay / 2 + root dispersion /// offset of the local machine relative to the server theta: i64 = 0, @@ -267,57 +283,6 @@ pub const Result = struct { // TODO : add validate() - ref time fresh enough, stratum <= 16 etc. - // TODO : make this a pretty-printer that takes a formatter for the timestamps - pub fn format(self: Result, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { - _ = fmt; - _ = options; - const refid_bytes: [4]u8 = @bitCast(self.ref_id); - const prc: u64 = NtpTime.precisionToNanos(self.precision); - const theat_f: f64 = @as(f64, @floatFromInt(self.theta)) / @as(f64, ns_per_s); - const delta_f: f64 = @as(f64, @floatFromInt(self.delta)) / @as(f64, ns_per_s); - const lamda_f: f64 = @as(f64, @floatFromInt(self.lambda)) / @as(f64, ns_per_s); - try writer.print( - \\--- NPT query result ---> - \\LI={d} VN={d} Mode={d} Stratum={d} Poll={d} Precision={d} ({d} ns) - \\ref_id: {d} ({any}) - \\root_delay: {d} ns, root_dispersion: {d} ns - \\=> syncronization distance: {d} s - \\--- - \\server last synced : {d} - \\orgigin timestamp (T1) : {d} - \\reception timstamp (T2) : {d} - \\transmit timestamp (T3) : {d} - \\process timestamp (T4) : {d} - \\--- - \\offset to timserver: {d:.6} s ({d} ns) - \\round-trip delay: {d:.6} s ({d} ns) - \\<--- - , - .{ - self.leap_indicator, - self.version, - self.mode, - self.stratum, - self.poll, - self.precision, - prc, - self.ref_id, - refid_bytes, - self.root_delay, - self.root_dispersion, - lamda_f, - self.ts_ref, - self.ts_org, - self.ts_rec, - self.ts_xmt, - self.ts_processed, - theat_f, - self.theta, - delta_f, - self.delta, - }, - ); - } }; test "query result" { diff --git a/src/prettyprint.zig b/src/prettyprint.zig new file mode 100644 index 0000000..1d228f5 --- /dev/null +++ b/src/prettyprint.zig @@ -0,0 +1,64 @@ +const std = @import("std"); +const ntp = @import("ntp.zig"); +const zdt = @import("zdt"); +const Datetime = zdt.Datetime; +const Timezone = zdt.Timezone; +const Resolution = zdt.Duration.Resolution; + +const ns_per_s: u64 = 1_000_000_000; + +// TODO : add JSON output + +// pretty-print an npt-results struct +pub fn pprint_result(writer: anytype, ntpr: ntp.Result, tz: ?*Timezone) !void { + const prc: u64 = ntp.NtpTime.precisionToNanos(ntpr.precision); + const theat_f: f64 = @as(f64, @floatFromInt(ntpr.theta)) / @as(f64, ns_per_s); + const delta_f: f64 = @as(f64, @floatFromInt(ntpr.delta)) / @as(f64, ns_per_s); + const lamda_f: f64 = @as(f64, @floatFromInt(ntpr.lambda)) / @as(f64, ns_per_s); + var z: *Timezone = if (tz == null) @constCast(&Timezone.UTC) else tz.?; + + try writer.print( + \\NPT query result: + \\--- + \\LI={d} VN={d} Mode={d} Stratum={d} Poll={d} Precision={d} ({d} ns) + \\ref_id: {d} + \\root_delay: {d} ns, root_dispersion: {d} ns + \\=> syncronization distance: {d} s + \\--- + \\Server last synced : {s} + \\T1, packet created : {s} + \\T2, server received : {s} + \\T3, server replied : {s} + \\T4, reply received : {s} + \\(timezone displayed : {s}) + \\--- + \\offset to timserver: {d:.6} s ({d} ns) + \\round-trip delay: {d:.6} s ({d} ns) + \\--- + \\ + , + .{ + ntpr.leap_indicator, + ntpr.version, + ntpr.mode, + ntpr.stratum, + ntpr.poll, + ntpr.precision, + prc, + ntpr.ref_id, + ntpr.root_delay, + ntpr.root_dispersion, + lamda_f, + try Datetime.fromUnix(ntpr.ts_ref, Resolution.nanosecond, z.*), + try Datetime.fromUnix(ntpr.ts_org, Resolution.nanosecond, z.*), + try Datetime.fromUnix(ntpr.ts_rec, Resolution.nanosecond, z.*), + try Datetime.fromUnix(ntpr.ts_xmt, Resolution.nanosecond, z.*), + try Datetime.fromUnix(ntpr.ts_processed, Resolution.nanosecond, z.*), + z.name(), + theat_f, + ntpr.theta, + delta_f, + ntpr.delta, + }, + ); +}