[Git][NTPsec/ntpsec][master] In pyntpq, reslist listing is working.

Eric S. Raymond gitlab at mg.gitlab.com
Thu Nov 3 17:18:17 UTC 2016


Eric S. Raymond pushed to branch master at NTPsec / ntpsec


Commits:
908fa090 by Eric S. Raymond at 2016-11-03T12:10:40-04:00
In pyntpq, reslist listing is working.

- - - - -


4 changed files:

- docs/mode6.txt
- ntpq/pyntpq
- pylib/packet.py
- pylib/util.py


Changes:

=====================================
docs/mode6.txt
=====================================
--- a/docs/mode6.txt
+++ b/docs/mode6.txt
@@ -29,7 +29,8 @@ include::includes/hand.txt[]
 This page describes the Mode 6 protocol used to get status information
 from a running ntpd and configure some of its behaviors on the fly.
 The protocol is normally used by the 'ntpq' program distributed with
-the suite.
+the suite. It is fully documented here so that other clients can be
+written.
 
 [[packet]]
 == Mode 6 packet structure ==
@@ -369,7 +370,7 @@ fly).
 This request is used for two purposes: to retrieve restriction lists
 and to retrieve interface statistics.  For the former use, the request
 payload should be the string "addr_restrictions"; for the latter case,
-the request payload should be be "ifstats" of empty.  Both uses
+the request payload should be be "ifstats" or empty.  Both uses
 require authentication.  The response payload is, in both cases, a
 textual varlist.
 
@@ -483,6 +484,18 @@ header fields, then the payload.
 The cryptographic hash is normally MD5, but if ntpd is built with
 OpenSSL support it is possible to use and generate SHA1 keys as well.
 
