2 # -*- coding: utf-8 -*-
4 ntpdig - simple SNTP client
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
25 except ImportError as e
:
27 "ntpdig: can't find Python NTP library -- check PYTHONPATH.\n")
28 sys
.stderr
.write("%s\n" % e
)
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
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
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)
65 ntp
.packet
.dump_hex_printable(d
)
67 if not ntp
.packet
.Authenticator
.have_mac(d
):
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"
75 log("MAC verification on reply from %s succeeded"
77 pkt
= ntp
.packet
.SyncPacket(d
)
79 pkt
.resolved
= sockaddr
[0]
84 def queryhost(server
, concurrent
, timeout
=5, port
=123, bindaddr
=None):
85 "Query IP addresses associated with a specified host."
87 iptuples
= socket
.getaddrinfo(server
, port
,
88 af
, socket
.SOCK_DGRAM
,
90 except socket
.gaierror
as e
:
91 log("lookup of %s failed, errno %d = %s" % (server
, e
.args
[0], e
.args
[1]))
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]))
102 request
= ntp
.packet
.SyncPacket()
103 request
.transmit_timestamp
= ntp
.packet
.SyncPacket
.posix_to_ntp(
105 packet
= request
.flatten()
106 needgap
= (len(iptuples
) > 1) and (gap
> 0)
108 for (family
, socktype
, proto
, canonname
, sockaddr
) in iptuples
:
109 if needgap
and not firstloop
:
114 log("querying %s (%s)" % (sockaddr
[0], server
))
116 s
= socket
.socket(family
, socktype
)
119 log("Skipping because socket for %s of family"
120 " %d, type %d could not be formed." %
121 (sockaddr
[0], family
, socktype
))
126 log("Binding to Source IP %s," % bindaddr
)
127 s
.bind(bindsock
[0][4])
129 log("binding to %s failed, errno %d = %s" % (bindaddr
, e
.args
[0], e
.args
[1]))
131 if keyid
and keytype
and passwd
:
133 log("authenticating with %s key %d" % (keytype
, keyid
))
134 mac
= ntp
.packet
.Authenticator
.compute_mac(packet
,
135 keyid
, keytype
, passwd
)
137 log("MAC generation failed while querying %s" % server
)
142 s
.sendto(packet
, sockaddr
)
143 except socket
.error
as e
:
145 log("socket error on transmission: %s" % e
)
148 log("Sent to %s:" % (sockaddr
[0],))
149 ntp
.packet
.dump_hex_printable(packet
)
153 r
, _
, _
= select
.select([s
], [], [], timeout
)
155 read_append(s
, packets
, packet
, sockaddr
)
157 r
, _
, _
= select
.select(sockets
, [], [], timeout
)
161 read_append(s
, packets
, packet
, sockaddr
)
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.
179 for response
in packets
:
181 log("%s: Response dropped: %s" % (response
.hostname
, msg
))
182 if response
.stratum
> NTP_INFIN
:
183 drop("stratum too high")
185 if response
.version() < ntp
.magic
.NTP_OLDVERSION
:
186 drop("response version %d is too old" % response
.version())
188 if response
.mode() != ntp
.magic
.MODE_SERVER
:
189 drop("unexpected response mode %d" % response
.mode())
191 if response
.version() > ntp
.magic
.NTP_VERSION
:
192 drop("response version %d is too new" % response
.version())
194 if response
.stratum
== 0:
195 # FIXME: Do some kind of semi-useful diagnostic dump here
196 drop("stratum 0, probable KOD packet")
198 if response
.leap() == "unsync":
199 drop("leap not in sync")
201 if not response
.trusted
:
202 drop("request was authenticated but server is untrusted")
204 # Bypass this test if we ever support broadcast-client mode again
205 if response
.origin_timestamp
== 0:
206 drop("unexpected response timestamp")
208 filtered
.append(response
)
210 if len(filtered
) <= 1:
213 # Sort by stratum and other figures of merit
214 filtered
.sort(key
=lambda s
: (s
.stratum
, s
.synchd(), s
.root_delay
))
220 def report(packet
, json
):
221 "Report on the SNTP packet selected for display, and its adjustment."
222 say
= sys
.stdout
.write
226 # The server's idea of the time
227 t
= time
.localtime(int(packet
.transmit_timestamp
))
228 ms
= int(packet
.transmit_timestamp
* 1000000) % 1000000
231 tmoffset
= -time
.altzone
// 60 # In minutes
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)
244 say('{"time":"%sT%s%s","offset":%f,"precision":%f,"host":"%s",'
245 '"ip":"%s","stratum":%s,"leap":"%s","adjusted":%s,"delay":%f}\n'
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()))
252 say("%s %s (%s) %+f +/- %f %s"
254 packet
.adjust(), packet
.synchd(),
256 if packet
.resolved
and packet
.resolved
!= packet
.hostname
:
257 say(" " + packet
.resolved
)
258 say(" s%d %s\n" % (packet
.stratum
, packet
.leap()))
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
)
296 (options
, arguments
) = getopt
.getopt(
298 "46a:c:dD:g:hI:jk:l:M:o:p:r:Sst:wWV",
303 "gap=", "help", "json",
304 "keyfile=", "logfile=",
306 "samples=", "steplimit=",
309 "debug", "set-debug-level=",
311 except getopt
.GetoptError
as e
:
314 progname
= sys
.argv
[0]
317 log
= lambda m
: logfp
.write("ntpdig: %s\n" % m
)
319 af
= socket
.AF_UNSPEC
322 concurrent_hosts
= []
327 steplimit
= 0 # Default is intentionally zero
334 for (switch
, val
) in options
:
335 if switch
in ("-4", "--ipv4"):
337 elif switch
in ("-6", "--ipv6"):
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"):
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"):
354 elif switch
in ("-j", "--json"):
356 elif switch
in ("-k", "--keyfile"):
358 elif switch
in ("-l", "--logfile"):
360 logfp
= open(val
, "w")
362 sys
.stderr
.write("logfile open of %s failed.\n" % val
)
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
)
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
373 elif switch
in ('-r', "--replay"):
375 elif switch
in ("-S", "--step"):
377 elif switch
in ("-s", "--slew"):
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"):
385 elif switch
in ("-V", "--version"):
386 print("ntpdig %s" % bin_ver
)
390 "Unknown command line switch or missing argument.\n")
391 sys
.stderr
.write(usage
)
394 sys
.stderr
.write("Invalid argument.\n")
395 sys
.stderr
.write(usage
)
398 gap
/= 1000 # convert to milliseconds
400 credentials
= keyid
= keytype
= passwd
= None
402 credentials
= ntp
.packet
.Authenticator(keyfile
)
403 except (OSError, IOError):
404 sys
.stderr
.write("ntpdig: %s nonexistent or unreadable\n" %
409 (keyid
, keytype
, passwd
) = credentials
.control(authkey
)
411 # There are no trusted keys. Barf.
412 log("cannot get authentication key")
415 if not credentials
and authkey
and keyfile
is None:
416 sys
.stderr
.write("-a option requires -k.\n")
420 arguments
= ["localhost"]
423 (pkt
, dst
) = replay
.split(":")
424 packet
= ntp
.packet
.SyncPacket(pkt
.decode("hex"))
425 packet
.received
= ntp
.packet
.SyncPacket
.posix_to_ntp(float(dst
))
429 needgap
= (samples
> 1) and (gap
> 0)
431 for s
in range(samples
):
432 if needgap
and not firstloop
:
436 for server
in concurrent_hosts
:
438 returned
+= queryhost(server
=server
,
442 except ntp
.packet
.SyncException
as e
:
445 for server
in arguments
:
447 returned
+= queryhost(server
=server
,
451 except ntp
.packet
.SyncException
as e
:
455 returned
= clock_select(returned
)
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())))
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()
475 (not slew
or (slew
and (abs(offset
) > steplimit
))))
477 # If we can step but we cannot slew, then step.
478 # If we can step or slew and |offset| > steplimit, then step.
480 ntp
.ntpc
.setprogname("ntpdig")
482 rc
= ntp
.ntpc
.step_systime(offset
)
484 rc
= ntp
.ntpc
.adj_systime(offset
)
490 log("no eligible servers")
492 except KeyboardInterrupt: