[Git][NTPsec/ntpsec][master] Prototype Python version of ntpdig lands as ntpdig/pyntpdig.
Eric S. Raymond
gitlab at mg.gitlab.com
Mon Nov 7 12:29:22 UTC 2016
Eric S. Raymond pushed to branch master at NTPsec / ntpsec
Commits:
6fdc20af by Eric S. Raymond at 2016-11-07T07:27:56-05:00
Prototype Python version of ntpdig lands as ntpdig/pyntpdig.
- - - - -
2 changed files:
- + ntpdig/pyntpdig
- wscript
Changes:
=====================================
ntpdig/pyntpdig
=====================================
--- /dev/null
+++ b/ntpdig/pyntpdig
@@ -0,0 +1,331 @@
+#!/usr/bin/python
+"""
+ntpdig - simple SNTP client
+
+"""
+from __future__ import print_function, division
+
+# This code is somewhat stripped down from the legacy C version. It
+# does however have one additional major feature; it can take 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.
+#
+# The one new option in this version is -p, borrowed from ntpdate.
+
+import sys, socket, select, struct, time, getopt, datetime
+
+class SNTPPacket:
+ @staticmethod
+ def rescale(t):
+ "Scale from NTP time to POSIX time"
+ # Note: assumes we're in the same NTP era as the transmitter...
+ UNIX_EPOCH = 2208988800L
+ return (t * 2**-32) - UNIX_EPOCH
+ def __init__(self, data):
+ self.hostname = None
+ self.resolved = None
+ self.received = time.time()
+ self.data = data
+ (self.livnm, self.stratum, self.poll, self.precision,
+ self.root_delay, self.root_dispersion,
+ self.reference_id, self.reference_timestamp,
+ self.origin_timestamp, self.receive_timestamp,
+ self.transmit_timestamp) = struct.unpack("!BBBBIIIQQQQ", data[:48])
+ self.root_delay *= 2**-16
+ self.root_dispersion *= 2**-16
+ self.reference_timestamp = SNTPPacket.rescale(self.reference_timestamp)
+ self.origin_timestamp = SNTPPacket.rescale(self.origin_timestamp)
+ self.receive_timestamp = SNTPPacket.rescale(self.receive_timestamp)
+ self.transmit_timestamp = SNTPPacket.rescale(self.transmit_timestamp)
+ if len(data) > 192:
+ self.extension_data = data[192:-12]
+ self.auth_data = data[-12:]
+ else:
+ self.extension_data = None
+ self.auth_data = None
+ def delta(self):
+ return self.root_delay
+ def epsilon(self):
+ return self.root_dispersion
+ def synchd(self):
+ "Synchronization distance, estimates worst-case error in seconds"
+ # This is "lambda" in NTP-speak, but that's a Python keyword
+ return abs(self.delta() - self.epsilon())
+ def adjust(self):
+ "Adjustment implied by this packet."
+ # FIXME: Clip low digits according to precision
+ return self.received - self.transmit_timestamp
+ def leap(self):
+ return ("no-leap", "add-leap", "del-leap", "unsync")[(self.livnm & 0x60) >> 6]
+
+def queryhost(server, concurrent, timeout=5, port=123):
+ "Query IP addresses associated with a specified host."
+ try:
+ iptuples = socket.getaddrinfo(server, 123,
+ af, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
+ except socket.gaierror as (errno, msg):
+ log("lookup of %s failed, errno %d = %s" % (server, errno, msg))
+ return []
+ sockets = []
+ packets = []
+ for (family, socktype, proto, canonname, sockaddr) in iptuples:
+ if debug:
+ log("querying %s (%s)" % (sockaddr[0], server))
+ s = socket.socket(family, socktype)
+ d = '\xe3' + '\0' * 47
+ s.sendto(d, sockaddr)
+ def read_append(s, packets):
+ d, a = s.recvfrom(1024)
+ pkt = SNTPPacket(d)
+ pkt.hostname = server
+ pkt.resolved = sockaddr[0]
+ packets.append(pkt)
+ time.sleep(gap)
+ if concurrent:
+ sockets.append(s)
+ else:
+ r, _, _ = select.select([s], [], [], timeout)
+ if not r:
+ return []
+ read_append(s, packets)
+ if concurrent:
+ while sockets:
+ r, _, _ = select.select(sockets, [], [], timeout)
+ if not r:
+ return packets
+ for s in sockets:
+ read_append(s, packets)
+ 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
+ NTP_MAXAGE = 86400 # one day in seconds
+
+ # This first chunk of code is supposed to go through all
+ # servers we know about to find the servers which
+ # 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.
+ #
+ nlist = 0 # none yet
+ filtered = []
+ for server in packets:
+ def drop(msg):
+ log("%s: Server dropped: %s\n" % (server.hostname, msg))
+ if server.stratum > NTP_INFIN:
+ drop("stratum too high")
+ continue
+ if server.leap() == "unsync":
+ drop("Leap not in sync")
+ continue
+ if not server.origin_timestamp < server.reference_timestamp:
+ drop("server is very broken")
+ continue
+ if server.origin_timestamp - server.reference_timestamp >= NTP_MAXAGE:
+ drop("Server has gone too long without sync")
+ continue
+ filtered.append(server)
+
+ 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, adjusted):
+ "Report on the SNTP packet selected for display, and its adjustment."
+ say = sys.stdout.write
+
+ # Cheesy way to get local timezone offset
+ gmt_time = int(time.time())
+ local_time = int(time.mktime(time.gmtime(gmt_time)))
+ tmoffset = (local_time - gmt_time) // 60 # In minutes
+
+ # The server's idea of the time
+ t = time.localtime(int(packet.transmit_timestamp))
+ ms = int(packet.transmit_timestamp * 1000000) % 1000000
+
+ date = time.strftime("%Y-%m-%d", t)
+ tod = time.strftime("%T", t) + (".%d" % ms)
+ sgn = ("%+d" % tmoffset)[0]
+ tz = "%s%02d%02d" % (sgn, 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}' % \
+ (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: sntp [ -<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 Normal verbose
+ -D yes set-debug-level Normal verbose
+ -g yes gap Set gap between requests
+ -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. SNTP 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__':
+ try:
+ (options, arguments) = getopt.getopt(sys.argv[1:],
+ "46a:c:dD:g:hjk:l:M:o:p:Sst:wWV",
+ ["ipv4","ipv6",
+ "authentication",
+ "concurrent",
+ "gap", "help", "json",
+ "keyfile", "logfile",
+ "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 = 50
+ json = False
+ keyfile = None
+ steplimit = 0 # Default is intentionally zero
+ samples = 1
+ step = False
+ slew = False
+ timeout = 5
+ 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"): # Not implemented yet
+ authkey = int(val)
+ elif switch in ("-c", "--concurrent"):
+ concurrent_hosts.append(val)
+ elif switch in ("-d", "--debug"):
+ debug += 1
+ elif switch in ("-D", "--set-debug-level"):
+ debug = int(val)
+ elif switch in ("-j", "--json"):
+ json = True
+ elif switch in ("-k", "--keyfile"): # Not implemented yet
+ keyfile = val
+ elif switch in ("-l", "--logfile"):
+ try:
+ logfp = open(val, "w")
+ except OSError:
+ self.warn("logfile open of %s failed.\n" % val)
+ raise SystemExit(1)
+ elif switch in ("-M", "--steplimit"): # Not implemented yet
+ steplimit = int(val)
+ elif switch in ("-p", "--samples"):
+ samples = int(val)
+ elif switch in ("-S", "--step"): # Not implemented yet
+ step = True
+ elif switch in ("-s", "--slew"): # Not implemented yet
+ slew = True
+ elif switch in ("-t", "--timeout"):
+ timeout = int(val)
+ elif switch in ("-h", "--help"):
+ print(usage)
+ raise SystemExit(0)
+ elif switch in ("-V", "--version"):
+ print("ntpdig 0.1\n") # FIXME: Use the version module
+ raise SystemExit(0)
+ else:
+ self.warn("Unknown command line switch or missing argument.\n")
+ self.warn(usage)
+ raise SystemExit(1)
+ except ValueError:
+ self.warn("Invalid argument.\n")
+ self.warn(usage)
+ raise SystemExit(1)
+
+ if authkey and keyfile is None:
+ self.warn("-a option requires -k.\n")
+ raise SystemExit(1)
+
+ gap /= 1000 # Scale gap to milliseconds
+
+ if not arguments:
+ arguments = ["localhost"]
+
+ returned = []
+ for server in concurrent_hosts:
+ returned += queryhost(server=server, concurrent=True, timeout=timeout)
+ if len(returned) >= samples:
+ break
+ for server in arguments:
+ returned += queryhost(server=server, concurrent=False, timeout=timeout)
+ if len(returned) >= samples:
+ break
+
+ returned = clock_select(returned)
+ if returned:
+ report(returned[0], json, False)
+ else:
+ log("no eligible servers")
+#end
=====================================
wscript
=====================================
--- a/wscript
+++ b/wscript
@@ -129,12 +129,13 @@ def linkmaker(ctx):
# can import compiled Python modules from the build directory.
# Also, they need to be able to see the Python extension
# module built in libntp.
- print("Making in-tree links...")
- bldnode = ctx.bldnode.abspath()
- srcnode = ctx.srcnode.abspath()
- for d in ("ntpq", "ntpstats", "ntpsweep", "ntptrace", "ntpwait"):
- os.system("ln -sf %s/pylib %s/%s/ntp" % (bldnode, srcnode, d))
- os.system("ln -sf %s/libntp/ntpc.so %s/pylib/ntpc.so " % (bldnode, bldnode))
+ if ctx.cmd == "build":
+ print("Making in-tree links...")
+ bldnode = ctx.bldnode.abspath()
+ srcnode = ctx.srcnode.abspath()
+ for d in ("ntpq", "ntpdig", "ntpstats", "ntpsweep", "ntptrace", "ntpwait"):
+ os.system("ln -sf %s/pylib %s/%s/ntp" % (bldnode, srcnode, d))
+ os.system("ln -sf %s/libntp/ntpc.so %s/pylib/ntpc.so " % (bldnode, bldnode))
def build(ctx):
ctx.load('waf', tooldir='wafhelpers/')
View it on GitLab: https://gitlab.com/NTPsec/ntpsec/commit/6fdc20af5e71d5477bcc5d8587c2017cea88669b
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.ntpsec.org/pipermail/vc/attachments/20161107/9318df91/attachment.html>
More information about the vc
mailing list