+== Known bugs ==
+
+The quality of legacy ntpd implementations of this protocol is poor.
+Clients should be prepared for the following quirks:
+
+* In textual varlists, value literals may have spurious trailing NULs
+  which must be stripped.
+
+* In the reslist response, tag names and values may be corrupted by
+  inserted binary garbage.  Any octet with the 0x80 bit set should
+  be discarded.
+
 '''''
 
 include::includes/footer.txt[]


=====================================
ntpq/pyntpq
=====================================
--- a/ntpq/pyntpq
+++ b/ntpq/pyntpq
@@ -1253,7 +1253,7 @@ usage: ifstats
             self.say(ReslistSummary.header)
             self.say(("=" * ReslistSummary.width) + "\n")
             for entry in entries:
-                self.say(formatter.summary(entry) + "\n")
+                self.say(formatter.summary(entry))
         except Mode6Exception as e:
             self.warn(e.message + "\n")
             return


=====================================
pylib/packet.py
=====================================
--- a/pylib/packet.py
+++ b/pylib/packet.py
@@ -690,18 +690,19 @@ class Mode6Session:
 
     def __parse_varlist(self):
         "Parse a response as a textual varlist."
-        # Trim trailing NULs from the text
-        response = self.response
-        while response.endswith(b"\x00"):
-            response = response[:-1]
+        # Strip out NULs and binary garbage from text;
+        # ntpd seems prone to generate these, especially
+        # in reslist responses.
+        response = ""
+        for c in self.response:
+            if ord(c) > 0 and ord(c) < 127:
+                response += c
         response = response.rstrip()
         items = []
         if response:
             for pair in response.split(","):
                 try:
-                    # Yes, some servers seem to ship embedded NULs.
-                    while pair.endswith(b"\x00"):
-                        pair = pair[:-1]
+                    pair = pair.strip()
                     eq = pair.index("=")
                     var = pair[:eq].strip()
                     val = pair[eq+1:].strip()
@@ -957,14 +958,23 @@ class Mode6Session:
     def __ordlist(self, listtype):
         "Retrieve ordered-list data."
         self.doquery(opcode=CTL_OP_READ_ORDLIST_A, qdata=listtype, auth=True)
-        return self.response
+        stanzas = []
+        for (key, value) in self.__parse_varlist().items():
+            if key[-1].isdigit() and key[-2] == '.':
+                (stem, stanza) = key.split(".")
+                stanza = int(stanza)
+                if stanza > len(stanzas) - 1:
+                    for i in range(len(stanzas), stanza + 1):
+                        stanzas.append(collections.OrderedDict())
+                stanzas[stanza][stem] = value
+        return stanzas
 
     def reslist(self):
         "Retrieve reslist data."
-        print(self.ordlist("addr_restrictions"))
+        return self.__ordlist("addr_restrictions")
 
     def ifstats(self):
         "Retrieve ifstats data."
-        print(self.ordlist("ifstats"))
+        print(self.__ordlist("ifstats"))
 
 # end


=====================================
pylib/util.py
=====================================
--- a/pylib/util.py
+++ b/pylib/util.py
@@ -7,6 +7,7 @@ import sys
 import subprocess
 import time
 import ntp.ntpc
+import re
 
 from ntp.packet import *
 
@@ -25,6 +26,11 @@ def portsplit(hostname):
     return (hostname, portsuffix)
 
 def canonicalize_dns(hostname):
+    "Canonicalize a hostname or numeric IP address."
+    # Catch garbaged hostnames in corrupted Mode 6 responses
+    m = re.match("([:.[\]]|\w)*", hostname)
+    if not m:
+        raise TypeError
     (hostname, portsuffix) = portsplit(hostname)
     try:
         ai = socket.getaddrinfo(hostname, None, 0, 0, 0, socket.AI_CANONNAME)
@@ -217,7 +223,10 @@ class PeerSummary:
         if srchost != None:
             clock_name = srchost
         elif self.showhostnames:
-            clock_name = canonicalize_dns(srcadr)
+            try:
+                clock_name = canonicalize_dns(srcadr)
+            except TypeError:
+                return ''
         else:
             clock_name = srcadr
         if self.wideremote and len(clock_name) > self.namewidth:
@@ -244,15 +253,19 @@ class PeerSummary:
         last_sync = variables.get("rec") or variables.get("reftime")
         jd = estjitter if have_jitter else estdisp
         jd = "      -" if jd >= 999 else ("%7.3f" % jd)
-        line += (
-            " %2ld %c %4.4s %4.4s  %3lo  %7.3f %8.3f %s\n" % \
-            (variables.get("stratum", 0),
-             ptype,
-             PeerSummary.prettyinterval(now if last_sync is None else int(now - ntp.ntpc.lfptofloat(last_sync))),
-             PeerSummary.prettyinterval(poll_sec),
-             reach, estdelay, estoffset,
-             jd))
-        return line
+        try:
+            line += (
+                " %2ld %c %4.4s %4.4s  %3lo  %7.3f %8.3f %s\n" % \
+                (variables.get("stratum", 0),
+                 ptype,
+                 PeerSummary.prettyinterval(now if last_sync is None else int(now - ntp.ntpc.lfptofloat(last_sync))),
+                 PeerSummary.prettyinterval(poll_sec),
+                 reach, estdelay, estoffset,
+                 jd))
+            return line
+        except TypeError:
+            # This can happen when ntpd ships a corrupt varlist
+            return ''
 
 class MRUSummary:
     "Reusable class for MRU entry summary generation."
@@ -281,11 +294,53 @@ class MRUSummary:
         else:
             rscode = '.'
         (dns, port) = portsplit(entry.addr)
-        if self.showhostnames:
-            dns = canonicalize_dns(dns)
-        stats += " %4hx %c %d %d %6d %5s %s" % \
-                 (entry.rs, rscode, PKT_MODE(entry.mv), PKT_VERSION(entry.mv),
-                  entry.ct, port[1:], dns)
-        return stats
+        try:
+            if self.showhostnames:
+                dns = canonicalize_dns(dns)
+            stats += " %4hx %c %d %d %6d %5s %s" % \
+                     (entry.rs, rscode,
+                      PKT_MODE(entry.mv), PKT_VERSION(entry.mv),
+                      entry.ct, port[1:], dns)
+            return stats
+        except TypeError:
+            # This can happen when ntpd ships a corrupt varlist
+            return ''
+
+class ReslistSummary:
+    "Reusable class for reslist entry summary generation."
+    header = """\
+   hits    addr/prefix or addr mask
+           restrictions
+"""
+    width = 72
+    @staticmethod
+    def __getPrefix(mask):
+        if not mask:
+            prefix = ''
+        if ':' in mask:
+            sep = ':'
+            base = 16
+        else:
+            sep = '.'
+            base = 10
+        prefix = sum([bin(int(x, base)).count('1') for x in mask.split(sep) if x])
+        return '/' + str(prefix)
+    def summary(self, variables):
+        hits = variables.get("hits", "?")
+        address = variables.get("addr", "?")
+        mask = variables.get("mask", "?")
+        if address == '?' or mask == '?':
+            return ''
+        address += ReslistSummary.__getPrefix(mask)
+        flags = variables.get("flags", "?")
+        # reslist reponses are often corrupted
+        s = "%10s %s\n           %s\n" % (hits, address, flags)
+        # Throw away corrupted entries.  This is a shim - we really
+        # want to make ntpd stop generating garbage
+        for c in s:
+            if not c.isalnum() and not c in "/.: \n":
+                print("Failed on %s" % repr(c))
+                return ''
+        return s
 
 # end



View it on GitLab: https://gitlab.com/NTPsec/ntpsec/commit/908fa0903b43e3af0e020ec4e3581d593570e9bc
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.ntpsec.org/pipermail/vc/attachments/20161103/f430bbfb/attachment.html>


More information about the vc mailing list