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