]> git.ipfire.org Git - thirdparty/dnspython.git/commitdiff
Improve TTL detection when reading master zone files. 255/head
authorTom Lanyon <tomlanyon@google.com>
Tue, 30 May 2017 07:19:20 +0000 (17:19 +1000)
committerTom Lanyon <tomlanyon@google.com>
Wed, 31 May 2017 01:50:21 +0000 (11:50 +1000)
This introduces the same behaviour that BIND has when it encounters implicit TTL
values, namely:
  * The $TTL directive is preferred to set the default TTL [RFC2308].
  * If no $TTL directive is seen prior to the SOA RR, the default TTL is set
    to the minimum TTL field of the SOA RR [RFC1033, RFC1035, RFC1912].
  * If neither $TTL nor an SOA are present, use the last seen explicit TTL on an
    RR or raise a SyntaxError if no explicit TTLs have been seen.

Previously, when neither $TTL nor SOA were present, the TTL for an RR without
an explicit TTL would be set to 0 which is known to be a somewhat dangerous
value and shouldn't be encouraged as a default.

One test seems to rely on this default TTL=0 behaviour whilst testing something
seemingly unrelated (RR starting with whitespace), so that test has been
updated to match this new expectation.

Fixes #254.

LICENSE
dns/zone.py
tests/test_zone.py

diff --git a/LICENSE b/LICENSE
index 2896ca974668e0283c647b9964e18c97dd20b72c..c0382e174681de5ceded6cd676deff736b9053bf 100644 (file)
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,7 @@
 ISC License
 
 Copyright (C) 2001-2003 Nominum, Inc.
+Copyright (C) Google Inc.
 
 Permission to use, copy, modify, and distribute this software and its
 documentation for any purpose with or without fee is hereby granted,
index 468618f67c9dbee5dcd54f5135099e799452566c..0cf268263022cd33b83aed476bd8748f6947efdf 100644 (file)
@@ -28,6 +28,7 @@ import dns.node
 import dns.rdataclass
 import dns.rdatatype
 import dns.rdata
+import dns.rdtypes.ANY.SOA
 import dns.rrset
 import dns.tokenizer
 import dns.ttl
@@ -589,8 +590,14 @@ class _MasterReader(object):
 
     @ivar tok: The tokenizer
     @type tok: dns.tokenizer.Tokenizer object
-    @ivar ttl: The default TTL
-    @type ttl: int
+    @ivar last_ttl: The last seen explicit TTL for an RR
+    @type last_ttl: int
+    @ivar last_ttl_known: Has last TTL been detected
+    @type last_ttl_known: bool
+    @ivar default_ttl: The default TTL from a $TTL directive or SOA RR
+    @type default_ttl: int
+    @ivar default_ttl_known: Has default TTL been detected
+    @type default_ttl_known: bool
     @ivar last_name: The last name read
     @type last_name: dns.name.Name object
     @ivar current_origin: The current origin
@@ -600,8 +607,8 @@ class _MasterReader(object):
     @ivar zone: the zone
     @type zone: dns.zone.Zone object
     @ivar saved_state: saved reader state (used when processing $INCLUDE)
-    @type saved_state: list of (tokenizer, current_origin, last_name, file)
-    tuples.
+    @type saved_state: list of (tokenizer, current_origin, last_name, file,
+    last_ttl, last_ttl_known, default_ttl, default_ttl_known) tuples.
     @ivar current_file: the file object of the $INCLUDed file being parsed
     (None if no $INCLUDE is active).
     @ivar allow_include: is $INCLUDE allowed?
@@ -618,7 +625,10 @@ class _MasterReader(object):
         self.tok = tok
         self.current_origin = origin
         self.relativize = relativize
-        self.ttl = 0
+        self.last_ttl = 0
+        self.last_ttl_known = False
+        self.default_ttl = 0
+        self.default_ttl_known = False
         self.last_name = self.current_origin
         self.zone = zone_factory(origin, rdclass, relativize=relativize)
         self.saved_state = []
