1# -*- coding: utf-8 -*-
2
3"""
4packet.py - definitions and classes for Python querying of NTP
5
6Freely translated from the old C ntpq code by ESR, with comments
7preserved.  The idea was to cleanly separate ntpq-that-was into a
8thin front-end layer handling mainly command interpretation and a
9back-end that presents the take from ntpd as objects that can be
10re-used by other front ends. Other reusable pieces live in util.py.
11
12This code should be Python2-vs-Python-3 agnostic.  Keep it that way!
13
14Here are some pictures to help make sense of this code. First, from RFC 5905,
15the general structure of an NTP packet (Figure 8):
16
17       0                   1                   2                   3
18       0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
19      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
20      |LI | VN  |Mode |    Stratum    |     Poll      |  Precision    |
21      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
22      |                         Root Delay                            |
23      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
24      |                         Root Dispersion                       |
25      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
26      |                          Reference ID                         |
27      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
28      |                                                               |
29      +                     Reference Timestamp (64)                  +
30      |                                                               |
31      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
32      |                                                               |
33      +                      Origin Timestamp (64)                    +
34      |                                                               |
35      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
36      |                                                               |
37      +                      Receive Timestamp (64)                   +
38      |                                                               |
39      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
40      |                                                               |
41      +                      Transmit Timestamp (64)                  +
42      |                                                               |
43      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
44      |                                                               |
45      .                                                               .
46      .                    Extension Field 1 (variable)               .
47      .                                                               .
48      |                                                               |
49      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
50      |                                                               |
51      .                                                               .
52      .                    Extension Field 2 (variable)               .
53      .                                                               .
54      |                                                               |
55      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
56      |                          Key Identifier                       |
57      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
58      |                                                               |
59      |                           digest (128)                        |
60      |                                                               |
61      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
62
63The fixed header is 48 bytes long.  The simplest possible case of an
64NTP packet is the minimal SNTP request, a mode 3 packet with the
65Stratum and all following fields zeroed out to byte 47.
66
67How to interpret these fields:
68
69The modes are as follows:
70
71+-------+--------------------------+
72| Value | Meaning                  |
73+-------+--------------------------+
74| 0     | reserved                 |
75| 1     | symmetric active         |
76| 2     | symmetric passive        |
77| 3     | client                   |
78| 4     | server                   |
79| 5     | broadcast                |
80| 6     | NTP control message      |
81| 7     | reserved for private use |
82+-------+--------------------------+
83
84While the Stratum field has 8 bytes, only values 0-16 (low 5 bits)
85are legal. Value 16 means 'unsynchronized' Values 17-255 are reserved.
86
87LI (Leap Indicator), Version, Poll, and Precision are not described
88here; see RFC 5905.
89
90t_1, the origin timestamp, is the time according to the client at
91which the request was sent.
92
93t_2, the receive timestamp, is the time according to the server at
94which the request was received.
95
96t_3, the transmit timestamp, is the time according to the server at
97which the reply was sent.
98
99You also need t_4, the destination timestamp, which is the time according to
100the client at which the reply was received.  This is not in the reply packet,
101it's the packet receipt time collected by the client.
102
103The 'Reference timestamp' is an unused historical relic.  It's supposed to be
104copied unchanged from upstream in the stratum hierarchy. Normal practice
105has been for Stratum 1 servers to fill it in with the raw timestamp from the
106most recent reference-clock.
107
108Theta is the thing we want to estimate: the offset between the server
109clock and the client clock. The sign convention is that theta is
110positive if the server is ahead of the client.
111
112Theta is estimated by [(t_2-t_1)+(t_3-t_4)]/2. The accuracy of this
113estimate is predicated upon network latency being symmetrical.
114
115Delta is the network round trip time, i.e. (t_4-t_1)-(t_3-t_2). Here's
116how the terms work: (t_4-t_1) is the total time that the request was
117in flight, and (t_3-t_2) is the time that the server spent processing it;
118when you subtract that out you're left with just network delays.
119
120Lambda nominally represents the maximum amount by which theta could be
121off. It's computed as delta/2 + epsilon. The delta/2 term usually
122dominates and represents the maximum amount by which network asymmetry
123could be throwing off the calculation. Epsilon is the sum of three
124other sources of error:
125
126rho_r: the (im)precision field from response packet, representing the
127server's inherent error in clock measurement.
128
129rho_s: the client's own (im)precision.
130
131PHI*(t_4-t_1): The amount by which the client's clock may plausibly
132have drifted while the packet was in flight. PHI is taken to be a
133constant of 15ppm.
134
135rho_r and rho_s are estimated by making back-to-back calls to
136clock_gettime() (or similar) and taking their difference. They're
137encoded on the wire as an eight-bit two's complement integer
138representing, to the nearest integer, log_2 of the value in seconds.
139
140If you look at the raw data, there are 3 unknowns:
141   * transit time client to server
142   * transit time server to client
143   * clock offset
144but there are only two equations, so you can't solve it.
145
146NTP gets the 3rd equation by assuming the transit times are equal.  That lets
147it solve for the clock offset.
148
149If you assume that both clocks are accurate which is reasonable if you have
150GPS at both ends, then you can easily solve for the transit times in each
151direction.
152
153The RFC 5905 diagram is slightly out of date in that the digest header assumes
154a 128-bit (16-octet) MD5 hash, but it is also possible for the field to be a
155128-bit AES_CMAC hash or 160-bit (20-octet) SHA-1 hash.  NTPsec will
156support any 128- or 160-bit MAC type in libcrypto.
157
158An extension field consists of a 16-bit network-order type field
159length, followed by a 16-bit network-order payload length in octets,
160followed by the payload (which must be padded to a 4-octet boundary).
161
162       0                   1                   2                   3
163       0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
164      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
165      |         Type field             |      Payload length          |
166      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
167      |                                                               |
168      |                        Payload (variable)                     |
169      |                                                               |
170      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
171
172Here's what a Mode 6 packet looks like:
173
174       0                   1                   2                   3
175       0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
176      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
177      |LI | VN  | 6   |R|E|M|  Opcode  |          Sequence            |
178      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
179      |               Status           |       Association ID         |
180      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
181      |               Offset           |            Count             |
182      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
183      |                                                               |
184      .                                                               .
185      .                        Payload (variable)                     .
186      .                                                               .
187      |                                                               |
188      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
189      |                          Key Identifier                       |
190      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
191      |                                                               |
192      |                           digest (128)                        |
193      |                                                               |
194      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
195
196In this case, the fixed header is 24 bytes long.
197
198R = Response bit
199E = Error bit
200M = More bit.
201
202A Mode 6 packet cannot have extension fields.
203
204"""
205# SPDX-License-Identifier: BSD-2-Clause
206from __future__ import print_function, division
207import getpass
208import hmac
209import os
210import random
211import select
212import socket
213import string
214import struct
215import sys
216import time
217import ntp.control
218import ntp.magic
219import ntp.ntpc
220import ntp.util
221import ntp.poly
222
223
224# Limit on packets in a single Mode 6 response.  Increasing this value to
225# 96 will marginally speed "mrulist" operation on lossless networks
226# but it has been observed to cause loss on WiFi networks and with
227# an IPv6 go6.net tunnel over UDP.  That loss causes the request
228# row limit to be cut in half, and it grows back very slowly to
229# ensure forward progress is made and loss isn't triggered too quickly
230# afterward.  While the lossless case gains only marginally with
231# MAXFRAGS == 96, the lossy case is a lot slower due to the repeated
232# timeouts.  Empirically, MAXFRAGS == 32 avoids most of the routine loss
233# on both the WiFi and UDP v6 tunnel tests and seems a good compromise.
234# This suggests some device in the path has a limit of 32 ~512 byte UDP
235# packets in queue.
236# Lowering MAXFRAGS may help with particularly lossy networks, but some
237# ntpq commands may rely on the longtime value of 24 implicitly,
238# assuming a single multipacket response will be large enough for any
239# needs.  In contrast, the "mrulist" command is implemented as a series
240# of requests and multipacket responses to each.
241MAXFRAGS = 32
242
243# Requests are automatically retried once, so total timeout with no
244# response is a bit over 2 * DEFTIMEOUT, or 10 seconds.  At the other
245# extreme, a request eliciting 32 packets of responses each for some
246# reason nearly DEFSTIMEOUT seconds after the prior in that series,
247# with a single packet dropped, would take around 32 * DEFSTIMEOUT, or
248# 93 seconds to fail each of two times, or 186 seconds.
249# Some commands involve a series of requests, such as "peers" and
250# "mrulist", so the cumulative timeouts are even longer for those.
251DEFTIMEOUT = 5000
252DEFSTIMEOUT = 3000
253
254# The maximum keyid for authentication, keyid is a 16-bit field
255MAX_KEYID = 0xFFFF
256
257# Some constants (in Bytes) for mode6 and authentication
258MODE_SIX_HEADER_LENGTH = 12
259MINIMUM_MAC_LENGTH = 16
260KEYID_LENGTH = 4
261MODE_SIX_ALIGNMENT = 8
262MAX_BARE_MAC_LENGTH = 20
263
264
265class Packet:
266    "Encapsulate an NTP fragment"
267    # The following two methods are copied from macros in includes/control.h
268    @staticmethod
269    def VN_MODE(v, m):
270        return (((v & 7) << 3) | (m & 0x7))
271
272    @staticmethod
273    def PKT_LI_VN_MODE(l, v, m):
274        return (((l & 3) << 6) | Packet.VN_MODE(v, m))
275
276    def __init__(self, mode=ntp.magic.MODE_CLIENT,
277                 version=ntp.magic.NTP_VERSION, session=None):
278        self.session = session  # Where to get session context
279        self.li_vn_mode = 0     # leap, version, mode (uint8_t)
280        # Subclasses have variable fields here
281        self.extension = b''     # extension data
282        self.li_vn_mode = Packet.PKT_LI_VN_MODE(ntp.magic.LEAP_NOTINSYNC,
283                                                version, mode)
284
285    # These decorators will allow us to assign the extension Python 3 strings
286    @property
287    def extension(self):
288        return self.__extension
289
290    @extension.setter
291    def extension(self, x):
292        self.__extension = ntp.poly.polybytes(x)
293
294    def leap(self):
295        return ("no-leap", "add-leap", "del-leap",
296                "unsync")[ntp.magic.PKT_LEAP(self.li_vn_mode)]
297
298    def version(self):
299        return (self.li_vn_mode >> 3) & 0x7
300
301    def mode(self):
302        return self.li_vn_mode & 0x7
303
304
305class SyncException(BaseException):  # pragma: no cover
306    def __init__(self, message, errorcode=0):
307        self.message = message
308        self.errorcode = errorcode
309
310    def __str__(self):
311        return self.message
312
313
314class SyncPacket(Packet):
315    "Mode 1-5 time-synchronization packet, including SNTP."
316    format = "!BBBbIIIQQQQ"
317    HEADER_LEN = 48
318    UNIX_EPOCH = 2208988800     # Midnight 1 Jan 1970 in secs since NTP epoch
319    PHI = 15 * 1e-6             # 15ppm
320
321    def __init__(self, data=''):
322        Packet.__init__(self)
323        self.status = 0         # status word for association (uint16_t)
324        self.stratum = 0
325        self.poll = 0
326        self.precision = 0
327        self.root_delay = 0
328        self.root_dispersion = 0
329        self.refid = 0
330        self.reference_timestamp = 0
331        self.origin_timestamp = 0
332        self.receive_timestamp = 0
333        self.transmit_timestamp = 0
334        self.extension = ''
335        self.extfields = []
336        self.mac = ''
337        self.hostname = None
338        self.resolved = None
339        self.received = SyncPacket.posix_to_ntp(time.time())
340        self.trusted = True
341        self.rescaled = False
342        if data:
343            self.analyze(ntp.poly.polybytes(data))
344
345    def analyze(self, data):
346        datalen = len(data)
347        if datalen < SyncPacket.HEADER_LEN or (datalen & 3) != 0:
348            raise SyncException("impossible packet length")
349        (self.li_vn_mode,
350         self.stratum,
351         self.poll,
352         self.precision,
353         self.root_delay,
354         self.root_dispersion,
355         self.refid,
356         self.reference_timestamp,
357         self.origin_timestamp,
358         self.receive_timestamp,
359         self.transmit_timestamp) = struct.unpack(
360            SyncPacket.format, data[:SyncPacket.HEADER_LEN])
361        self.extension = data[SyncPacket.HEADER_LEN:]
362        # Parse the extension field if present. We figure out whether
363        # an extension field is present by measuring the MAC size. If
364        # the number of 4-octet words following the packet header is
365        # 0, no MAC is present and the packet is not authenticated. If
366        # 1, the packet is a crypto-NAK; if 3, the packet is
367        # authenticated with DES; if 5, the packet is authenticated
368        # with MD5; if 6, the packet is authenticated with SHA1. If 2
369        # or 4, the packet is a runt and discarded forthwith. If
370        # greater than 6, an extension field is present, so we
371        # subtract the length of the field and go around again.
372        payload = self.extension  # Keep extension intact for flatten()
373        while len(payload) > 24:
374            (ftype, flen) = struct.unpack("!II", payload[:8])
375            self.extfields.append((ftype, payload[8:8+flen]))
376            payload = payload[8+flen:]
377        if len(payload) == 4:    # Crypto-NAK
378            self.mac = payload
379        elif len(payload) == 12:   # DES
380            raise SyncException("Unsupported DES authentication")
381        elif len(payload) in (8, 16):
382            raise SyncException("Packet is a runt")
383        elif len(payload) in (20, 24):   # MD5 or SHA1
384            self.mac = payload
385
386    @staticmethod
387    def ntp_to_posix(t):
388        "Scale from NTP time to POSIX time"
389        # Note: assumes we're in the same NTP era as the transmitter...
390        return (t / (2**32)) - SyncPacket.UNIX_EPOCH
391
392    @staticmethod
393    def posix_to_ntp(t):
394        "Scale from POSIX time to NTP time"
395        # Note: assumes we're in the same NTP era as the transmitter...
396        # This receives floats, can't use shifts
397        return int((t + SyncPacket.UNIX_EPOCH) * 2**32)
398
399    def posixize(self):
400        "Rescale all timestamps to POSIX time."
401        if not self.rescaled:
402            self.rescaled = True
403            self.root_delay >>= 16
404            self.root_dispersion >>= 16
405            self.reference_timestamp = SyncPacket.ntp_to_posix(
406                self.reference_timestamp)
407            self.origin_timestamp = SyncPacket.ntp_to_posix(
408                self.origin_timestamp)
409            self.receive_timestamp = SyncPacket.ntp_to_posix(
410                self.receive_timestamp)
411            self.transmit_timestamp = SyncPacket.ntp_to_posix(
412                self.transmit_timestamp)
413            self.received = SyncPacket.ntp_to_posix(self.received)
414
415    def t1(self):
416        return self.origin_timestamp
417
418    def t2(self):
419        return self.receive_timestamp
420
421    def t3(self):
422        return self.transmit_timestamp
423
424    def t4(self):
425        return self.received
426
427    def delta(self):
428        "Packet flight time"
429        return (self.t4() - self.t1()) - (self.t3() - self.t2())
430
431    def epsilon(self):
432        "Residual error due to clock imprecision."
433        # FIXME: Include client imprecision.
434        return SyncPacket.PHI * (self.t4() - self.t1()) + 2**self.precision
435
436    def synchd(self):
437        "Synchronization distance, estimates worst-case error in seconds"
438        # This is "lambda" in NTP-speak, but that's a Python keyword
439        return abs(self.delta()/2 + self.epsilon())
440
441    def adjust(self):
442        "Adjustment implied by this packet - 'theta' in NTP-speak."
443        return ((self.t2()-self.t1())+(self.t3()-self.t4()))/2
444
445    def flatten(self):
446        "Flatten the packet into an octet sequence."
447        body = struct.pack(SyncPacket.format,
448                           self.li_vn_mode,
449                           self.stratum,
450                           self.poll,
451                           self.precision,
452                           self.root_delay,
453                           self.root_dispersion,
454                           self.refid,
455                           self.reference_timestamp,
456                           self.origin_timestamp,
457                           self.receive_timestamp,
458                           self.transmit_timestamp)
459        return body + self.extension
460
461    def refid_octets(self):
462        "Analyze refid into octets."
463        return ((self.refid >> 24) & 0xff,
464                (self.refid >> 16) & 0xff,
465                (self.refid >> 8) & 0xff,
466                self.refid & 0xff)
467
468    def refid_as_string(self):
469        "Sometimes it's a clock name or KOD type"
470        return ntp.poly.polystr(struct.pack(*(("BBBB",) + self.refid_octets())))
471
472    def refid_as_address(self):
473        "Sometimes it's an IPV4 address."
474        return ntp.poly.polystr("%d.%d.%d.%d" % self.refid_octets())
475
476    def is_crypto_nak(self):
477        return len(self.mac) == 4
478
479    def has_MD5(self):
480        return len(self.mac) == 20
481
482    def has_SHA1(self):
483        return len(self.mac) == 24
484
485    def __repr__(self):
486        "Represent a posixized sync packet in an eyeball-friendly format."
487        r = "<NTP:%s:%d:%d:" % (self.leap(), self.version(), self.mode())
488        r += "%f:%f" % (self.root_delay, self.root_dispersion)
489        rs = self.refid_as_string()
490        if not all(c in string.printable for c in rs):
491            rs = self.refid_as_address()
492        r += ":" + rs
493        r += ":" + ntp.util.rfc3339(
494            SyncPacket.ntp_to_posix(self.reference_timestamp))
495        r += ":" + ntp.util.rfc3339(
496            SyncPacket.ntp_to_posix(self.origin_timestamp))
497        r += ":" + ntp.util.rfc3339(
498            SyncPacket.ntp_to_posix(self.receive_timestamp))
499        r += ":" + ntp.util.rfc3339(
500            SyncPacket.ntp_to_posix(self.transmit_timestamp))
501        if self.extfields:
502            r += ":" + repr(self.extfields)
503        if self.mac:
504            r += ":" + repr(self.mac)[1:-1]
505        r += ">"
506        return r
507
508
509class ControlPacket(Packet):
510    "Mode 6 request/response."
511
512    def __init__(self, session, opcode=0, associd=0, qdata=''):
513        Packet.__init__(self, mode=ntp.magic.MODE_CONTROL,
514                        version=session.pktversion,
515                        session=session)
516        self.r_e_m_op = opcode    # ntpq operation code
517        self.sequence = 1         # sequence number of request (uint16_t)
518        self.status = 0           # status word for association (uint16_t)
519        self.associd = associd    # association ID (uint16_t)
520        self.offset = 0           # offset of this batch of data (uint16_t)
521        self.extension = qdata    # Data for this packet
522        self.count = len(qdata)   # length of data
523    format = "!BBHHHHH"
524    HEADER_LEN = 12
525
526    def is_response(self):
527        return True if self.r_e_m_op & 0x80 else False
528
529    def is_error(self):
530        return True if self.r_e_m_op & 0x40 else False
531
532    def more(self):
533        return True if self.r_e_m_op & 0x20 else False
534
535    def opcode(self):
536        return self.r_e_m_op & 0x1F
537
538    def errcode(self):
539        return (self.status >> 8) & 0xff
540
541    def end(self):
542        return self.count + self.offset
543
544    def stats(self):
545        "Return statistics on a fragment."
546        return "%5d %5d\t%3d octets\n" % (self.offset, self.end(), self.count)
547
548    def analyze(self, rawdata):
549        rawdata = ntp.poly.polybytes(rawdata)
550        (self.li_vn_mode,
551         self.r_e_m_op,
552         self.sequence,
553         self.status,
554         self.associd,
555         self.offset,
556         self.count) = struct.unpack(ControlPacket.format,
557                                     rawdata[:ControlPacket.HEADER_LEN])
558        self.extension = rawdata[ControlPacket.HEADER_LEN:]
559        return (self.sequence, self.status, self.associd, self.offset)
560
561    def flatten(self):
562        "Flatten the packet into an octet sequence."
563        body = struct.pack(ControlPacket.format,
564                           self.li_vn_mode,
565                           self.r_e_m_op,
566                           self.sequence,
567                           self.status,
568                           self.associd,
569                           self.offset,
570                           self.count)
571        return body + self.extension
572
573    def send(self):
574        self.session.sendpkt(self.flatten())
575
576
577class Peer:
578    "The information we have about an NTP peer."
579
580    def __init__(self, session, associd, status):
581        self.session = session
582        self.associd = associd
583        self.status = status
584        self.variables = {}
585
586    def readvars(self):
587        self.variables = self.session.readvar()
588
589    def __str__(self):
590        return "<Peer: associd=%s status=%0x>" % (self.associd, self.status)
591    __repr__ = __str__
592
593
594SERR_BADFMT = "***Server reports a bad format request packet\n"
595SERR_PERMISSION = "***Server disallowed request (authentication?)\n"
596SERR_BADOP = "***Server reports a bad opcode in request\n"
597SERR_BADASSOC = "***Association ID {0} unknown to server\n"
598SERR_UNKNOWNVAR = "***A request variable unknown to the server\n"
599SERR_BADVALUE = "***Server indicates a request variable was bad\n"
600SERR_UNSPEC = "***Server returned an unspecified error\n"
601SERR_SOCKET = "***Socket error; probably ntpd is not running\n"
602SERR_TIMEOUT = "***Request timed out\n"
603SERR_INCOMPLETE = "***Response from server was incomplete\n"
604SERR_TOOMUCH = "***Buffer size exceeded for returned data\n"
605SERR_SELECT = "***Select call failed\n"
606SERR_NOHOST = "***No host open\n"
607SERR_BADLENGTH = "***Response length should have been a multiple of 4"
608SERR_BADKEY = "***Invalid key identifier"
609SERR_INVPASS = "***Invalid password"
610SERR_NOKEY = "***Key not found"
611SERR_BADNONCE = "***Unexpected nonce response format"
612SERR_BADPARAM = "***Unknown parameter '%s'"
613SERR_NOCRED = "***No credentials"
614SERR_SERVER = "***Server error code %s"
615SERR_STALL = "***No response, probably high-traffic server with low MRU limit"
616SERR_BADTAG = "***Bad MRU tag %s"
617SERR_BADSORT = "***Sort order %s is not implemented"
618SERR_NOTRUST = "***No trusted keys have been declared"
619
620
621def dump_hex_printable(xdata, outfp=sys.stdout):
622    "Dump a packet in hex, in a familiar hex format"
623    rowsize = 16
624    while xdata:
625        # Slice one row off of our data
626        linedata, xdata = ntp.util.slicedata(xdata, rowsize)
627        # Output data in hex form
628        linelen = len(linedata)
629        line = "%02x " * linelen
630        # Will need linedata later
631        linedata = [ntp.poly.polyord(x) for x in linedata]
632        line %= tuple(linedata)
633        if linelen < rowsize:  # Pad out the line to keep columns neat
634            line += "   " * (rowsize - linelen)
635        # Output printable data in string form
636        linedata = [chr(x) if (32 <= x < 127) else "." for x in linedata]
637        line += "".join(linedata) + "\n"
638        outfp.write(line)
639
640
641class MRUEntry:
642    "A traffic entry for an MRU list."
643
644    def __init__(self):
645        self.addr = None        # text of IPv4 or IPv6 address and port
646        self.last = None        # timestamp of last receipt
647        self.first = None       # timestamp of first receipt
648        self.mv = None          # mode and version
649        self.rs = None          # restriction mask (RES_* bits)
650        self.ct = 0             # count of packets received
651        self.sc = None          # score
652        self.dr = None          # dropped packets
653
654    def avgint(self):
655        last = ntp.ntpc.lfptofloat(self.last)
656        first = ntp.ntpc.lfptofloat(self.first)
657        return (last - first) / self.ct
658
659    def sortaddr(self):
660        addr = self.addr
661        if addr[0] == '[':
662            # IPv6   [n:n:n::n:n]:sock
663            # or     [n:n:n::n:n%x]:sock
664            addr = addr[1:addr.find(']')]
665            pct = addr.find('%')
666            if pct > 0:
667                # <addr>%<n> for local IPv6 address on interface n
668                addr = addr[:pct]
669            return socket.inet_pton(socket.AF_INET6, addr)
670        else:
671            # IPv4   a.b.c.d:sock
672            addr = addr[:addr.find(':')]
673            # prefix with 0s so IPv6 sorts after IPv4
674            # Need 16 rather than 12 to catch ::1
675            return b'\0'*16 + socket.inet_pton(socket.AF_INET, addr)
676
677    def __repr__(self):
678        return "<MRUEntry: " + repr(self.__dict__)[1:-1] + ">"
679
680
681class MRUList:
682    "A sequence of address-timespan pairs returned by ntpd in one response."
683
684    def __init__(self):
685        self.entries = []       # A list of MRUEntry objects
686        self.now = None         # server timestamp marking end of operation
687
688    def is_complete(self):
689        "Is the server done shipping entries for this span?"
690        return self.now is not None
691
692    def __repr__(self):
693        return "<MRUList: entries=%s now=%s>" % (self.entries, self.now)
694
695
696class ControlException(BaseException):
697
698    def __init__(self, message, errorcode=0):
699        self.message = message
700        self.errorcode = errorcode
701
702    def __str__(self):
703        return self.message
704
705
706class ControlSession:
707    "A session to a host"
708    MRU_ROW_LIMIT = 256
709    _authpass = True
710    server_errors = {
711        ntp.control.CERR_UNSPEC: "UNSPEC",
712        ntp.control.CERR_PERMISSION: "PERMISSION",
713        ntp.control.CERR_BADFMT: "BADFMT",
714        ntp.control.CERR_BADOP: "BADOP",
715        ntp.control.CERR_BADASSOC: "BADASSOC",
716        ntp.control.CERR_UNKNOWNVAR: "UNKNOWNVAR",
717        ntp.control.CERR_BADVALUE: "BADVALUE",
718        ntp.control.CERR_RESTRICT: "RESTRICT",
719        }
720
721    def __init__(self):
722        self.debug = 0
723        self.ai_family = socket.AF_UNSPEC
724        self.primary_timeout = DEFTIMEOUT       # Timeout for first select
725        self.secondary_timeout = DEFSTIMEOUT    # Timeout for later selects
726        # Packet version number we use
727        self.pktversion = ntp.magic.NTP_OLDVERSION + 1
728        self.always_auth = False  # Always send authenticated requests
729        self.keytype = "MD5"
730        self.keyid = None
731        self.passwd = None
732        self.auth = None
733        self.hostname = None
734        self.isnum = False
735        self.sock = None
736        self.port = 0
737        self.sequence = 0
738        self.response = ""
739        self.rstatus = 0
740        self.ntpd_row_limit = ControlSession.MRU_ROW_LIMIT
741        self.logfp = sys.stdout
742        self.nonce_xmit = 0
743        self.slots = 0
744        self.flakey = None
745
746    def warndbg(self, text, threshold):
747        ntp.util.dolog(self.logfp, text, self.debug, threshold)
748
749    def close(self):
750        if self.sock:
751            self.sock.close()
752            self.sock = None
753
754    def havehost(self):
755        "Is the session connected to a host?"
756        return self.sock is not None
757
758    def __lookuphost(self, hname, fam):
759        "Try different ways to interpret an address and family"
760        if hname.startswith("["):
761            hname = hname[1:-1]
762        # First try to resolve it as an IP address and if that fails,
763        # do a fullblown (DNS) lookup. That way we only use the DNS
764        # when it is needed and work around some implementations that
765        # will return an "IPv4-mapped IPv6 address" address if you
766        # give it an IPv4 address to lookup.
767
768        def hinted_lookup(port, hints):
769            return socket.getaddrinfo(hname, port, self.ai_family,
770                                      socket.SOCK_DGRAM,
771                                      socket.IPPROTO_UDP,
772                                      hints)
773        try:
774            return hinted_lookup(port="ntp", hints=socket.AI_NUMERICHOST)
775        except socket.gaierror as e:
776            ntp.util.dolog(self.logfp,
777                           "ntpq: numeric-mode lookup of %s failed, %s"
778                           % (hname, e.strerror), self.debug, 3)
779        try:
780            return hinted_lookup(port="ntp", hints=0)
781        except socket.gaierror as e1:
782            if self.logfp is not None:
783                self.logfp.write("ntpq: standard-mode lookup "
784                                 "of %s failed, %s\n"
785                                 % (hname, e1.strerror))
786            # EAI_NODATA and AI_CANONNAME should both exist - they're in the
787            # POSIX API.  If this code throws AttributeErrors there is
788            # probably a very old and broken socket layer in your Python
789            # build.  The C implementation had a second fallback mode that
790            # removed AI_ADDRCONFIG if the first fallback raised BADFLAGS.
791            fallback_hints = socket.AI_CANONNAME
792            try:
793                fallback_hints |= socket.AI_ADDRCONFIG
794            except AttributeError:
795                pass
796            try:
797                if hasattr(socket, "EAI_NODATA"):
798                    errlist = (socket.EAI_NONAME, socket.EAI_NODATA)
799                else:
800                    errlist = (socket.EAI_NONAME,)
801                if e1.errno in errlist:
802                    try:
803                        return hinted_lookup(port="ntp", hints=0)
804                    except socket.gaierror as e2:
805                        if self.logfp is not None:
806                            self.logfp.write("ntpq: ndp lookup failed, %s\n"
807                                             % e2.strerror)
808            except AttributeError:  # pragma: no cover
809                if self.logfp is not None:
810                    self.logfp.write(
811                        "ntpq: API error, missing socket attributes\n")
812        return None
813
814    def openhost(self, hname, fam=socket.AF_UNSPEC):
815        "openhost - open a socket to a host"
816        res = self.__lookuphost(hname, fam)
817        if res is None:
818            return False
819        # C implementation didn't use multiple responses, so we don't either
820        (family, socktype, protocol, canonname, sockaddr) = res[0]
821        if canonname is None:
822            self.hostname = socket.inet_ntop(sockaddr[0], family)
823            self.isnum = True
824        else:
825            self.hostname = canonname or hname
826            self.isnum = False
827        ntp.util.dolog(self.logfp, "Opening host %s" % self.hostname,
828                       self.debug, 3)
829        self.port = sockaddr[1]
830        try:
831            self.sock = socket.socket(family, socktype, protocol)
832        except socket.error as e:
833            raise ControlException("Error opening %s: %s [%d]"
834                                   % (hname, e.strerror, e.errno))
835        try:
836            self.sock.connect(sockaddr)
837        except socket.error as e:
838            raise ControlException("Error connecting to %s: %s [%d]"
839                                   % (hname, e.strerror, e.errno))
840        return True
841
842    def password(self):
843        "Get a keyid and the password if we don't have one."
844        if self.keyid is None:
845            if self.auth is None:
846                try:
847                    self.auth = Authenticator()
848                except (OSError, IOError):
849                    pass
850            if self.auth and self.hostname == "localhost":
851                try:
852                    (self.keyid, self.keytype, self.passwd) \
853                        = self.auth.control()
854                    return
855                except ValueError:
856                    # There are no trusted keys.  Barf.
857                    raise ControlException(SERR_NOTRUST)
858            try:
859                if os.isatty(0):
860                    key_id = int(ntp.poly.polyinput("Keyid: "))
861                else:
862                    key_id = 0
863                if key_id == 0 or key_id > MAX_KEYID:
864                    raise ControlException(SERR_BADKEY)
865            except (SyntaxError, ValueError):  # pragma: no cover
866                raise ControlException(SERR_BADKEY)
867            self.keyid = key_id
868
869        if self.passwd is None:
870            try:
871                self.keytype, passwd = self.auth[self.keyid]
872            except (IndexError, TypeError):
873                passwd = getpass.getpass("%s Password: " % self.keytype)
874                if passwd is None:
875                    raise ControlException(SERR_INVPASS)
876                # If the password is longer then 20 chars we assume it is
877                # hex encoded binary string. This assumption exists across all
878                # of NTP.
879                if len(passwd) > 20:
880                    passwd = ntp.util.hexstr2octets(passwd)
881            self.passwd = passwd
882
883    def sendpkt(self, xdata):
884        "Send a packet to the host."
885        while len(xdata) % 4:
886            xdata += b"\x00"
887        ntp.util.dolog(self.logfp,
888                       "Sending %d octets.  seq=%d"
889                       % (len(xdata), self.sequence), self.debug, 3)
890        try:
891            self.sock.sendall(ntp.poly.polybytes(xdata))
892        except socket.error:
893            # On failure, we don't know how much data was actually received
894            if self.logfp is not None:
895                self.logfp.write("Write to %s failed\n" % self.hostname)
896            return -1
897        if (self.debug >= 5) and (self.logfp is not None):  # pragma: no cover
898            # special, not replacing with dolog()
899            self.logfp.write("Request packet:\n")
900            dump_hex_printable(xdata, self.logfp)
901        return 0
902
903    def sendrequest(self, opcode, associd, qdata, auth=False):
904        "Ship an ntpq request packet to a server."
905        if (self.debug >= 1) and (self.logfp is not None):
906            # special, not replacing with dolog()
907            if self.debug >= 3:
908                self.logfp.write("\n")   # extra space to help find clumps
909            self.logfp.write("sendrequest: opcode=%d, associd=%d, qdata=%s\n"
910                             % (opcode, associd, qdata))
911
912        # Check to make sure the data will fit in one packet
913        if len(qdata) > ntp.control.CTL_MAX_DATA_LEN:
914            if self.logfp is not None:
915                self.logfp.write("***Internal error! Data too large (%d)\n" %
916                                 len(qdata))
917            return -1
918
919        # Assemble the packet
920        pkt = ControlPacket(self, opcode, associd, qdata)
921
922        self.sequence += 1
923        self.sequence %= 0x10000  # Has to fit in a struct H field
924        pkt.sequence = self.sequence
925
926        # If we have data, pad it out to a 32-bit boundary.
927        # Do not include these in the payload count.
928        if pkt.extension:
929            pkt.extension = ntp.poly.polybytes(pkt.extension)
930            while ((ControlPacket.HEADER_LEN + len(pkt.extension)) & 3):
931                pkt.extension += b"\x00"
932
933        # If it isn't authenticated we can just send it.  Otherwise
934        # we're going to have to think about it a little.
935        if not auth and not self.always_auth:
936            return pkt.send()
937
938        if self.keyid is None or self.passwd is None:
939            raise ControlException(SERR_NOCRED)
940
941        # Pad out packet to a multiple of 8 octets to be sure
942        # receiver can handle it. Note: these pad bytes should
943        # *not* be counted in the header count field.
944        while ((ControlPacket.HEADER_LEN + len(pkt.extension)) & 7):
945            pkt.extension += b"\x00"
946
947        # Do the MAC compuation.
948        mac = Authenticator.compute_mac(pkt.flatten(),
949                                        self.keyid, self.keytype, self.passwd)
950        if mac is None:
951            raise ControlException(SERR_NOKEY)
952        else:
953            pkt.extension += ntp.poly.polybytes(mac)
954        return pkt.send()
955
956    def getresponse(self, opcode, associd, timeo):
957        "Get a response expected to match a given opcode and associd."
958        # This is pretty tricky.  We may get between 1 and MAXFRAG packets
959        # back in response to the request.  We peel the data out of
960        # each packet and collect it in one long block.  When the last
961        # packet in the sequence is received we'll know how much data we
962        # should have had.  Note we use one long time out, should reconsider.
963        fragments = []
964        self.response = ''
965        seenlastfrag = False
966        bail = 0
967        # TODO: refactor to simplify while retaining semantic info
968        if self.logfp is not None:
969            warn = self.logfp.write
970        else:
971            warn = (lambda x: x)
972        warndbg = (lambda txt, th: ntp.util.dolog(self.logfp, txt,
973                                                  self.debug, th))
974
975        warndbg("Fragment collection begins", 1)
976        # Loop until we have an error or a complete response.  Nearly all
977        # code paths to loop again use continue.
978        while True:
979            # Discarding various invalid packets can cause us to
980            #  loop more than MAXFRAGS times, but enforce a sane bound
981            #  on how long we're willing to spend here.
982            bail += 1
983            if bail >= (2*MAXFRAGS):
984                raise ControlException(SERR_TOOMUCH)
985
986            if not fragments:
987                tvo = self.primary_timeout / 1000
988            else:
989                tvo = self.secondary_timeout / 1000
990
991            warndbg("At %s, select with timeout %d begins"
992                    % (time.asctime(), tvo), 5)
993            try:
994                (rd, _, _) = select.select([self.sock], [], [], tvo)
995            except select.error:
996                raise ControlException(SERR_SELECT)
997            warndbg("At %s, select with timeout %d ends"
998                    % (time.asctime(), tvo), 5)
999
1000            if not rd:
1001                # Timed out.  Return what we have
1002                if not fragments:
1003                    if timeo:
1004                        raise ControlException(SERR_TIMEOUT)
1005                if timeo:
1006                    if (self.debug >= 1) and \
1007                       (self.logfp is not None):  # pragma: no cover
1008                        # special, not replacing with dolog()
1009                        self.logfp.write(
1010                            "ERR_INCOMPLETE: Received fragments:\n")
1011                        for (i, frag) in enumerate(fragments):
1012                            self.logfp.write("%d: %s" % (i+1, frag.stats()))
1013                        self.logfp.write("last fragment %sreceived\n"
1014                                         % ("not ", "")[seenlastfrag])
1015                raise ControlException(SERR_INCOMPLETE)
1016
1017            warndbg("At %s, socket read begins" % time.asctime(), 4)
1018            try:
1019                rawdata = ntp.poly.polybytes(self.sock.recv(4096))
1020            except socket.error:  # pragma: no cover
1021                # usually, errno 111: connection refused
1022                raise ControlException(SERR_SOCKET)
1023
1024            if self.flakey and self.flakey >= random.random():
1025                warndbg('Flaky: I deliberately dropped a packet.', 1)
1026                rawdata = None
1027
1028            warndbg("Received %d octets" % len(rawdata), 3)
1029            rpkt = ControlPacket(self)
1030            try:
1031                rpkt.analyze(rawdata)
1032            except struct.error:
1033                raise ControlException(SERR_UNSPEC)
1034
1035            # Validate that packet header is sane, and the correct type
1036            valid = self.__validate_packet(rpkt, rawdata, opcode, associd)
1037            if not valid:  # pragma: no cover
1038                continue
1039
1040            # Someday, perhaps, check authentication here
1041            if self._authpass and self.auth:
1042                _pend = rpkt.count + MODE_SIX_HEADER_LENGTH
1043                _pend += (-_pend % MODE_SIX_ALIGNMENT)
1044                if len(rawdata) < (_pend + KEYID_LENGTH + MINIMUM_MAC_LENGTH):
1045                    self.logfp.write('AUTH - packet too short for MAC %d < %d\n' %
1046                                     (len(rawdata), (_pend + KEYID_LENGTH + MINIMUM_MAC_LENGTH)))
1047                    self._authpass = False
1048                elif not self.auth.verify_mac(rawdata, packet_end=_pend,
1049                                            mac_begin=_pend):
1050                    self._authpass = False
1051
1052            # Clip off the MAC, if any
1053            rpkt.extension = rpkt.extension[:rpkt.count]
1054
1055            if rpkt.count == 0 and rpkt.more():
1056                warn("Received count of 0 in non-final fragment\n")
1057                continue
1058
1059            if seenlastfrag and rpkt.more():  # pragma: no cover
1060                # I'n not sure this can be triggered without hitting another
1061                # error first.
1062                warn("Received second last fragment\n")
1063                continue
1064
1065            # Find the most recent fragment with a
1066            not_earlier = [frag for frag in fragments
1067                           if frag.offset >= rpkt.offset]
1068            if not_earlier:
1069                not_earlier = not_earlier[0]
1070                if not_earlier.offset == rpkt.offset:
1071                    warn("duplicate %d octets at %d ignored, prior "
1072                         " %d at %d\n"
1073                         % (rpkt.count, rpkt.offset,
1074                            not_earlier.count, not_earlier.offset))
1075                    continue
1076
1077            if fragments:
1078                last = fragments[-1]
1079                if last.end() > rpkt.offset:
1080                    warn("received frag at %d overlaps with %d octet "
1081                         "frag at %d\n"
1082                         % (rpkt.offset, last.count, last.offset))
1083                    continue
1084
1085            if not_earlier and rpkt.end() > not_earlier.offset:
1086                warn("received %d octet frag at %d overlaps with "
1087                     "frag at %d\n"
1088                     % (rpkt.count, rpkt.offset, not_earlier.offset))
1089                continue
1090
1091            warndbg("Recording fragment %d, size = %d offset = %d, "
1092                    " end = %d, more=%s"
1093                    % (len(fragments)+1, rpkt.count,
1094                       rpkt.offset, rpkt.end(), rpkt.more()), 3)
1095
1096            # Passed all tests, insert it into the frag list.
1097            fragments.append(rpkt)
1098            fragments.sort(key=lambda frag: frag.offset)
1099
1100            # Figure out if this was the last.
1101            # Record status info out of the last packet.
1102            if not rpkt.more():
1103                seenlastfrag = True
1104                self.rstatus = rpkt.status
1105
1106            # If we've seen the last fragment, look for holes in the sequence.
1107            # If there aren't any, we're done.
1108            if seenlastfrag and fragments[0].offset == 0:
1109                for f in range(1, len(fragments)):
1110                    if fragments[f-1].end() != fragments[f].offset:
1111                        warndbg("Hole in fragment sequence, %d of %d"
1112                                % (f, len(fragments)), 1)
1113                        break
1114                else:
1115                    tempfraglist = [ntp.poly.polystr(f.extension) \
1116                                    for f in fragments]
1117                    self.response = ntp.poly.polybytes("".join(tempfraglist))
1118                    warndbg("Fragment collection ends. %d bytes "
1119                            " in %d fragments"
1120                            % (len(self.response), len(fragments)), 1)
1121                    # special loggers, not replacing with dolog()
1122                    if self.debug >= 5:  # pragma: no cover
1123                        warn("Response packet:\n")
1124                        dump_hex_printable(self.response, self.logfp)
1125                    elif self.debug >= 3:  # pragma: no cover
1126                        # FIXME: Garbage when retrieving assoc list (binary)
1127                        warn("Response packet:\n%s\n" % repr(self.response))
1128                    elif self.debug >= 2:  # pragma: no cover
1129                        # FIXME: Garbage when retrieving assoc list (binary)
1130                        eol = self.response.find(b"\n")
1131                        firstline = self.response[:eol]
1132                        warn("First line:\n%s\n" % repr(firstline))
1133                    return None
1134                break
1135        if not self._authpass:
1136            warn('AUTH: Content untrusted due to authentication failure!\n')
1137
1138    def __validate_packet(self, rpkt, rawdata, opcode, associd):
1139        # TODO: refactor to simplify while retaining semantic info
1140        if self.logfp is not None:
1141            warn = self.logfp.write
1142        else:
1143            warn = (lambda x: x)
1144        warndbg = (lambda txt, th: ntp.util.dolog(self.logfp, txt,
1145                                                  self.debug, th))
1146
1147        if ((rpkt.version() > ntp.magic.NTP_VERSION) or
1148           (rpkt.version() < ntp.magic.NTP_OLDVERSION)):
1149            warndbg("Fragment received with version %d"
1150                    % rpkt.version(), 1)
1151            return False
1152        if rpkt.mode() != ntp.magic.MODE_CONTROL:
1153            warndbg("Fragment received with mode %d" % rpkt.mode(), 1)
1154            return False
1155        if not rpkt.is_response():
1156            warndbg("Received request, wanted response", 1)
1157            return False
1158
1159        # Check opcode and sequence number for a match.
1160        # Could be old data getting to us.
1161        if rpkt.sequence != self.sequence:
1162            warndbg("Received sequence number %d, wanted %d" %
1163                    (rpkt.sequence, self.sequence), 1)
1164            return False
1165        if rpkt.opcode() != opcode:
1166            warndbg("Received opcode %d, wanted %d" %
1167                    (rpkt.opcode(), opcode), 1)
1168            return False
1169
1170        # Check the error code.  If non-zero, return it.
1171        if rpkt.is_error():
1172            if rpkt.more():
1173                warn("Error %d received on non-final fragment\n"
1174                     % rpkt.errcode())
1175            self.keyid = self.passwd = None
1176            raise ControlException(
1177                SERR_SERVER
1178                % ControlSession.server_errors[rpkt.errcode()],
1179                rpkt.errcode())
1180
1181        # Check the association ID to make sure it matches what we expect
1182        if rpkt.associd != associd:
1183            warn("Association ID %d doesn't match expected %d\n"
1184                 % (rpkt.associd, associd))
1185
1186        # validate received payload size is padded to next 32-bit
1187        # boundary and no smaller than claimed by rpkt.count
1188        if len(rawdata) & 0x3:
1189            warn("Response fragment not padded, size = %d\n"
1190                 % len(rawdata))
1191            return False
1192
1193        shouldbesize = (ControlPacket.HEADER_LEN + rpkt.count + 3) & ~3
1194        if len(rawdata) < shouldbesize:
1195            warn("Response fragment claims %u octets payload, "
1196                 "above %d received\n"
1197                 % (rpkt.count, len(rawdata) - ControlPacket.HEADER_LEN))
1198            raise ControlException(SERR_INCOMPLETE)
1199
1200        return True
1201
1202    def doquery(self, opcode, associd=0, qdata="", auth=False):
1203        "send a request and save the response"
1204        if not self.havehost():
1205            raise ControlException(SERR_NOHOST)
1206        retry = True
1207        while True:
1208            # Ship the request
1209            self.sendrequest(opcode, associd, qdata, auth)
1210            # Get the response.
1211            try:
1212                res = self.getresponse(opcode, associd, not retry)
1213            except ControlException as e:
1214                if retry and e.message in (SERR_TIMEOUT, SERR_INCOMPLETE):
1215                    retry = False
1216                    continue
1217                else:
1218                    raise e
1219            break
1220        # Return data on success
1221        return res
1222
1223    def readstat(self, associd=0):
1224        "Read peer status, or throw an exception."
1225        self.doquery(opcode=ntp.control.CTL_OP_READSTAT, associd=associd)
1226        if len(self.response) % 4:
1227            raise ControlException(SERR_BADLENGTH)
1228        idlist = []
1229        if associd == 0:
1230            for i in range(len(self.response)//4):
1231                data = self.response[4*i:4*i+4]
1232                (associd, status) = struct.unpack("!HH", data)
1233                idlist.append(Peer(self, associd, status))
1234        idlist.sort(key=lambda a: a.associd)
1235        return idlist
1236
1237    def __parse_varlist(self, raw=False):
1238        "Parse a response as a textual varlist."
1239        # Strip out NULs and binary garbage from text;
1240        # ntpd seems prone to generate these, especially
1241        # in reslist responses.
1242        kvpairs = []
1243        instring = False
1244        response = ""
1245        self.response = ntp.poly.polystr(self.response)
1246        for c in self.response:
1247            cord = ntp.poly.polyord(c)
1248            if c == '"':
1249                response += c
1250                instring = not instring
1251            elif not instring and c == ",":
1252                # Separator between key=value pairs, done with this pair
1253                kvpairs.append(response.strip())
1254                response = ""
1255            elif 0 < cord < 127:
1256                # if it isn't a special case or garbage, add it
1257                response += c
1258        if response:  # The last item won't be caught by the loop
1259            kvpairs.append(response.strip())
1260        items = []
1261        for pair in kvpairs:
1262            if "=" in pair:
1263                key, value = ntp.util.slicedata(pair, pair.index("="))
1264                value = value[1:]  # Remove '='
1265            else:
1266                key, value = pair, ""
1267            key, value = key.strip(), value.strip()
1268            # Start trying to cast to non-string types
1269            if value:
1270                try:
1271                    castedvalue = int(value, 0)
1272                except ValueError:
1273                    try:
1274                        castedvalue = float(value)
1275                        if key == "delay" and not raw:
1276                            # Hack for non-raw-mode to get precision
1277                            items.append(("delay-s", value))
1278                    except ValueError:
1279                        if (value[0] == '"') and (value[-1] == '"'):
1280                            value = value[1:-1]
1281                        castedvalue = value  # str / unknown, stillneed casted
1282            else:  # no value
1283                castedvalue = value
1284            if raw:
1285                items.append((key, (castedvalue, value)))
1286            else:
1287                items.append((key, castedvalue))
1288        return ntp.util.OrderedDict(items)
1289
1290    def readvar(self, associd=0, varlist=None,
1291                opcode=ntp.control.CTL_OP_READVAR, raw=False):
1292        "Read system vars from the host as a dict, or throw an exception."
1293        if varlist is None:
1294            qdata = ""
1295        else:
1296            qdata = ",".join(varlist)
1297        self.doquery(opcode, associd=associd, qdata=qdata)
1298        return self.__parse_varlist(raw)
1299
1300    def config(self, configtext):
1301        "Send configuration text to the daemon. Return True if accepted."
1302        self.doquery(opcode=ntp.control.CTL_OP_CONFIGURE,
1303                     qdata=configtext, auth=True)
1304        # Copes with an implementation error - ntpd uses putdata without
1305        # setting the size correctly.
1306        if not self.response:
1307            raise ControlException(SERR_PERMISSION)
1308        elif b"\x00" in self.response:
1309            self.response = self.response[:self.response.index(b"\x00")]
1310        self.response = self.response.rstrip()
1311        return self.response == ntp.poly.polybytes("Config Succeeded")
1312
1313    def fetch_nonce(self):
1314        """
1315Ask for, and get, a nonce that can be replayed.
1316This combats source address spoofing
1317"""
1318        for i in range(4):
1319            # retry 4 times
1320            self.doquery(opcode=ntp.control.CTL_OP_REQ_NONCE)
1321            self.nonce_xmit = time.time()
1322            if self.response.startswith(ntp.poly.polybytes("nonce=")):
1323                return ntp.poly.polystr(self.response.strip())
1324            # maybe a delay between tries?
1325
1326        # uh, oh, no nonce seen
1327        # this print probably never can be seen...
1328        if str is bytes:
1329            resp = self.response
1330        else:
1331            resp = self.response.decode()
1332        self.logfp.write("## Nonce expected: %s" % resp)
1333        raise ControlException(SERR_BADNONCE)
1334
1335    def __mru_analyze(self, variables, span, direct):
1336        """Extracts data from the key/value list into a more useful form"""
1337        mru = None
1338        nonce = None
1339        items = list(variables.items())
1340        fake_list = []
1341        fake_dict = {}
1342        if items:                   # See issue #642
1343            items.sort(key=mru_kv_key)
1344        for (tag, val) in items:
1345            self.warndbg("tag=%s, val=%s" % (tag, val), 4)
1346            if tag == "nonce":
1347                nonce = "%s=%s" % (tag, val)
1348            elif tag == "last.older":
1349                continue
1350            elif tag == "addr.older":
1351                continue
1352            if tag == "now":
1353                # finished marker
1354                span.now = ntp.ntpc.lfptofloat(val)
1355                continue
1356            elif tag == "last.newest":
1357                # more finished
1358                continue
1359            for prefix in ("addr", "last", "first", "ct", "mv", "rs", "sc", "dr"):
1360                if tag.startswith(prefix + "."):
1361                    (member, idx) = tag.split(".")
1362                    try:
1363                        idx = int(idx)
1364                    except ValueError:
1365                        raise ControlException(SERR_BADTAG % tag)
1366                    ### Does not check missing/gappy entries
1367                    if idx not in fake_list:
1368                        fake_dict[str(idx)] = {}
1369                        fake_list.append(idx)
1370                    fake_dict[str(idx)][member] = val
1371        fake_list.sort()
1372        for idx in fake_list:
1373            mru = MRUEntry()
1374            self.slots += 1
1375            # Always 6 in practice, in the tests not so much
1376#            if len(fake_dict[str(idx)]) != 6:
1377#                continue
1378            for prefix in ("addr", "last", "first", "ct", "mv", "rs", "sc", "dr"):
1379                if prefix in fake_dict[str(idx)]:  # dodgy test needs this line
1380                    setattr(mru, prefix, fake_dict[str(idx)][prefix])
1381            span.entries.append(mru)
1382        if direct is not None:
1383            direct(span.entries)
1384        return nonce
1385
1386    def __mru_query_error(self, e, restarted_count, cap_frags, limit, frags):
1387        if e.errorcode is None:
1388            raise e
1389        elif e.errorcode == ntp.control.CERR_UNKNOWNVAR:
1390            # None of the supplied prior entries match, so
1391            # toss them from our list and try again.
1392            self.warndbg("no overlap between prior entries and "
1393                         "server MRU list", 1)
1394            restarted_count += 1
1395            if restarted_count > 8:
1396                raise ControlException(SERR_STALL)
1397            self.warndbg("--->   Restarting from the beginning, "
1398                         "retry #%u" % restarted_count, 1)
1399        elif e.errorcode == ntp.control.CERR_BADVALUE:
1400            if cap_frags:
1401                cap_frags = False
1402                self.warndbg("Reverted to row limit from "
1403                             "fragments limit.", 1)
1404            else:
1405                # ntpd has lower cap on row limit
1406                self.ntpd_row_limit -= 1
1407                limit = min(limit, self.ntpd_row_limit)
1408                self.warndbg("Row limit reduced to %d following "
1409                             "CERR_BADVALUE." % limit, 1)
1410        elif e.errorcode in (SERR_INCOMPLETE, SERR_TIMEOUT):
1411            # Reduce the number of rows/frags requested by
1412            # half to recover from lost response fragments.
1413            if cap_frags:
1414                frags = max(2, frags / 2)
1415                self.warndbg("Frag limit reduced to %d following "
1416                             "incomplete response." % frags, 1)
1417            else:
1418                limit = max(2, limit / 2)
1419                self.warndbg("Row limit reduced to %d following "
1420                             " incomplete response." % limit, 1)
1421        elif e.errorcode:
1422            raise e
1423        return restarted_count, cap_frags, limit, frags
1424
1425    def mrulist(self, variables=None, rawhook=None, direct=None):
1426        "Retrieve MRU list data"
1427        restarted_count = 0
1428        cap_frags = True
1429        sorter = None
1430        sortkey = None
1431        frags = MAXFRAGS
1432        if variables is None:
1433            variables = {}
1434
1435        if variables:
1436            sorter, sortkey, frags = parse_mru_variables(variables)
1437
1438        nonce = self.fetch_nonce()
1439
1440        span = MRUList()
1441        try:
1442            # Form the initial request
1443            limit = min(3 * MAXFRAGS, self.ntpd_row_limit)
1444            req_buf = "%s, frags=%d" % (nonce, frags)
1445            if variables:
1446                if 'resall' in variables:
1447                    variables['resall'] = hex(variables['resall'])
1448                if 'resany' in variables:
1449                    variables['resany'] = hex(variables['resany'])
1450            parms, firstParms = generate_mru_parms(variables)
1451            req_buf += firstParms
1452
1453            while True:
1454                # Request additions to the MRU list
1455                try:
1456                    self.doquery(opcode=ntp.control.CTL_OP_READ_MRU,
1457                                 qdata=req_buf)
1458                    recoverable_read_errors = False
1459                except ControlException as e:
1460                    recoverable_read_errors = True
1461                    res = self.__mru_query_error(e, restarted_count,
1462                                                 cap_frags, limit, frags)
1463                    restarted_count, cap_frags, limit, frags = res
1464
1465                # Parse the response
1466                variables = self.__parse_varlist()
1467
1468                # Comment from the C code:
1469                # This is a cheap cop-out implementation of rawmode
1470                # output for mrulist.  A better approach would be to
1471                # dump similar output after the list is collected by
1472                # ntpq with a continuous sequence of indexes.  This
1473                # cheap approach has indexes resetting to zero for
1474                # each query/response, and duplicates are not
1475                # coalesced.
1476                if rawhook:
1477                    rawhook(variables)
1478
1479                # Analyze the contents of this response into a span structure
1480                newNonce = self.__mru_analyze(variables, span, direct)
1481                if newNonce:
1482                    nonce = newNonce
1483
1484                # If we've seen the end sentinel on the span, break out
1485                if span.is_complete():
1486                    break
1487
1488                # The C version of ntpq used to snooze for a bit
1489                # between MRU queries to let ntpd catch up with other
1490                # duties.  It turns out this is quite a bad idea.  Above
1491                # a certain traffic threshold, servers accumulate MRU records
1492                # faster than this protocol loop can capture them such that
1493                # you never get a complete span.  The last thing you want to
1494                # do when trying to keep up with a high-traffic server is stall
1495                # in the read loop.
1496                # time.sleep(0.05)
1497
1498                # If there were no errors, increase the number of rows
1499                # to a maximum of 3 * MAXFRAGS (the most packets ntpq
1500                # can handle in one response), on the assumption that
1501                # no less than 3 rows fit in each packet, capped at
1502                # our best guess at the server's row limit.
1503                if not recoverable_read_errors:
1504                    if cap_frags:
1505                        frags = min(MAXFRAGS, frags + 1)
1506                    else:
1507                        limit = min(3 * MAXFRAGS,
1508                                    self.ntpd_row_limit,
1509                                    max(limit + 1,
1510                                        limit * 33 / 32))
1511
1512                # Prepare next query with as many address and last-seen
1513                # timestamps as will fit in a single packet.  A new nonce
1514                # might be required.
1515                if time.time() - self.nonce_xmit >= ntp.control.NONCE_TIMEOUT:
1516                    nonce = self.fetch_nonce()
1517                req_buf = "%s, %s=%d%s" % \
1518                          (nonce,
1519                           "frags" if cap_frags else "limit",
1520                           frags if cap_frags else limit,
1521                           parms)
1522                req_buf += generate_mru_lastseen(span, len(req_buf))
1523                if direct is not None:
1524                    span.entries = []
1525        except KeyboardInterrupt:  # pragma: no cover
1526            pass        # We can test for interruption with is_complete()
1527
1528        stitch_mru(span, sorter, sortkey)
1529        return span
1530
1531    def __ordlist(self, listtype):
1532        "Retrieve ordered-list data."
1533        self.doquery(opcode=ntp.control.CTL_OP_READ_ORDLIST_A,
1534                     qdata=listtype, auth=True)
1535        stanzas = []
1536        for (key, value) in self.__parse_varlist().items():
1537            if key[-1].isdigit() and '.' in key:
1538                (stem, stanza) = key.split(".")
1539                stanza = int(stanza)
1540                if stanza > len(stanzas) - 1:
1541                    for i in range(len(stanzas), stanza + 1):
1542                        stanzas.append(ntp.util.OrderedDict())
1543                stanzas[stanza][stem] = value
1544        return stanzas
1545
1546    def reslist(self):
1547        "Retrieve reslist data."
1548        return self.__ordlist("addr_restrictions")
1549
1550    def ifstats(self):
1551        "Retrieve ifstats data."
1552        return self.__ordlist("ifstats")
1553
1554
1555def parse_mru_variables(variables):
1556    sorter = None
1557    sortkey = None
1558    frags = MAXFRAGS
1559    if "sort" in variables:
1560        sortkey = variables["sort"]
1561        del variables["sort"]
1562        # Slots are retrieved oldest first.
1563        # Slots are printed in reverse so the normal/no-sort
1564        #  case prints youngest first.
1565        # That means sort functions are backwards.
1566        # Note lstint is backwards again (aka normal/forward)
1567        #  since we really want to sort on now-last rather than last.
1568        sortdict = {
1569            # lstint ascending
1570            "lstint": lambda e: ntp.ntpc.lfptofloat(e.last),
1571            # lstint descending
1572            "-lstint": lambda e: -ntp.ntpc.lfptofloat(e.last),
1573            # avgint ascending
1574            "avgint": lambda e: -e.avgint(),
1575            # avgint descending
1576            "-avgint": lambda e: e.avgint(),
1577            # IPv4 asc. then IPv6 asc.
1578            "addr": lambda e: e.sortaddr(),
1579            # IPv6 desc. then IPv4 desc.
1580            "-addr": lambda e: e.sortaddr(),
1581            # hit count ascending
1582            "count": lambda e: -e.ct,
1583            # hit count descending
1584            "-count": lambda e: e.ct,
1585            # score ascending
1586            "score": lambda e: -e.sc,
1587            # score descending
1588            "-score": lambda e: e.sc,
1589            # drop count ascending
1590            "drop": lambda e: -e.dr,
1591            # drop count descending
1592            "-drop": lambda e: e.dr,
1593        }
1594        if sortkey == "lstint":
1595            sortkey = None   # normal/default case, no need to sort
1596        if sortkey is not None:
1597            sorter = sortdict.get(sortkey)
1598            if sorter is None:
1599                raise ControlException(SERR_BADSORT % sortkey)
1600    for k in list(variables.keys()):
1601        if k in ("mincount", "mindrop", "minscore",
1602                 "resall", "resany", "kod", "limited",
1603                 "maxlstint", "minlstint", "laddr", "recent",
1604                 "sort", "frags", "limit"):
1605            continue
1606        elif k.startswith('addr.') or k.startswith('last.'):
1607            kn = k.split('.')
1608            if len(kn) != 2 or kn[1] not in map(str, list(range(16))):
1609                raise ControlException(SERR_BADPARAM % k)
1610            continue
1611        else:
1612            raise ControlException(SERR_BADPARAM % k)
1613    if 'frags' in variables:
1614        frags = int(variables.get('frags'))
1615        del variables['frags']
1616    if 'kod' in variables:
1617        variables['resany'] = variables.get('resany', 0) \
1618                              | ntp.magic.RES_KOD
1619        del variables['kod']
1620    if 'limited' in variables:
1621        variables['resany'] = variables.get('resany', 0) \
1622                              | ntp.magic.RES_LIMITED
1623        del variables['limited']
1624    return sorter, sortkey, frags
1625
1626
1627def stitch_mru(span, sorter, sortkey):
1628    # C ntpq's code for stitching together spans was absurdly
1629    # overelaborate - all that dancing with last.older and
1630    # addr.older was, as far as I can tell, just pointless.
1631    # Much simpler to just run through the final list throwing
1632    # out every entry with an IP address that is duplicated
1633    # with a later most-recent-transmission time.
1634    addrdict = {}
1635    deletia = []
1636    for (i, entry) in enumerate(span.entries):
1637        if entry.addr not in addrdict:
1638            addrdict[entry.addr] = []
1639        addrdict[entry.addr].append((i, entry.last))
1640    for addr in addrdict:
1641        deletia += sorted(addrdict[addr], key=lambda x: x[1])[:-1]
1642    deletia = [x[0] for x in deletia]
1643    deletia.sort(reverse=True)  # Delete from top down so indices don't change
1644    for i in deletia:
1645        span.entries.pop(i)
1646
1647    # Sort for presentation
1648    if sorter:
1649        span.entries.sort(key=sorter)
1650        if sortkey == "addr":
1651            # I don't know how to feed a minus sign to text sort
1652            span.entries.reverse()
1653
1654
1655def generate_mru_parms(variables):
1656    if not variables:
1657        return "", ""
1658    # generate all sans recent
1659    parmStrs = [("%s=%s" % it)
1660                for it in list(variables.items()) if (it[0] != "recent")]
1661    parms = ", " + ", ".join(parmStrs)
1662    # Only ship 'recent' on the first request
1663    if "recent" in variables:
1664        firstParms = ", recent=%s" % variables["recent"]
1665        firstParms += parms
1666    else:
1667        firstParms = parms
1668    return parms, firstParms
1669
1670
1671def generate_mru_lastseen(span, existingBufferSize):
1672    buf = ""
1673    for i in range(len(span.entries)):
1674        e = span.entries[len(span.entries) - i - 1]
1675        incr = ", addr.%d=%s, last.%d=%s" % (i, e.addr, i, e.last)
1676        if (existingBufferSize + len(buf) + len(incr) >=
1677                ntp.control.CTL_MAX_DATA_LEN):
1678            break
1679        else:
1680            buf += incr
1681    return buf
1682
1683
1684def mru_kv_key(token):
1685    bits = token[0].split('.')
1686    if len(bits) == 1:
1687        return -2
1688    try:
1689        return int(bits[1])
1690    except ValueError:
1691        return -1
1692
1693
1694class Authenticator:
1695    "MAC authentication manager for NTP packets."
1696
1697    def __init__(self, keyfile=None):
1698        # We allow I/O and permission errors upward deliberately
1699        self.passwords = {}
1700        if keyfile is not None:
1701            for line in open(keyfile):
1702                if '#' in line:
1703                    line = line[:line.index("#")]
1704                line = line.strip()
1705                if not line:
1706                    continue
1707                (keyid, keytype, passwd) = line.split()
1708                if keytype.upper() in ['AES', 'AES128CMAC']:
1709                    keytype = 'AES-128'
1710                if len(passwd) > 20:
1711                    # if len(passwd) > 64:
1712                        # print('AUTH: Truncating key %s to 256bits (32Bytes)' % keyid)
1713                    passwd = ntp.util.hexstr2octets(passwd[:64])
1714                self.passwords[int(keyid)] = (keytype, passwd)
1715
1716    def __len__(self):
1717        'return the number of keytype/passwd tuples stored'
1718        return len(self.passwords)
1719
1720    def __getitem__(self, keyid):
1721        'get a keytype/passwd tuple by keyid'
1722        return self.passwords.get(keyid)
1723
1724    def control(self, keyid=None):
1725        "Get the keytype/passwd tuple that controls localhost and its id"
1726        if keyid is not None:
1727            if keyid in self.passwords:
1728                return (keyid,) + self.passwords[keyid]
1729            else:
1730                return (keyid, None, None)
1731        for line in open("/etc/ntp.conf"):
1732            if line.startswith("control"):
1733                keyid = int(line.split()[1])
1734                (keytype, passwd) = self.passwords[keyid]
1735                if passwd is None:
1736                    # Invalid key ID
1737                    raise ValueError
1738                if len(passwd) > 20:
1739                    passwd = ntp.util.hexstr2octets(passwd)
1740                return (keyid, keytype, passwd)
1741        # No control lines found
1742        raise ValueError
1743
1744    @staticmethod
1745    def compute_mac(payload, keyid, keytype, passwd):
1746        'Create the authentication payload to send'
1747        if not ntp.ntpc.checkname(keytype):
1748            return False
1749        mac2 = ntp.ntpc.mac(ntp.poly.polybytes(payload),
1750                            ntp.poly.polybytes(passwd), keytype)
1751        if not mac2 or len(mac2) == 0:
1752            return b''
1753        return struct.pack("!I", keyid) + mac2
1754
1755    @staticmethod
1756    def have_mac(packet):
1757        "Does this packet have a MAC?"
1758        # According to RFC 5909 7.5 the MAC is always present when an extension
1759        # field is present. Note: this crude test will fail on Mode 6 packets.
1760        # On those you have to go in and look at the count.
1761        return len(packet) > ntp.magic.LEN_PKT_NOMAC
1762
1763    def verify_mac(self, packet, packet_end=48, mac_begin=48):
1764        "Does the MAC on this packet verify according to credentials we have?"
1765        payload = packet[:packet_end]
1766        keyid = packet[mac_begin:mac_begin+KEYID_LENGTH]
1767        mac = packet[mac_begin+KEYID_LENGTH:]
1768        (keyid,) = struct.unpack("!I", keyid)
1769        if keyid not in self.passwords:
1770            # print('AUTH: No key %08x...' % keyid)
1771            return False
1772        (keytype, passwd) = self.passwords[keyid]
1773        if not ntp.ntpc.checkname(keytype):
1774            return False
1775        mac2 = ntp.ntpc.mac(ntp.poly.polybytes(payload),
1776                            ntp.poly.polybytes(passwd), keytype)
1777        if not mac2:
1778            return False
1779        # typically preferred to avoid timing attacks client-side (in theory)
1780        try:
1781            return hmac.compare_digest(mac, mac2) # supported 2.7.7+ and 3.3+
1782        except AttributeError:
1783            return mac == mac2  # solves issue #666
1784
1785# end
1786