From 11bdb15d9c8037850d9ca5ea21b713d6283fab19 Mon Sep 17 00:00:00 2001 From: Tom Lanyon Date: Tue, 30 May 2017 17:19:20 +1000 Subject: [PATCH] Improve TTL detection when reading master zone files. 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 | 1 + dns/zone.py | 58 +++++++++++++++++++++++++++++++++++-------- tests/test_zone.py | 62 +++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 108 insertions(+), 13 deletions(-) diff --git a/LICENSE b/LICENSE index 2896ca97..c0382e17 100644 --- 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, diff --git a/dns/zone.py b/dns/zone.py index 468618f6..0cf26826 100644 --- a/dns/zone.py +++ b/dns/zone.py @@ -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) diff --git a/tests/test_zone.py b/tests/test_zone.py index 3c497a63..ce7caf01 100644 --- a/tests/test_zone.py +++ b/tests/test_zone.py @@ -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.') -- 2.47.3