@@ -659,11 +669,18 @@ class _MasterReader(object):
         # TTL
         try:
             ttl = dns.ttl.from_text(token.value)
+            self.last_ttl = ttl
+            self.last_ttl_known = True
             token = self.tok.get()
             if not token.is_identifier():
                 raise dns.exception.SyntaxError
         except dns.ttl.BadTTL:
-            ttl = self.ttl
+            if not (self.last_ttl_known or self.default_ttl_known):
+                raise dns.exception.SyntaxError("Missing default TTL value")
+            if self.default_ttl_known:
+                ttl = self.default_ttl
+            else:
+                ttl = self.last_ttl
         # Class
         try:
             rdclass = dns.rdataclass.from_text(token.value)
@@ -703,6 +720,13 @@ class _MasterReader(object):
             raise dns.exception.SyntaxError(
                 "caught exception %s: %s" % (str(ty), str(va)))
 
+        if not self.default_ttl_known and isinstance(rd, dns.rdtypes.ANY.SOA.SOA):
+            # The pre-RFC2308 and pre-BIND9 behavior inherits the zone default
+            # TTL from the SOA minttl if no $TTL statement is present before the
+            # SOA is parsed.
+            self.default_ttl = rd.minimum
+            self.default_ttl_known = True
+
         rd.choose_relativity(self.zone.origin, self.relativize)
         covers = rd.covers()
         rds = n.find_rdataset(rdclass, rdtype, covers, True)
@@ -778,11 +802,18 @@ class _MasterReader(object):
         # TTL
         try:
             ttl = dns.ttl.from_text(token.value)
+            self.last_ttl = ttl
+            self.last_ttl_known = True
             token = self.tok.get()
             if not token.is_identifier():
                 raise dns.exception.SyntaxError
         except dns.ttl.BadTTL:
-            ttl = self.ttl
+            if not (self.last_ttl_known or self.default_ttl_known):
+                raise dns.exception.SyntaxError("Missing default TTL value")
+            if self.default_ttl_known:
+                ttl = self.default_ttl
+            else:
+                ttl = self.last_ttl
         # Class
         try:
             rdclass = dns.rdataclass.from_text(token.value)
@@ -884,7 +915,10 @@ class _MasterReader(object):
                          self.current_origin,
                          self.last_name,
                          self.current_file,
-                         self.ttl) = self.saved_state.pop(-1)
+                         self.last_ttl,
+                         self.last_ttl_known,
+                         self.default_ttl,
+                         self.default_ttl_known) = self.saved_state.pop(-1)
                         continue
                     break
                 elif token.is_eol():
@@ -898,7 +932,8 @@ class _MasterReader(object):
                         token = self.tok.get()
                         if not token.is_identifier():
                             raise dns.exception.SyntaxError("bad $TTL")
-                        self.ttl = dns.ttl.from_text(token.value)
+                        self.default_ttl = dns.ttl.from_text(token.value)
+                        self.default_ttl_known = True
                         self.tok.get_eol()
                     elif c == u'$ORIGIN':
                         self.current_origin = self.tok.get_name()
@@ -923,7 +958,10 @@ class _MasterReader(object):
                                                  self.current_origin,
                                                  self.last_name,
                                                  self.current_file,
-                                                 self.ttl))
+                                                 self.last_ttl,
+                                                 self.last_ttl_known,
+                                                 self.default_ttl,
+                                                 self.default_ttl_known))
                         self.current_file = open(filename, 'r')
                         self.tok = dns.tokenizer.Tokenizer(self.current_file,
                                                            filename)
