Repository: rthalley/dnspython

Issue:
XFR example uses deprecated function
The XFR example in [examples/xfr.py](https://github.com/rthalley/dnspython/blob/main/examples/xfr.py) uses the deprecated function `dns.query.xfr()` instead of `dns.query.inbound_xfr()`.

This means that there is little guidance on how to go about generating and using the results of a zone transfer.

I assume the example would involve some use of `dns.xfr.make_query()`, e.g. similar to the following:

```
import dns.xfr
import dns.query
import dns.resolver
import dns.zone

zone_name = "dnspython.org"
soa_answer = dns.resolver.resolve(zone_name, "SOA")
master_answer = dns.resolver.resolve(soa_answer[0].mname, "A")

zone = dns.zone.Zone(zone_name)
query, serial = dns.xfr.make_query (zone)
z = dns.zone.from_xfr(dns.query.inbound_xfr(master_answer[0].address, zone, query))
for n in sorted(z.nodes.keys()):
    print(z[n].to_text(n))
```

Discussion:

Solution patch:
diff --git a/dns/rdatatype.py b/dns/rdatatype.py
index aa9e561..f2d0eef 100644
--- a/dns/rdatatype.py
+++ b/dns/rdatatype.py
@@ -86,6 +86,7 @@ class RdataType(dns.enum.IntEnum):
     ZONEMD = 63
     SVCB = 64
     HTTPS = 65
+    DSYNC = 66
     SPF = 99
     UNSPEC = 103
     NID = 104
@@ -332,5 +333,6 @@ RESINFO = RdataType.RESINFO
 WALLET = RdataType.WALLET
 TA = RdataType.TA
 DLV = RdataType.DLV
+DSYNC = RdataType.DSYNC
 
 ### END generated RdataType constants
diff --git a/dns/rdtypes/ANY/DSYNC.py b/dns/rdtypes/ANY/DSYNC.py
new file mode 100644
index 0000000..42e4476
--- /dev/null
+++ b/dns/rdtypes/ANY/DSYNC.py
@@ -0,0 +1,74 @@
+# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
+
+import struct
+
+import dns.enum
+import dns.exception
+import dns.immutable
+import dns.rdata
+import dns.rdatatype
+import dns.rdtypes.util
+
+
+class UnknownScheme(dns.exception.DNSException):
+    """Unknown DSYNC scheme"""
+
+
+class Scheme(dns.enum.IntEnum):
+    """DSYNC SCHEME"""
+
+    NOTIFY = 1
+
+    @classmethod
+    def _maximum(cls):
+        return 255
+
+    @classmethod
+    def _unknown_exception_class(cls):
+        return UnknownScheme
+
+
+@dns.immutable.immutable
+class DSYNC(dns.rdata.Rdata):
+    """DSYNC record"""
+
+    # see: draft-ietf-dnsop-generalized-notify
+
+    __slots__ = ["rrtype", "scheme", "port", "target"]
+
+    def __init__(self, rdclass, rdtype, rrtype, scheme, port, target):
+        super().__init__(rdclass, rdtype)
+        self.rrtype = self._as_rdatatype(rrtype)
+        self.scheme = Scheme.make(scheme)
+        self.port = self._as_uint16(port)
+        self.target = self._as_name(target)
+
+    def to_text(self, origin=None, relativize=True, **kw):
+        target = self.target.choose_relativity(origin, relativize)
+        return "%s %s %d %s" % (
+            dns.rdatatype.to_text(self.rrtype),
+            Scheme.to_text(self.scheme),
+            self.port,
+            target,
+        )
+
+    @classmethod
+    def from_text(
+        cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None
+    ):
+        rrtype = dns.rdatatype.from_text(tok.get_string())
+        scheme = Scheme.make(tok.get_string())
+        port = tok.get_uint16()
+        target = tok.get_name(origin, relativize, relativize_to)
+        return cls(rdclass, rdtype, rrtype, scheme, port, target)
+
+    def _to_wire(self, file, compress=None, origin=None, canonicalize=False):
+        three_ints = struct.pack("!HBH", self.rrtype, self.scheme, self.port)
+        file.write(three_ints)
+        self.target.to_wire(file, None, origin, False)
+
+    @classmethod
+    def from_wire_parser(cls, rdclass, rdtype, parser, origin=None):
+        (rrtype, scheme, port) = parser.get_struct("!HBH")
+        target = parser.get_name(origin)
+        return cls(rdclass, rdtype, rrtype, scheme, port, target)
diff --git a/dns/rdtypes/ANY/__init__.py b/dns/rdtypes/ANY/__init__.py
index 647b215..cc39f86 100644
--- a/dns/rdtypes/ANY/__init__.py
+++ b/dns/rdtypes/ANY/__init__.py
@@ -31,6 +31,7 @@ __all__ = [
     "DNAME",
     "DNSKEY",
     "DS",
+    "DSYNC",
     "EUI48",
     "EUI64",
     "GPOS",
diff --git a/examples/xfr.py b/examples/xfr.py
index 1c8175e..1336053 100755
--- a/examples/xfr.py
+++ b/examples/xfr.py
@@ -7,6 +7,7 @@ import dns.zone
 soa_answer = dns.resolver.resolve("dnspython.org", "SOA")
 master_answer = dns.resolver.resolve(soa_answer[0].mname, "A")
 
-z = dns.zone.from_xfr(dns.query.xfr(master_answer[0].address, "dnspython.org"))
+z = dns.zone.Zone("dnspython.org")
+dns.query.inbound_xfr(master_answer[0].address, z)
 for n in sorted(z.nodes.keys()):
     print(z[n].to_text(n))


Test patch:
diff --git a/tests/test_rdata.py b/tests/test_rdata.py
index 8c4c510..6691fa7 100644
--- a/tests/test_rdata.py
+++ b/tests/test_rdata.py
@@ -68,6 +68,7 @@ class RdataTestCase(unittest.TestCase):
 
     def test_class_registration(self):
         CTXT = 64003
+
         class CTXTImp(dns.rdtypes.txtbase.TXTBase):
             """Test TXT-like record"""
 
@@ -1030,6 +1031,15 @@ class UtilTestCase(unittest.TestCase):
             dns.rdatatype.RdataType.make("NSAP-PTR"), dns.rdatatype.NSAP_PTR
         )
 
+    def test_bad_DSYNC_text(self):
+        bad_dsync = [
+            "DSYNC CDS NOTIFY 5300 notify-endpoint.parent.net."
+            "DSYNC CDS 256 5300 notify-endpoint.parent.net."
+        ]
+        for text in bad_dsync:
+            with self.assertRaises(dns.exception.SyntaxError):
+                dns.rdata.from_text("in", "dsync", text)
+
 
 Rdata = dns.rdata.Rdata