ntplogtemp: Record nvme temperatures on Asahi
[ntpsec.git] / ntpclients / ntpdig.py
blobd2abf1c63b9a8c1ce97380574eec1abc509fd0f0
1 #! @PYSHEBANG@
2 # -*- coding: utf-8 -*-
3 """
4 ntpdig - simple SNTP client
6 """
8 # Copyright the NTPsec project contributors
10 # SPDX-License-Identifier: BSD-2-Clause
11 # This code runs identically under Python 2 and Python 3. Keep it that way!
13 from __future__ import print_function, division
14 import getopt
15 import math
16 import select
17 import socket
18 import sys
19 import time
21 try:
22 import ntp.magic
23 import ntp.packet
24 import ntp.util
25 except ImportError as e:
26 sys.stderr.write(
27 "ntpdig: can't find Python NTP library -- check PYTHONPATH.\n")
28 sys.stderr.write("%s\n" % e)
29 sys.exit(1)
30 # This code is somewhat stripped down from the legacy C version. It
31 # does however have one additional major feature; it can filter
32 # out falsetickers from multiple samples, like the ntpdate of old,
33 # rather than just taking the first reply it gets.
35 # Listening to broadcast addresses is not implemented because that is
36 # impossible to secure. KOD recording is also not implemented, as it
37 # can too easily be spammed. Thus, the options -b and -K are not
38 # implemented.
40 # There are no version 3 NTP servers left, so the -o version for setting
41 # NTP version has been omitted.
43 # Because ntpdig doesn't use symmetric-peer mode (it never did, and NTPsec has
44 # abolished that mode because it was a security hazard), there's no need to
45 # set the packet source port, so -r/--usereservedport has been dropped.
46 # If this option ever needs to be reinstated, the magic is described here:
47 # http://stackoverflow.com/questions/2694212/socket-set-source-port-number
48 # and would be s.bind(('', 123)) right after the socket creation.
50 # The -w/--wait and -W/--nowait options only made sense with asynchronous
51 # DNS. Asynchronous DNS was absurd overkill for this application, we are
52 # not looking up 20,000 hosts here. It has not been implemented, so neither
53 # have these options.
55 # Finally, logging to syslog by default was a design error, violating
56 # Unix principles, that has been fixed. To get this behavior when
57 # running in a script, redirect standard error to logger(1).
59 # The one new option in this version is -p, borrowed from ntpdate.
62 def read_append(s, packets, packet, sockaddr):
63 d, a = s.recvfrom(1024)
64 if debug >= 2:
65 ntp.packet.dump_hex_printable(d)
66 if credentials:
67 if not ntp.packet.Authenticator.have_mac(d):
68 if debug:
69 log("no MAC on reply from %s" % packet.hostname)
70 if not credentials.verify_mac(d, packet_end=48, mac_begin=48):
71 packet.trusted = False
72 log("MAC verification on reply from %s failed"
73 % sockaddr[0])
74 elif debug:
75 log("MAC verification on reply from %s succeeded"
76 % sockaddr[0])
77 pkt = ntp.packet.SyncPacket(d)
78 pkt.hostname = server
79 pkt.resolved = sockaddr[0]
80 packets.append(pkt)
81 return packets
84 def queryhost(server, concurrent, timeout=5, port=123, bindaddr=None):
85 "Query IP addresses associated with a specified host."
86 try:
87 iptuples = socket.getaddrinfo(server, port,
88 af, socket.SOCK_DGRAM,
89 socket.IPPROTO_UDP)
90 except socket.gaierror as e:
91 log("lookup of %s failed, errno %d = %s" % (server, e.args[0], e.args[1]))
92 return []
93 if bindaddr:
94 try:
95 bindsock = socket.getaddrinfo(bindaddr,None,af,
96 socket.SOCK_DGRAM,socket.IPPROTO_UDP)
97 except socket.gaierror as e:
98 log("lookup of %s failed, errno %d = %s" % (server, e.args[0], e.args[1]))
99 raise SystemExit(1)
100 sockets = []
101 packets = []
102 request = ntp.packet.SyncPacket()
103 request.transmit_timestamp = ntp.packet.SyncPacket.posix_to_ntp(
104 time.time())
105 packet = request.flatten()
106 needgap = (len(iptuples) > 1) and (gap > 0)
107 firstloop = True
108 for (family, socktype, proto, canonname, sockaddr) in iptuples:
109 if needgap and not firstloop:
110 time.sleep(gap)
111 if firstloop:
112 firstloop = False
113 if debug:
114 log("querying %s (%s)" % (sockaddr[0], server))
115 try:
116 s = socket.socket(family, socktype)
117 except OSError:
118 if debug:
119 log("Skipping because socket for %s of family"
120 " %d, type %d could not be formed." %
121 (sockaddr[0], family, socktype))
122 continue
123 if bindaddr:
124 try:
125 if debug:
126 log("Binding to Source IP %s," % bindaddr)
127 s.bind(bindsock[0][4])
128 except OSError as e:
129 log("binding to %s failed, errno %d = %s" % (bindaddr, e.args[0], e.args[1]))
130 raise SystemExit(1)
131 if keyid and keytype and passwd:
132 if debug:
133 log("authenticating with %s key %d" % (keytype, keyid))
134 mac = ntp.packet.Authenticator.compute_mac(packet,
135 keyid, keytype, passwd)
136 if mac is None:
137 log("MAC generation failed while querying %s" % server)
138 raise SystemExit(1)
139 else:
140 packet += mac
141 try:
142 s.sendto(packet, sockaddr)
143 except socket.error as e:
144 if debug:
145 log("socket error on transmission: %s" % e)
146 continue
147 if debug >= 2:
148 log("Sent to %s:" % (sockaddr[0],))
149 ntp.packet.dump_hex_printable(packet)
150 if concurrent:
151 sockets.append(s)
152 else:
153 r, _, _ = select.select([s], [], [], timeout)
154 if r:
155 read_append(s, packets, packet, sockaddr)
156 while sockets:
157 r, _, _ = select.select(sockets, [], [], timeout)
158 if not r:
159 return packets
160 for s in sockets:
161 read_append(s, packets, packet, sockaddr)
162 sockets.remove(s)
163 return packets
166 def clock_select(packets):
167 "Select the pick-of-the-litter clock from the samples we've got."
168 # This is a slightly simplified version of the filter ntpdate used
169 NTP_INFIN = 15 # max stratum, infinity a la Bellman-Ford
171 # This first chunk of code is supposed to go through all
172 # servers we know about to find the servers that
173 # are most likely to succeed. We run through the list
174 # doing the sanity checks and trying to insert anyone who
175 # looks okay. We are at all times aware that we should
176 # only keep samples from the top two strata.
178 filtered = []
179 for response in packets:
180 def drop(msg):
181 log("%s: Response dropped: %s" % (response.hostname, msg))
182 if response.stratum > NTP_INFIN:
183 drop("stratum too high")
184 continue
185 if response.version() < ntp.magic.NTP_OLDVERSION:
186 drop("response version %d is too old" % response.version())
187 continue
188 if response.mode() != ntp.magic.MODE_SERVER:
189 drop("unexpected response mode %d" % response.mode())
190 continue
191 if response.version() > ntp.magic.NTP_VERSION:
192 drop("response version %d is too new" % response.version())
193 continue
194 if response.stratum == 0:
195 # FIXME: Do some kind of semi-useful diagnostic dump here
196 drop("stratum 0, probable KOD packet")
197 continue
198 if response.leap() == "unsync":
199 drop("leap not in sync")
200 continue
201 if not response.trusted:
202 drop("request was authenticated but server is untrusted")
203 continue
204 # Bypass this test if we ever support broadcast-client mode again
205 if response.origin_timestamp == 0:
206 drop("unexpected response timestamp")
207 continue
208 filtered.append(response)
210 if len(filtered) <= 1:
211 return filtered
213 # Sort by stratum and other figures of merit
214 filtered.sort(key=lambda s: (s.stratum, s.synchd(), s.root_delay))
216 # Return the best
217 return filtered[:1]
220 def report(packet, json):
221 "Report on the SNTP packet selected for display, and its adjustment."
222 say = sys.stdout.write
224 packet.posixize()
226 # The server's idea of the time
227 t = time.localtime(int(packet.transmit_timestamp))
228 ms = int(packet.transmit_timestamp * 1000000) % 1000000
230 if t.tm_isdst:
231 tmoffset = -time.altzone // 60 # In minutes
232 else:
233 tmoffset = -time.timezone // 60 # In minutes
235 # Number of decimal digits of precision indicated by the precision field
236 digits = min(6, -int(math.log10(2**packet.precision)))
238 date = time.strftime("%Y-%m-%d", t)
239 tod = time.strftime("%T", t) + (".%0*d" % (digits, ms)).rstrip()
240 sgn = ("%+d" % tmoffset)[0]
241 tz = "%s%02d%02d" % (sgn, abs(tmoffset) // 60, tmoffset % 60)
243 if json:
244 say('{"time":"%sT%s%s","offset":%f,"precision":%f,"host":"%s",'
245 '"ip":"%s","stratum":%s,"leap":"%s","adjusted":%s,"delay":%f}\n'
246 % (date, tod, tz,
247 packet.adjust(), packet.synchd(),
248 packet.hostname, packet.resolved or packet.hostname,
249 packet.stratum, packet.leap(),
250 "true" if adjusted else "false", packet.delta()))
251 else:
252 say("%s %s (%s) %+f +/- %f %s"
253 % (date, tod, tz,
254 packet.adjust(), packet.synchd(),
255 packet.hostname))
256 if packet.resolved and packet.resolved != packet.hostname:
257 say(" " + packet.resolved)
258 say(" s%d %s\n" % (packet.stratum, packet.leap()))
261 usage = """
262 USAGE: ntpdig [-<flag> [<val>] | --<name>[{=| }<val>]]...
263 [ hostname-or-IP ...]
264 Flg Arg Option-Name Description
265 -4 no ipv4 Force IPv4 DNS name resolution
266 - prohibits the option 'ipv6'
267 -6 no ipv6 Force IPv6 DNS name resolution
268 - prohibits the option 'ipv4'
269 -a Num authentication Enable authentication with the numbered key
270 -c yes concurrent Hosts to be queried concurrently
271 -d no debug Increase debug verbosity
272 -D yes set-debug-level Set debug verbosity
273 -g yes gap Set gap between requests in milliseconds
274 -I IP bindaddr Set the source address to send request on
275 -j no json Use JSON output format
276 -l Str logfile Log to specified logfile
277 - prohibits the option 'syslog'
278 -p yes samples Number of samples to take (default 1)
279 -S no step Set (step) the time with clock_settime()
280 - prohibits the option 'step'
281 -s no slew Set (slew) the time with adjtime()
282 - prohibits the option 'slew'
283 -t Num timeout Request timeout in seconds (default 5)
284 -k Str keyfile Specify a keyfile. ntpdig will look in this file
285 for the key specified with -a
286 -V no version Output version information and exit
287 -h no help Display extended usage information and exit
291 if __name__ == '__main__':
292 bin_ver = "ntpsec-@NTPSEC_VERSION_EXTENDED@"
293 ntp.util.stdversioncheck(bin_ver)
294 try:
295 try:
296 (options, arguments) = getopt.getopt(
297 sys.argv[1:],
298 "46a:c:dD:g:hI:jk:l:M:o:p:r:Sst:wWV",
299 ["ipv4", "ipv6",
300 "bindaddr=",
301 "authentication=",
302 "concurrent=",
303 "gap=", "help", "json",
304 "keyfile=", "logfile=",
305 "replay=",
306 "samples=", "steplimit=",
307 "step", "slew",
308 "timeout=",
309 "debug", "set-debug-level=",
310 "version"])
311 except getopt.GetoptError as e:
312 print(e)
313 raise SystemExit(1)
314 progname = sys.argv[0]
316 logfp = sys.stderr
317 log = lambda m: logfp.write("ntpdig: %s\n" % m)
319 af = socket.AF_UNSPEC
320 bindaddr = None
321 authkey = None
322 concurrent_hosts = []
323 debug = 0
324 gap = .05
325 json = False
326 keyfile = None
327 steplimit = 0 # Default is intentionally zero
328 samples = 1
329 step = False
330 slew = False
331 timeout = 5
332 replay = None
333 try:
334 for (switch, val) in options:
335 if switch in ("-4", "--ipv4"):
336 af = socket.AF_INET
337 elif switch in ("-6", "--ipv6"):
338 af = socket.AF_INET6
339 elif switch in ("-a", "--authentication"):
340 errmsg = "Error: -a parameter '%s' not a number\n"
341 authkey = ntp.util.safeargcast(val, int, errmsg, usage)
342 elif switch in ("-c", "--concurrent"):
343 concurrent_hosts.append(val)
344 elif switch in ("-d", "--debug"):
345 debug += 1
346 elif switch in ("-D", "--set-debug-level"):
347 errmsg = "Error: -D parameter '%s' not a number\n"
348 debug = ntp.util.safeargcast(val, int, errmsg, usage)
349 elif switch in ("-g", "--gap"):
350 errmsg = "Error: -g parameter '%s' not a number\n"
351 gap = ntp.util.safeargcast(val, int, errmsg, usage)
352 elif switch in ("-I", "--bindaddr"):
353 bindaddr = val
354 elif switch in ("-j", "--json"):
355 json = True
356 elif switch in ("-k", "--keyfile"):
357 keyfile = val
358 elif switch in ("-l", "--logfile"):
359 try:
360 logfp = open(val, "w")
361 except OSError:
362 sys.stderr.write("logfile open of %s failed.\n" % val)
363 raise SystemExit(1)
364 elif switch in ("-M", "--steplimit"):
365 errmsg = "Error: -M parameter '%s' not a number\n"
366 steplimit = ntp.util.safeargcast(val, int, errmsg, usage)
367 steplimit /= 1000.0
368 elif switch in ("-p", "--samples"):
369 errmsg = "Error: -p parameter '%s' not a number\n"
370 samples = ntp.util.safeargcast(val, int, errmsg, usage)
371 if samples < 1: # If <1 it won't run at all
372 samples = 1
373 elif switch in ('-r', "--replay"):
374 replay = val
375 elif switch in ("-S", "--step"):
376 step = True
377 elif switch in ("-s", "--slew"):
378 slew = True
379 elif switch in ("-t", "--timeout"):
380 errmsg = "Error: -t parameter '%s' not a number\n"
381 timeout = ntp.util.safeargcast(val, int, errmsg, usage)
382 elif switch in ("-h", "--help"):
383 print(usage)
384 raise SystemExit(0)
385 elif switch in ("-V", "--version"):
386 print("ntpdig %s" % bin_ver)
387 raise SystemExit(0)
388 else:
389 sys.stderr.write(
390 "Unknown command line switch or missing argument.\n")
391 sys.stderr.write(usage)
392 raise SystemExit(1)
393 except ValueError:
394 sys.stderr.write("Invalid argument.\n")
395 sys.stderr.write(usage)
396 raise SystemExit(1)
398 gap /= 1000 # convert to milliseconds
400 credentials = keyid = keytype = passwd = None
401 try:
402 credentials = ntp.packet.Authenticator(keyfile)
403 except (OSError, IOError):
404 sys.stderr.write("ntpdig: %s nonexistent or unreadable\n" %
405 keyfile)
406 raise SystemExit(1)
407 if credentials:
408 try:
409 (keyid, keytype, passwd) = credentials.control(authkey)
410 except ValueError:
411 # There are no trusted keys. Barf.
412 log("cannot get authentication key")
413 raise SystemExit(1)
415 if not credentials and authkey and keyfile is None:
416 sys.stderr.write("-a option requires -k.\n")
417 raise SystemExit(1)
419 if not arguments:
420 arguments = ["localhost"]
422 if replay:
423 (pkt, dst) = replay.split(":")
424 packet = ntp.packet.SyncPacket(pkt.decode("hex"))
425 packet.received = ntp.packet.SyncPacket.posix_to_ntp(float(dst))
426 returned = [packet]
427 else:
428 returned = []
429 needgap = (samples > 1) and (gap > 0)
430 firstloop = True
431 for s in range(samples):
432 if needgap and not firstloop:
433 time.sleep(gap)
434 if firstloop:
435 firstloop = False
436 for server in concurrent_hosts:
437 try:
438 returned += queryhost(server=server,
439 concurrent=True,
440 timeout=timeout,
441 bindaddr=bindaddr)
442 except ntp.packet.SyncException as e:
443 log(str(e))
444 continue
445 for server in arguments:
446 try:
447 returned += queryhost(server=server,
448 concurrent=False,
449 timeout=timeout,
450 bindaddr=bindaddr)
451 except ntp.packet.SyncException as e:
452 log(str(e))
453 continue
455 returned = clock_select(returned)
457 if returned:
458 pkt = returned[0]
459 if debug:
460 # print(repr(pkt))
461 def hexstamp(n):
462 return "%08x.%08x" % (n >> 32, n & 0x00000000ffffffff)
463 print("org t1: %s rec t2: %s"
464 % (hexstamp(pkt.t1()), hexstamp(pkt.t2())))
465 print("xmt t3: %s dst t4: %s"
466 % (hexstamp(pkt.t3()), hexstamp(pkt.t4())))
467 pkt.posixize()
468 if debug:
469 print("org t1: %f rec t2: %f" % (pkt.t1(), pkt.t2()))
470 print("xmt t3: %f dst t4: %f" % (pkt.t3(), pkt.t4()))
471 print("rec-org t21: %f xmt-dst t34: %f"
472 % (pkt.t2() - pkt.t1(), pkt.t3() - pkt.t4()))
473 offset = pkt.adjust()
474 adjusted = (step and
475 (not slew or (slew and (abs(offset) > steplimit))))
476 report(pkt, json)
477 # If we can step but we cannot slew, then step.
478 # If we can step or slew and |offset| > steplimit, then step.
479 rc = True
480 ntp.ntpc.setprogname("ntpdig")
481 if adjusted:
482 rc = ntp.ntpc.step_systime(offset)
483 elif slew:
484 rc = ntp.ntpc.adj_systime(offset)
485 if rc:
486 raise SystemExit(0)
487 else:
488 raise SystemExit(1)
489 else:
490 log("no eligible servers")
491 raise SystemExit(1)
492 except KeyboardInterrupt:
493 print("")
495 # end