index 3c497a6380688308e351a26c1139c00ad1e90187..ce7caf0178ac61fc2eda69c6acd370d1894e6cd3 100644 (file)
@@ -76,6 +76,33 @@ ns1 1d1s a 10.0.0.1
 ns2 1w1D1h1m1S a 10.0.0.2
 """
 
+# No $TTL so default TTL for RRs should be inherited from SOA minimum TTL (
+# not from the last explicit RR TTL).
+ttl_from_soa_text = """$ORIGIN example.
+@ 1h soa foo bar 1 2 3 4 5
+@ 1h ns ns1
+@ 1h ns ns2
+ns1 1w1D1h1m1S a 10.0.0.2
+ns2 a 10.0.0.1
+"""
+
+# No $TTL and no SOA, so default TTL for RRs should be inherited from last
+# explicit RR TTL.
+ttl_from_last_text = """$ORIGIN example.
+@ 1h ns ns1
+@ 1h ns ns2
+ns1 a 10.0.0.1
+ns2 1w1D1h1m1S a 10.0.0.2
+"""
+
+# No $TTL and no SOA should raise SyntaxError as no TTL can be determined.
+no_ttl_text = """$ORIGIN example.
+@ ns ns1
+@ ns ns2
+ns1 a 10.0.0.1
+ns2 a 10.0.0.2
+"""
+
 no_soa_text = """$TTL 1h
 $ORIGIN example.
 @ ns ns1
@@ -442,6 +469,36 @@ class ZoneTestCase(unittest.TestCase):
         rds = n.get_rdataset(dns.rdataclass.IN, dns.rdatatype.A)
         self.failUnless(rds.ttl == 694861)
 
+    def testTTLFromSOA(self):
+        z = dns.zone.from_text(ttl_from_soa_text, 'example.', relativize=True)
+        n = z['@']
+        rds = n.get_rdataset(dns.rdataclass.IN, dns.rdatatype.SOA)
+        self.failUnless(rds.ttl == 3600)
+        soa_rd = rds[0]
+        n = z['ns1']
+        rds = n.get_rdataset(dns.rdataclass.IN, dns.rdatatype.A)
+        self.failUnless(rds.ttl == 694861)
+        n = z['ns2']
+        rds = n.get_rdataset(dns.rdataclass.IN, dns.rdatatype.A)
+        self.failUnless(rds.ttl == soa_rd.minimum)
+
+    def testTTLFromLast(self):
+        z = dns.zone.from_text(ttl_from_last_text, 'example.', check_origin=False)
+        n = z['@']
+        rds = n.get_rdataset(dns.rdataclass.IN, dns.rdatatype.NS)
+        self.failUnless(rds.ttl == 3600)
+        n = z['ns1']
+        rds = n.get_rdataset(dns.rdataclass.IN, dns.rdatatype.A)
+        self.failUnless(rds.ttl == 3600)
+        n = z['ns2']
+        rds = n.get_rdataset(dns.rdataclass.IN, dns.rdatatype.A)
+        self.failUnless(rds.ttl == 694861)
+
+    def testNoTTL(self):
+        def bad():
+            dns.zone.from_text(no_ttl_text, 'example.', check_origin=False)
+        self.failUnlessRaises(dns.exception.SyntaxError, bad)
+
     def testNoSOA(self):
         def bad():
             dns.zone.from_text(no_soa_text, 'example.', relativize=True)
@@ -465,12 +522,11 @@ class ZoneTestCase(unittest.TestCase):
 
     def testFirstRRStartsWithWhitespace(self):
         # no name is specified, so default to the initial origin
-        # no ttl is specified, so default to the initial TTL of 0
-        z = dns.zone.from_text(' IN A 10.0.0.1', origin='example.',
+        z = dns.zone.from_text(' 300 IN A 10.0.0.1', origin='example.',
                                check_origin=False)
         n = z['@']
         rds = n.get_rdataset(dns.rdataclass.IN, dns.rdatatype.A)
-        self.failUnless(rds.ttl == 0)
+        self.failUnless(rds.ttl == 300)
 
     def testZoneOrigin(self):
         z = dns.zone.Zone('example.')