diff --git a/LICENSE b/LICENSE
index 2896ca9..c0382e1 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 468618f..0cf2682 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)

Inherited TTL when parsing zone master files defaults to zero
When parsing a master zone file which has neither a $TTL directive nor an SOA RR, the default TTL applied to any RRs which don't specify one explicitly is zero.

This is not only surprising given the differing implementations of ttl=0, but also because [BIND](https://github.com/mirroring/bind9/blob/5125df6753f4b648a61502b261d2706a452e07d0/lib/dns/master.c#L1845-L1874) seemingly treats this as a syntax error and rejects the zone.

Example:
```
$ORIGIN example.
localhost IN A 127.0.0.1
```

Currently this parses as a localhost.example. RR with TTL=0.
diff --git a/tests/test_zone.py b/tests/test_zone.py
index 3c497a6..ce7caf0 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.')

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest coverage pylint flake8 idna>=2.1 pycrypto>=2.6.1 ecdsa>=0.13
3.9
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
astroid==3.3.9
coverage==7.8.0
dill==0.3.9
-e git+https://github.com/rthalley/dnspython.git@6b11f2677fe6d4da653a85512ab73fa31c27214f#egg=dnspython
ecdsa==0.19.1
exceptiongroup==1.2.2
flake8==7.2.0
idna==3.10
iniconfig==2.1.0
isort==6.0.1
mccabe==0.7.0
packaging==24.2
platformdirs==4.3.7
pluggy==1.5.0
pycodestyle==2.13.0
pycrypto==2.6.1
pyflakes==3.3.1
pylint==3.3.6
pytest==8.3.5
six==1.17.0
tomli==2.2.1
tomlkit==0.13.2
typing_extensions==4.13.0

name: dnspython
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - astroid==3.3.9
      - coverage==7.8.0
      - dill==0.3.9
      - ecdsa==0.19.1
      - exceptiongroup==1.2.2
      - flake8==7.2.0
      - idna==3.10
      - iniconfig==2.1.0
      - isort==6.0.1
      - mccabe==0.7.0
      - packaging==24.2
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pycodestyle==2.13.0
      - pycrypto==2.6.1
      - pyflakes==3.3.1
      - pylint==3.3.6
      - pytest==8.3.5
      - six==1.17.0
      - tomli==2.2.1
      - tomlkit==0.13.2
      - typing-extensions==4.13.0
prefix: /opt/conda/envs/dnspython

tests/test_zone.py::ZoneTestCase::testNoTTL tests/test_zone.py::ZoneTestCase::testTTLFromLast tests/test_zone.py::ZoneTestCase::testTTLFromSOA
tests/test_zone.py::ZoneTestCase::testFromFile1 tests/test_zone.py::ZoneTestCase::testFromFile2 tests/test_zone.py::ZoneTestCase::testInclude tests/test_zone.py::ZoneTestCase::testToFileBinary tests/test_zone.py::ZoneTestCase::testToFileFilename tests/test_zone.py::ZoneTestCase::testToFileTextual tests/test_zone.py::ZoneTestCase::testToText tests/test_zone.py::ZoneTestCase::testTorture1
tests/test_zone.py::ZoneTestCase::testBadDirective tests/test_zone.py::ZoneTestCase::testDeleteRdataset1 tests/test_zone.py::ZoneTestCase::testDeleteRdataset2 tests/test_zone.py::ZoneTestCase::testEqual tests/test_zone.py::ZoneTestCase::testFindRRset1 tests/test_zone.py::ZoneTestCase::testFindRRset2 tests/test_zone.py::ZoneTestCase::testFindRdataset1 tests/test_zone.py::ZoneTestCase::testFindRdataset2 tests/test_zone.py::ZoneTestCase::testFirstRRStartsWithWhitespace tests/test_zone.py::ZoneTestCase::testFromText tests/test_zone.py::ZoneTestCase::testGetRRset1 tests/test_zone.py::ZoneTestCase::testGetRRset2 tests/test_zone.py::ZoneTestCase::testGetRdataset1 tests/test_zone.py::ZoneTestCase::testGetRdataset2 tests/test_zone.py::ZoneTestCase::testIterateAllRdatas tests/test_zone.py::ZoneTestCase::testIterateAllRdatasets tests/test_zone.py::ZoneTestCase::testIterateRdatas tests/test_zone.py::ZoneTestCase::testIterateRdatasets tests/test_zone.py::ZoneTestCase::testNoNS tests/test_zone.py::ZoneTestCase::testNoSOA tests/test_zone.py::ZoneTestCase::testNodeDeleteRdataset1 tests/test_zone.py::ZoneTestCase::testNodeDeleteRdataset2 tests/test_zone.py::ZoneTestCase::testNodeFindRdataset1 tests/test_zone.py::ZoneTestCase::testNodeFindRdataset2 tests/test_zone.py::ZoneTestCase::testNodeGetRdataset1 tests/test_zone.py::ZoneTestCase::testNodeGetRdataset2 tests/test_zone.py::ZoneTestCase::testNotEqual1 tests/test_zone.py::ZoneTestCase::testNotEqual2 tests/test_zone.py::ZoneTestCase::testNotEqual3 tests/test_zone.py::ZoneTestCase::testReplaceRdataset1 tests/test_zone.py::ZoneTestCase::testReplaceRdataset2 tests/test_zone.py::ZoneTestCase::testTTLs tests/test_zone.py::ZoneTestCase::testToFileBinaryStream tests/test_zone.py::ZoneTestCase::testToFileTextualStream tests/test_zone.py::ZoneTestCase::testZoneOrigin tests/test_zone.py::ZoneTestCase::testZoneOriginNone
LICENSE dns/zone.py
LICENSE dns/zone.py