Skip to content

Commit

Permalink
to v0.0.6, Win compat
Browse files Browse the repository at this point in the history
  • Loading branch information
FObersteiner committed May 28, 2024
1 parent 15cb1a0 commit c3604c2
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 139 deletions.
20 changes: 9 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 <NTP-server-name> [options]
Arguments:
<NTP-server-name> 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).
Expand All @@ -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
3 changes: 2 additions & 1 deletion build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions build.zig.zon
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
30 changes: 30 additions & 0 deletions src/cmd.zig
Original file line number Diff line number Diff line change
@@ -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',
};
79 changes: 12 additions & 67 deletions src/main.zig
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright © 2024 Florian Obersteiner <[email protected]>
// 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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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});
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 <NTP-server-name>";
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:
\\ <NTP-server-name> 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',
};
};
81 changes: 23 additions & 58 deletions src/ntp.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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));
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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" {
Expand Down
Loading

0 comments on commit c3604c2

Please sign in to comment.