#5033 plugins/dns.py:get_reverse_zone Reverse DNS record creation fails to take into account CNAME and zone delegations
Opened 4 years ago by clauluck. Modified 2 years ago

Reverse DNS record creation fails to take into account aliasing and zone delegations. It fails to follow DNAME and CNAME to find the final PTR name to update.

As an example, the current implementation resolves 172.16.230.161 to 161.230.16.172.in-addr.arpa. and will look for the zone 230.16.172.in-addr.arpa. no matter what.

But our provider is routing only 172.16.230.160/27 to us, and delegates 160-27.230.16.172.in-addr.arpa. to our nameservers and hosts CNAME under his zone 230.16.172.in-addr.arpa. pointing to ours. In this scenario, IPA fails to find out the correct PTR record to be 161.160-27.230.16.172.in-addr.arpa. pointed to by a CNAME 161.230.16.172.in-addr.arpa elsewhere.

To cover real usecases the procedure is more involved: compute the canonical reverse lookup name, then query DNS for any such CNAME. If the response, even with NXDOMAIN set, carries a CNAME, then substitute the canonical reverse lookup name using the CNAME target.

To implement this, ipalib should do the DNS query in ipalib.plugins.dns.get_reverse_zone. Unfortunately dns.resolver.Resolver.query() used by ipalib eats the answer when it encounters NXDOMAIN. I propose to alter dns.resolver.Resolver.query() to add the response to the exception so that ipalib can inspect it.

I've implemented this by subclassing dns.resolver.Resolver in ipalib/plugins/dns.py with the second patch below, and the following patch on get_reverse_zone:

--- dns.py.orig 2015-05-24 00:41:20.861545139 +0200
+++ dns.py.1    2015-05-24 00:57:14.094508551 +0200
@@ -521,7 +521,32 @@

 def get_reverse_zone(ipaddr, prefixlen=None):
     ip = netaddr.IPAddress(str(ipaddr))
-    revdns = DNSName(unicode(ip.reverse_dns))
+    revdns_name = unicode(ip.reverse_dns)
+    revdns = DNSName(revdns_name)
+
+    resolver = Resolver()
+    rdtype = _CNAME = dns.rdatatype.from_text('CNAME')
+    try:
+        dns_rrset = None
+        try:
+            dns_answer = resolver.query(revdns_name, rdtype,
+                                        dns.rdataclass.IN,
+                                        raise_on_no_answer=False)
+            dns_rrset = dns_answer.rrset
+        except dns.resolver.NXDOMAIN as e_nx:
+            if e_nx.message is None or not len(e_nx.message.answer):
+                raise
+            dns_rrset = e_nx.message.answer
+        for rrset in dns_rrset:
+            if rrset.rdtype == _CNAME:
+                for rrset_item in rrset.items:
+                    revdns = DNSName(unicode(rrset_item.target))
+                    prefixlen = None
+    except (dns.resolver.NXDOMAIN,
+            dns.resolver.YXDOMAIN,
+            dns.resolver.NoNameservers,
+            dns.resolver.Timeout) as e:
+        pass

     if prefixlen is None:
         revzone = None
}}}


And this is the patch for dnspython:

--- resolver.py 2014-07-24 17:11:36.000000000 +0200
+++ resolver.py.orig    2015-05-24 01:02:02.328497488 +0200
@@ -902,12 +902,11 @@
                     sleep_time = min(timeout, backoff)
                     backoff *= 2
                     time.sleep(sleep_time)
-            if response.rcode() == dns.rcode.NXDOMAIN:
-                continue
-            all_nxdomain = False
+            if response.rcode() != dns.rcode.NXDOMAIN:
+                all_nxdomain = False
             break
         if all_nxdomain:
-            raise NXDOMAIN
+            raise NXDOMAIN(response)
         answer = Answer(qname, rdtype, rdclass, response,
                         raise_on_no_answer)
         if self.cache:

--- dns.py.orig 2015-05-24 00:41:20.861545139 +0200
+++ dns.py.1    2015-05-24 00:57:14.094508551 +0200
@@ -521,7 +521,32 @@

 def get_reverse_zone(ipaddr, prefixlen=None):
     ip = netaddr.IPAddress(str(ipaddr))
-    revdns = DNSName(unicode(ip.reverse_dns))
+    revdns_name = unicode(ip.reverse_dns)
+    revdns = DNSName(revdns_name)
+
+    resolver = Resolver()
+    rdtype = _CNAME = dns.rdatatype.from_text('CNAME')
+    try:
+        try:
+            dns_answer = resolver.query(revdns_name, rdtype,
+                                        dns.rdataclass.IN,
+                                        raise_on_no_answer=False)
+            dns_rrset = (dns_answer.rrset,)
+        except dns.resolver.NXDOMAIN as e_nx:
+            if e_nx.message is None or not len(e_nx.message.answer):
+                raise
+            dns_rrset = e_nx.message.answer
+        for rrset in dns_rrset:
+            if rrset.rdtype == _CNAME:
+                for rrset_item in rrset.items:
+                    revdns = DNSName(unicode(rrset_item.target))
+                    prefixlen = None
+    except (dns.resolver.NXDOMAIN,
+            dns.resolver.YXDOMAIN,
+            dns.resolver.NoNameservers,
+            dns.resolver.Timeout) as e:
+        pass

     if prefixlen is None:
         revzone = None

Hello!

Thank you for yor feedback. I can see that you are touching python-dns/dnspython library. This library is not controlled by FreeIPA project - please contact dnspython upstream here:
https://github.com/rthalley/dnspython/issues

Feel free to post link here so we can want the progress and consider further integration. Thank you!

Steps to reproduce:
1. Create reverse zone 2.0.192.in-addr.arpa.
2. Create CNAME record 1 in zone 2.0.192.in-addr.arpa. pointing to target.test.
3. Attempt to use option --a-create-reverse while adding A record with value 192.0.2.1 to some other zone.

IPA should detect that 1.2.0.192.in-addr.arpa. is an alias and update the target name if it is under IPA's control.

Metadata Update from @clauluck:
- Issue assigned to someone
- Issue set to the milestone: FreeIPA 4.5 backlog

2 years ago

Login to comment on this ticket.

Metadata