File: //bin/ntpdig
#! /usr/bin/python3
# -*- coding: utf-8 -*-
"""
ntpdig - simple SNTP client
"""
# SPDX-License-Identifier: BSD-2-Clause
# This code runs identically under Python 2 and Python 3. Keep it that way!
from __future__ import print_function, division
import getopt
import math
import select
import socket
import sys
import time
try:
import ntp.magic
import ntp.packet
import ntp.util
except ImportError as e:
sys.stderr.write(
"ntpdig: can't find Python NTP library -- check PYTHONPATH.\n")
sys.stderr.write("%s\n" % e)
sys.exit(1)
# This code is somewhat stripped down from the legacy C version. It
# does however have one additional major feature; it can filter
# out falsetickers from multiple samples, like the ntpdate of old,
# rather than just taking the first reply it gets.
#
# Listening to broadcast addresses is not implemented because that is
# impossible to secure. KOD recording is also not implemented, as it
# can too easily be spammed. Thus, the options -b and -K are not
# implemented.
#
# There are no version 3 NTP servers left, so the -o version for setting
# NTP version has been omitted.
#
# Because ntpdig doesn't use symmetric-peer mode (it never did, and NTPsec has
# abolished that mode because it was a security hazard), there's no need to
# set the packet source port, so -r/--usereservedport has been dropped.
# If this option ever needs to be reinstated, the magic is described here:
# http://stackoverflow.com/questions/2694212/socket-set-source-port-number
# and would be s.bind(('', 123)) right after the socket creation.
#
# The -w/--wait and -W/--nowait options only made sense with asynchronous
# DNS. Asynchronous DNS was absurd overkill for this application, we are
# not looking up 20,000 hosts here. It has not been implemented, so neither
# have these options.
#
# Finally, logging to syslog by default was a design error, violating
# Unix principles, that has been fixed. To get this behavior when
# running in a script, redirect standard error to logger(1).
#
# The one new option in this version is -p, borrowed from ntpdate.
def read_append(s, packets, packet, sockaddr):
d, a = s.recvfrom(1024)
if debug >= 2:
ntp.packet.dump_hex_printable(d)
if credentials:
if not ntp.packet.Authenticator.have_mac(d):
if debug:
log("no MAC on reply from %s" % packet.hostname)
if not credentials.verify_mac(d, packet_end=48, mac_begin=48):
packet.trusted = False
log("MAC verification on reply from %s failed"
% sockaddr[0])
elif debug:
log("MAC verification on reply from %s succeeded"
% sockaddr[0])
pkt = ntp.packet.SyncPacket(d)
pkt.hostname = server
pkt.resolved = sockaddr[0]
packets.append(pkt)
return packets
def queryhost(server, concurrent, timeout=5, port=123):
"Query IP addresses associated with a specified host."
try:
iptuples = socket.getaddrinfo(server, port,
af, socket.SOCK_DGRAM,
socket.IPPROTO_UDP)
except socket.gaierror as e:
log("lookup of %s failed, errno %d = %s" % (server, e.args[0], e.args[1]))
return []
sockets = []
packets = []
request = ntp.packet.SyncPacket()
request.transmit_timestamp = ntp.packet.SyncPacket.posix_to_ntp(
time.time())
packet = request.flatten()
needgap = (len(iptuples) > 1) and (gap > 0)
firstloop = True
for (family, socktype, proto, canonname, sockaddr) in iptuples:
if needgap and not firstloop:
time.sleep(gap)
if firstloop:
firstloop = False
if debug:
log("querying %s (%s)" % (sockaddr[0], server))
s = socket.socket(family, socktype)
if keyid and keytype and passwd:
if debug:
log("authenticating with %s key %d" % (keytype, keyid))
mac = ntp.packet.Authenticator.compute_mac(packet,
keyid, keytype, passwd)
if mac is None:
log("MAC generation failed while querying %s" % server)
raise SystemExit(1)
else:
packet += mac
try:
s.sendto(packet, sockaddr)
except socket.error as e:
if debug:
log("socket error on transmission: %s" % e)
continue
if debug >= 2:
log("Sent to %s:" % (sockaddr[0],))
ntp.packet.dump_hex_printable(packet)
if concurrent:
sockets.append(s)
else:
r, _, _ = select.select([s], [], [], timeout)
if r:
read_append(s, packets, packet, sockaddr)
while sockets:
r, _, _ = select.select(sockets, [], [], timeout)
if not r:
return packets
for s in sockets:
read_append(s, packets, packet, sockaddr)
sockets.remove(s)
return packets
def clock_select(packets):
"Select the pick-of-the-litter clock from the samples we've got."
# This is a slightly simplified version of the filter ntpdate used
NTP_INFIN = 15 # max stratum, infinity a la Bellman-Ford
# This first chunk of code is supposed to go through all
# servers we know about to find the servers that
# are most likely to succeed. We run through the list
# doing the sanity checks and trying to insert anyone who
# looks okay. We are at all times aware that we should
# only keep samples from the top two strata.
#
filtered = []
for response in packets:
def drop(msg):
log("%s: Response dropped: %s" % (response.hostname, msg))
if response.stratum > NTP_INFIN:
drop("stratum too high")
continue
if response.version() < ntp.magic.NTP_OLDVERSION:
drop("response version %d is too old" % response.version())
continue
if response.mode() != ntp.magic.MODE_SERVER:
drop("unexpected response mode %d" % response.mode())
continue
if response.version() > ntp.magic.NTP_VERSION:
drop("response version %d is too new" % response.version())
continue
if response.stratum == 0:
# FIXME: Do some kind of semi-useful diagnostic dump here
drop("stratum 0, probable KOD packet")
continue
if response.leap() == "unsync":
drop("leap not in sync")
continue
if not response.trusted:
drop("request was authenticated but server is untrusted")
continue
# Bypass this test if we ever support broadcast-client mode again
if response.origin_timestamp == 0:
drop("unexpected response timestamp")
continue
filtered.append(response)
if len(filtered) <= 1:
return filtered
# Sort by stratum and other figures of merit
filtered.sort(key=lambda s: (s.stratum, s.synchd(), s.root_delay))
# Return the best
return filtered[:1]
def report(packet, json):
"Report on the SNTP packet selected for display, and its adjustment."
say = sys.stdout.write
packet.posixize()
# The server's idea of the time
t = time.localtime(int(packet.transmit_timestamp))
ms = int(packet.transmit_timestamp * 1000000) % 1000000
if t.tm_isdst:
tmoffset = -time.altzone // 60 # In minutes
else:
tmoffset = -time.timezone // 60 # In minutes
# Number of decimal digits of precision indicated by the precision field
digits = min(6, -int(math.log10(2**packet.precision)))
date = time.strftime("%Y-%m-%d", t)
tod = time.strftime("%T", t) + (".%-*d" % (digits, ms)).rstrip()
sgn = ("%+d" % tmoffset)[0]
tz = "%s%02d%02d" % (sgn, abs(tmoffset) // 60, tmoffset % 60)
if json:
say('{"time":"%sT%s%s","offset":%f,"precision":%f,"host":"%s",'
'"ip":"%s","stratum":%s,"leap":"%s","adjusted":%s}\n'
% (date, tod, tz,
packet.adjust(), packet.synchd(),
packet.hostname, packet.resolved or packet.hostname,
packet.stratum, packet.leap(),
"true" if adjusted else "false"))
else:
say("%s %s (%s) %+f +/- %f %s"
% (date, tod, tz,
packet.adjust(), packet.synchd(),
packet.hostname))
if packet.resolved and packet.resolved != packet.hostname:
say(" " + packet.resolved)
say(" s%d %s\n" % (packet.stratum, packet.leap()))
usage = """
USAGE: ntpdig [-<flag> [<val>] | --<name>[{=| }<val>]]...
[ hostname-or-IP ...]
Flg Arg Option-Name Description
-4 no ipv4 Force IPv4 DNS name resolution
- prohibits the option 'ipv6'
-6 no ipv6 Force IPv6 DNS name resolution
- prohibits the option 'ipv4'
-a Num authentication Enable authentication with the numbered key
-c yes concurrent Hosts to be queried concurrently
-d no debug Increase debug verbosity
-D yes set-debug-level Set debug verbosity
-g yes gap Set gap between requests in milliseconds
-j no json Use JSON output format
-l Str logfile Log to specified logfile
- prohibits the option 'syslog'
-p yes samples Number of samples to take (default 1)
-S no step Set (step) the time with clock_settime()
- prohibits the option 'step'
-s no slew Set (slew) the time with adjtime()
- prohibits the option 'slew'
-t Num timeout Request timeout in seconds (default 5)
-k Str keyfile Specify a keyfile. ntpdig will look in this file
for the key specified with -a
-V no version Output version information and exit
-h no help Display extended usage information and exit
"""
if __name__ == '__main__':
bin_ver = "ntpsec-1.2.2"
if ntp.util.stdversion() != bin_ver:
sys.stderr.write("Module/Binary version mismatch\n")
sys.stderr.write("Binary: %s\n" % bin_ver)
sys.stderr.write("Module: %s\n" % ntp.util.stdversion())
try:
try:
(options, arguments) = getopt.getopt(
sys.argv[1:],
"46a:c:dD:g:hjk:l:M:o:p:r:Sst:wWV",
["ipv4", "ipv6",
"authentication=",
"concurrent=",
"gap=", "help", "json",
"keyfile=", "logfile=",
"replay=",
"samples=", "steplimit=",
"step", "slew",
"timeout=",
"debug", "set-debug-level=",
"version"])
except getopt.GetoptError as e:
print(e)
raise SystemExit(1)
progname = sys.argv[0]
logfp = sys.stderr
log = lambda m: logfp.write("ntpdig: %s\n" % m)
af = socket.AF_UNSPEC
authkey = None
concurrent_hosts = []
debug = 0
gap = .05
json = False
keyfile = None
steplimit = 0 # Default is intentionally zero
samples = 1
step = False
slew = False
timeout = 5
replay = None
try:
for (switch, val) in options:
if switch in ("-4", "--ipv4"):
af = socket.AF_INET
elif switch in ("-6", "--ipv6"):
af = socket.AF_INET6
elif switch in ("-a", "--authentication"):
errmsg = "Error: -a parameter '%s' not a number\n"
authkey = ntp.util.safeargcast(val, int, errmsg, usage)
elif switch in ("-c", "--concurrent"):
concurrent_hosts.append(val)
elif switch in ("-d", "--debug"):
debug += 1
elif switch in ("-D", "--set-debug-level"):
errmsg = "Error: -D parameter '%s' not a number\n"
debug = ntp.util.safeargcast(val, int, errmsg, usage)
elif switch in ("-g", "--gap"):
errmsg = "Error: -g parameter '%s' not a number\n"
gap = ntp.util.safeargcast(val, int, errmsg, usage)
elif switch in ("-j", "--json"):
json = True
elif switch in ("-k", "--keyfile"):
keyfile = val
elif switch in ("-l", "--logfile"):
try:
logfp = open(val, "w")
except OSError:
sys.stderr.write("logfile open of %s failed.\n" % val)
raise SystemExit(1)
elif switch in ("-M", "--steplimit"):
errmsg = "Error: -M parameter '%s' not a number\n"
steplimit = ntp.util.safeargcast(val, int, errmsg, usage)
steplimit /= 1000.0
elif switch in ("-p", "--samples"):
errmsg = "Error: -p parameter '%s' not a number\n"
samples = ntp.util.safeargcast(val, int, errmsg, usage)
if samples < 1: # If <1 it won't run at all
samples = 1
elif switch in ('-r', "--replay"):
replay = val
elif switch in ("-S", "--step"):
step = True
elif switch in ("-s", "--slew"):
slew = True
elif switch in ("-t", "--timeout"):
errmsg = "Error: -t parameter '%s' not a number\n"
timeout = ntp.util.safeargcast(val, int, errmsg, usage)
elif switch in ("-h", "--help"):
print(usage)
raise SystemExit(0)
elif switch in ("-V", "--version"):
print("ntpdig %s" % bin_ver)
raise SystemExit(0)
else:
sys.stderr.write(
"Unknown command line switch or missing argument.\n")
sys.stderr.write(usage)
raise SystemExit(1)
except ValueError:
sys.stderr.write("Invalid argument.\n")
sys.stderr.write(usage)
raise SystemExit(1)
gap /= 1000 # convert to milliseconds
credentials = keyid = keytype = passwd = None
try:
credentials = ntp.packet.Authenticator(keyfile)
except (OSError, IOError):
sys.stderr.write("ntpdig: %s nonexistent or unreadable\n" %
keyfile)
raise SystemExit(1)
if credentials:
try:
(keyid, keytype, passwd) = credentials.control(authkey)
except ValueError:
# There are no trusted keys. Barf.
log("cannot get authentication key")
raise SystemExit(1)
if not credentials and authkey and keyfile is None:
sys.stderr.write("-a option requires -k.\n")
raise SystemExit(1)
if not arguments:
arguments = ["localhost"]
if replay:
(pkt, dst) = replay.split(":")
packet = ntp.packet.SyncPacket(pkt.decode("hex"))
packet.received = ntp.packet.SyncPacket.posix_to_ntp(float(dst))
returned = [packet]
else:
returned = []
needgap = (samples > 1) and (gap > 0)
firstloop = True
for s in range(samples):
if needgap and not firstloop:
time.sleep(gap)
if firstloop:
firstloop = False
for server in concurrent_hosts:
try:
returned += queryhost(server=server,
concurrent=True,
timeout=timeout)
except ntp.packet.SyncException as e:
log(str(e))
continue
for server in arguments:
try:
returned += queryhost(server=server,
concurrent=False,
timeout=timeout)
except ntp.packet.SyncException as e:
log(str(e))
continue
returned = clock_select(returned)
if returned:
pkt = returned[0]
if debug:
# print(repr(pkt))
def hexstamp(n):
return "%08x.%08x" % (n >> 32, n & 0x00000000ffffffff)
print("org t1: %s rec t2: %s"
% (hexstamp(pkt.t1()), hexstamp(pkt.t2())))
print("xmt t3: %s dst t4: %s"
% (hexstamp(pkt.t3()), hexstamp(pkt.t4())))
pkt.posixize()
if debug:
print("org t1: %f rec t2: %f" % (pkt.t1(), pkt.t2()))
print("xmt t3: %f dst t4: %f" % (pkt.t3(), pkt.t4()))
print("rec-org t21: %f xmt-dst t34: %f"
% (pkt.t2() - pkt.t1(), pkt.t3() - pkt.t4()))
offset = pkt.adjust()
adjusted = (step and
(not slew or (slew and (abs(offset) > steplimit))))
report(pkt, json)
# If we can step but we cannot slew, then step.
# If we can step or slew and |offset| > steplimit, then step.
rc = True
ntp.ntpc.setprogname("ntpdig")
if adjusted:
rc = ntp.ntpc.step_systime(offset)
elif slew:
rc = ntp.ntpc.adj_systime(offset)
if rc:
raise SystemExit(0)
else:
raise SystemExit(1)
else:
log("no eligible servers")
raise SystemExit(1)
except KeyboardInterrupt:
print("")
# end