Skip to content

Commit

Permalink
update to v0.0.17
Browse files Browse the repository at this point in the history
  • Loading branch information
FObersteiner committed Aug 4, 2024
1 parent efcb64d commit bf0d65c
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 125 deletions.
37 changes: 20 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand All @@ -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)
---***---
```
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion build.zig
Original file line number Diff line number Diff line change
@@ -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(.{});
Expand Down
10 changes: 5 additions & 5 deletions build.zig.zon
Original file line number Diff line number Diff line change
@@ -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,
},
},
Expand Down
6 changes: 6 additions & 0 deletions docs/change.log
Original file line number Diff line number Diff line change
@@ -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
Expand Down
25 changes: 15 additions & 10 deletions src/cmd.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
181 changes: 107 additions & 74 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
Expand All @@ -54,106 +65,128 @@ 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
try posix.setsockopt(
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;
}
Loading

0 comments on commit bf0d65c

Please sign in to comment.