Patches attached.

--
Martin Basti

From 874a8c6be571953f3bc09db015e6209d627de628 Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Wed, 22 Apr 2015 15:29:21 +0200
Subject: [PATCH 1/2] DNSSEC: Improve global forwarders validation

Validation now provides more detailed information and less false
positives failures.

https://fedorahosted.org/freeipa/ticket/4657
---
 ipalib/messages.py                |  15 ++++-
 ipalib/plugins/dns.py             |  58 +++++++++---------
 ipalib/util.py                    | 123 +++++++++++++++++++++++++++++++-------
 ipaserver/install/bindinstance.py |  22 +++++--
 4 files changed, 163 insertions(+), 55 deletions(-)

diff --git a/ipalib/messages.py b/ipalib/messages.py
index b44beca729f5483a7241e4c98a9f724ed663e70f..dd33ad75276bb96c1993175945ad26d4822ec5ca 100644
--- a/ipalib/messages.py
+++ b/ipalib/messages.py
@@ -186,7 +186,7 @@ class DNSServerNotRespondingWarning(PublicMessage):
 
     errno = 13006
     type = "warning"
-    format = _(u"DNS server %(server)s not responding.")
+    format = _(u"DNS server %(server)s not responding (%(error)s).")
 
 
 class DNSServerDoesNotSupportDNSSECWarning(PublicMessage):
@@ -214,6 +214,19 @@ class ForwardzoneIsNotEffectiveWarning(PublicMessage):
                u"\"%(ns_rec)s\" to parent zone \"%(authzone)s\".")
 
 
+class DNSServerDoesNotSupportEDNS0Warning(PublicMessage):
+    """
+    **13009** Used when a DNS server does not support ENDS0, required for
+    DNSSEC support
+    """
+
+    errno = 13009
+    type = "warning"
+    format = _(u"DNS server %(server)s does not support EDNS0 (RFC 6891). "
+               u"If DNSSEC validation is enabled on IPA server(s), "
+               u"please disable it.")
+
+
 def iter_messages(variables, base):
     """Return a tuple with all subclasses
     """
diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py
index f589ab5b77a918b75fe6c48b465ecd9f02cb6d42..09378c6ec9808fd247fffd94c1cd904f4a7b1696 100644
--- a/ipalib/plugins/dns.py
+++ b/ipalib/plugins/dns.py
@@ -43,7 +43,10 @@ from ipalib.util import (normalize_zonemgr,
                          get_dns_forward_zone_update_policy,
                          get_dns_reverse_zone_update_policy,
                          get_reverse_zone_default, REVERSE_DNS_ZONES,
-                         normalize_zone, validate_dnssec_forwarder)
+                         normalize_zone, validate_dnssec_global_forwarder,
+                         DNSSECSignatureMissingError, UnresolvableRecordError,
+                         EDNS0UnsupportedError)
+
 from ipapython.ipautil import CheckedIPAddress, is_host_resolvable
 from ipapython.dnsutil import DNSName
 
@@ -4267,35 +4270,36 @@ class dnsconfig_mod(LDAPUpdate):
 
     def execute(self, *keys, **options):
         # test dnssec forwarders
-        non_dnssec_forwarders = []
-        not_responding_forwarders = []
         forwarders = options.get('idnsforwarders')
+
+        result = super(dnsconfig_mod, self).execute(*keys, **options)
+        self.obj.postprocess_result(result)
+
         if forwarders:
             for forwarder in forwarders:
-                dnssec_status = validate_dnssec_forwarder(forwarder)
-                if dnssec_status is None:
-                    not_responding_forwarders.append(forwarder)
-                elif dnssec_status is False:
-                    non_dnssec_forwarders.append(forwarder)
-
-        result = super(dnsconfig_mod, self).execute(*keys, **options)
-        self.obj.postprocess_result(result)
-
-        # add messages
-        for forwarder in not_responding_forwarders:
-            messages.add_message(
-                options['version'],
-                result, messages.DNSServerNotRespondingWarning(
-                    server=forwarder,
-                )
-            )
-        for forwarder in non_dnssec_forwarders:
-            messages.add_message(
-                options['version'],
-                result, messages.DNSServerDoesNotSupportDNSSECWarning(
-                    server=forwarder,
-                )
-            )
+                try:
+                    validate_dnssec_global_forwarder(forwarder, log=self.log)
+                except DNSSECSignatureMissingError as e:
+                    messages.add_message(
+                        options['version'],
+                        result, messages.DNSServerDoesNotSupportDNSSECWarning(
+                            server=forwarder
+                        )
+                    )
+                except EDNS0UnsupportedError as e:
+                    messages.add_message(
+                        options['version'],
+                        result, messages.DNSServerDoesNotSupportEDNS0Warning(
+                            server=forwarder
+                        )
+                    )
+                except UnresolvableRecordError as e:
+                    messages.add_message(
+                        options['version'],
+                        result, messages.DNSServerNotRespondingWarning(
+                            server=forwarder, error=e
+                        )
+                    )
 
         return result
 
diff --git a/ipalib/util.py b/ipalib/util.py
index 2c17d80a0427a5c7e45a6a0b64fa1f4d39fffa8a..92c23959bd92e0cf947a1693155e44d6b4a09732 100644
--- a/ipalib/util.py
+++ b/ipalib/util.py
@@ -36,7 +36,7 @@ from dns import resolver, rdatatype
 from dns.exception import DNSException
 from netaddr.core import AddrFormatError
 
-from ipalib import errors
+from ipalib import errors, messages
 from ipalib.text import _
 from ipapython.ssh import SSHPublicKey
 from ipapython.dn import DN, RDN
@@ -559,38 +559,119 @@ def validate_hostmask(ugettext, hostmask):
         return _('invalid hostmask')
 
 
-def validate_dnssec_forwarder(ip_addr):
-    """Test DNS forwarder properties.
+class ForwarderValidationError(Exception):
+    format = None
 
-    :returns:
-     True if forwarder works as expected and supports DNSSEC.
-     False if forwarder does not support DNSSEC.
-     None if forwarder does not respond.
+    def __init__(self, format=None, message=None, **kw):
+        messages.process_message_arguments(self, format, message, **kw)
+        super(ForwarderValidationError, self).__init__(self.msg)
+
+
+class UnresolvableRecordError(ForwarderValidationError):
+    format = _("no %(rtype)s record for %(owner)s was returned from "
+               "server %(ip)s")
+
+
+class EDNS0UnsupportedError(ForwarderValidationError):
+    format = _("server %(ip)s does not support EDNS0")
+
+
+class DNSSECSignatureMissingError(ForwarderValidationError):
+    format = _("server %(ip)s does not support DNSSEC")
+
+
+def _resolve_record(owner, rtype, nameserver_ip=None, edns0=False,
+                    dnssec=False, timeout=10):
     """
-    ip_addr = str(ip_addr)
+    :param nameserver_ip: if None, default resolvers will be used
+    :param edns0: enables EDNS0
+    :param dnssec: enabled EDNS0, flags: DO
+    :raise DNSException: if error occurs
+    """
+    assert isinstance(nameserver_ip, basestring)
+    assert isinstance(rtype, basestring)
+
     res = dns.resolver.Resolver()
-    res.nameservers = [ip_addr]
-    res.lifetime = 10  # wait max 10 seconds for reply
+    if nameserver_ip:
+        res.nameservers = [nameserver_ip]
+    res.lifetime = timeout
 
-    # enable Authenticated Data + Checking Disabled flags
-    res.set_flags(dns.flags.AD | dns.flags.CD)
+    # Recursion Desired,
+    # this option prevents to get answers in authority section instead of answer
+    res.set_flags(dns.flags.RD)
 
-    # enable EDNS v0 + enable DNSSEC-Ok flag
-    res.use_edns(0, dns.flags.DO, 0)
+    if dnssec:
+        res.use_edns(0, dns.flags.DO, 4096)
+        res.set_flags(dns.flags.RD)
+    elif edns0:
+        res.use_edns(0, 0, 4096)
+
+    return res.query(owner, rtype)
+
+
+def _validate_ends0_forwarder(owner, rtype, ip_addr, log=None, timeout=10):
+    """
+    Validate if forwarder supports EDNS0
+
+    :raise UnresolvableRecordError: record cannot be resolved
+    :raise EDNS0UnsupportedError: ENDS0 is not supported by forwarder
+    """
+
+    try:
+        _resolve_record(owner, rtype, nameserver_ip=ip_addr, timeout=timeout)
+    except DNSException as e:
+        if log:
+            log.debug("Resolving record '%s %s @%s' returned error: %s",
+                      owner, rtype, ip_addr, e)
+        raise UnresolvableRecordError(owner=owner, rtype=rtype, ip=ip_addr)
+
+    try:
+        _resolve_record(owner, rtype, nameserver_ip=ip_addr, edns0=True,
+                        timeout=timeout)
+    except DNSException as e:
+        if log:
+            log.debug("Resolving record '%s %s @%s' with EDNS0 returned "
+                      "error: %s",  owner, rtype, ip_addr, e)
+        raise EDNS0UnsupportedError(ip=ip_addr)
+
+
+def validate_dnssec_global_forwarder(ip_addr, log=None, timeout=10):
+    """Test DNS forwarder properties. against root zone.
+
+    Global forwarders should be able return signed root zone
+
+    :raise UnresolvableRecordError: record cannot be resolved
+    :raise EDNS0UnsupportedError: ENDS0 is not supported by forwarder
+    :raise DNSSECSignatureMissingError: did not receive RRSIG for root zone
+    """
+
+    ip_addr = str(ip_addr)
+    owner = "."
+    rtype = "SOA"
+
+    _validate_ends0_forwarder(owner, rtype, ip_addr, log, timeout=timeout)
 
     # DNS root has to be signed
     try:
-        ans = res.query('.', 'NS')
-    except DNSException:
-        return None
+        ans = _resolve_record(owner, rtype, nameserver_ip=ip_addr, dnssec=True,
+                              timeout=timeout)
+    except DNSException as e:
+        if log:
+            log.debug("Resolving record '%s %s @%s' with dnssec returned "
+                      "error: %s", owner, rtype, ip_addr, e)
+        raise DNSSECSignatureMissingError(ip=ip_addr)
 
     try:
-        ans.response.find_rrset(ans.response.answer, dns.name.root,
-                dns.rdataclass.IN, dns.rdatatype.RRSIG, dns.rdatatype.NS)
+        ans.response.find_rrset(
+            ans.response.answer, dns.name.root, dns.rdataclass.IN,
+            dns.rdatatype.RRSIG, dns.rdatatype.SOA
+        )
     except KeyError:
-        return False
+        if log:
+            log.debug("No RRSIG data in returned answer to query '%s %s @%s'",
+                      owner, rtype, ip_addr)
+        raise DNSSECSignatureMissingError(ip=ip_addr)
 
-    return True
 
 
 def validate_idna_domain(value):
diff --git a/ipaserver/install/bindinstance.py b/ipaserver/install/bindinstance.py
index 97dcb3d950fb63d0e9bf6332868efd52190b27a7..773322831b50ac189fab5ceb22eab870cecdffe8 100644
--- a/ipaserver/install/bindinstance.py
+++ b/ipaserver/install/bindinstance.py
@@ -43,7 +43,8 @@ from ipaplatform.tasks import tasks
 from ipalib.util import (validate_zonemgr_str, normalize_zonemgr,
         get_dns_forward_zone_update_policy, get_dns_reverse_zone_update_policy,
         normalize_zone, get_reverse_zone_default, zone_is_reverse,
-        validate_dnssec_forwarder)
+        validate_dnssec_global_forwarder, DNSSECSignatureMissingError,
+        EDNS0UnsupportedError, UnresolvableRecordError)
 from ipalib.constants import CACERT
 
 NAMED_CONF = paths.NAMED_CONF
@@ -468,11 +469,9 @@ def check_forwarders(dns_forwarders, logger):
     forwarders_dnssec_valid = True
     for forwarder in dns_forwarders:
         logger.debug("Checking forwarder: %s", forwarder)
-        result = validate_dnssec_forwarder(forwarder)
-        if result is None:
-            logger.error("Forwarder %s does not work", forwarder)
-            raise RuntimeError("Forwarder %s does not respond" % forwarder)
-        elif result is False:
+        try:
+            validate_dnssec_global_forwarder(forwarder, log=logger)
+        except DNSSECSignatureMissingError:
             forwarders_dnssec_valid = False
             logger.warning("DNS forwarder %s does not return DNSSEC signatures in answers", forwarder)
             logger.warning("Please fix forwarder configuration to enable DNSSEC support.\n"
@@ -481,6 +480,17 @@ def check_forwarders(dns_forwarders, logger):
                    "signatures in answers" % forwarder)
             print "Please fix forwarder configuration to enable DNSSEC support."
             print "(For BIND 9 add directive \"dnssec-enable yes;\" to \"options {}\")"
+        except EDNS0UnsupportedError:
+            forwarders_dnssec_valid = False
+            logger.warning("DNS forwarder %s does not support ENDS0 (RFC 6891)", forwarder)
+            logger.warning("Please fix forwarder configuration. "
+                           "DNSSEC support cannot be enabled without EDNS0")
+            print ("WARNING: DNS forwarder %s does not support EDNS0 (RFC 6891)"
+                   % forwarder)
+        except UnresolvableRecordError as e:
+            logger.error("Forwarder %s does not work (%s)", forwarder, e)
+            raise RuntimeError("Forwarder %s does not respond (%s)" % (
+                forwarder, e))
 
     return forwarders_dnssec_valid
 
-- 
2.1.0

From 1f0376b77859a89cc7d6c1177fd970d4cd224895 Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Fri, 24 Apr 2015 13:37:07 +0200
Subject: [PATCH 2/2] DNSSEC: validate forward zone forwarders

Show warning messages if DNSSEC validation is failing for particular FW
zone or if the specified forwarders do not work

https://fedorahosted.org/freeipa/ticket/4657
---
 ipalib/messages.py    | 12 +++++++
 ipalib/plugins/dns.py | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++-
 ipalib/util.py        | 52 ++++++++++++++++++++++++++--
 3 files changed, 157 insertions(+), 3 deletions(-)

diff --git a/ipalib/messages.py b/ipalib/messages.py
index dd33ad75276bb96c1993175945ad26d4822ec5ca..e8f0b7c929f7857ec15783de4ba482193c92d6d1 100644
--- a/ipalib/messages.py
+++ b/ipalib/messages.py
@@ -227,6 +227,18 @@ class DNSServerDoesNotSupportEDNS0Warning(PublicMessage):
                u"please disable it.")
 
 
+class DNSSECValidationFailingWarning(PublicMessage):
+    """
+    **13010** Used when a DNSSEC validation failed on IPA DNS server
+    """
+
+    errno = 13010
+    type = "warning"
+    format = _(u"DNSSEC validation for %(name)s failed. "
+               u"Please verify your DNSSEC signatures or disable DNSSEC "
+               u"validation on all IPA servers.")
+
+
 def iter_messages(variables, base):
     """Return a tuple with all subclasses
     """
diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py
index 09378c6ec9808fd247fffd94c1cd904f4a7b1696..c41b01e411f97cdb77dc7b43bc5c3c1ca29074d5 100644
--- a/ipalib/plugins/dns.py
+++ b/ipalib/plugins/dns.py
@@ -26,6 +26,7 @@ import re
 import binascii
 import dns.name
 import dns.exception
+import dns.rdatatype
 import dns.resolver
 import encodings.idna
 
@@ -45,7 +46,9 @@ from ipalib.util import (normalize_zonemgr,
                          get_reverse_zone_default, REVERSE_DNS_ZONES,
                          normalize_zone, validate_dnssec_global_forwarder,
                          DNSSECSignatureMissingError, UnresolvableRecordError,
-                         EDNS0UnsupportedError)
+                         EDNS0UnsupportedError, DNSSECValidationError,
+                         validate_dnssec_zone_forwarder_step1,
+                         validate_dnssec_zone_forwarder_step2)
 
 from ipapython.ipautil import CheckedIPAddress, is_host_resolvable
 from ipapython.dnsutil import DNSName
@@ -4335,11 +4338,87 @@ class dnsforwardzone(DNSZoneBase):
         _add_warning_fw_zone_is_not_effective(result, fwzone,
                                               options['version'])
 
+    def _warning_if_forwarders_do_not_work(self, result, *keys, **options):
+        fwzone = keys[-1]
+        forwarders = options.get('idnsforwarders', [])
+        any_forwarder_work = False
+
+        for forwarder in forwarders:
+            try:
+                validate_dnssec_zone_forwarder_step1(forwarder, fwzone,
+                                                     self.log)
+            except UnresolvableRecordError as e:
+                messages.add_message(
+                    options['version'],
+                    result, messages.DNSServerNotRespondingWarning(
+                        server=forwarder, error=e
+                    )
+                )
+            except EDNS0UnsupportedError as e:
+                messages.add_message(
+                    options['version'],
+                    result, messages.DNSServerDoesNotSupportEDNS0Warning(
+                        server=forwarder,
+                    )
+                )
+            else:
+                any_forwarder_work = True
+
+        if not any_forwarder_work:
+            # do not test DNSSEC validation if there is no valid forwarder
+            return
+
+        # resolve IP address of any DNS replica
+        # FIXME: https://fedorahosted.org/bind-dyndb-ldap/ticket/143
+        # we currenly should to test all IPA DNS replica, because DNSSEC
+        # validation is configured just in named.conf per replica
+
+        ipa_dns_masters = [normalize_zone(x) for x in
+                           api.Object.dnsrecord.get_dns_masters()]
+
+        if not ipa_dns_masters:
+            # something very bad happened, DNS is installed, but no IPA DNS
+            # servers available
+            self.log.error("No IPA DNS server can be found, but integrated DNS "
+                           "is installed")
+            return
+
+        ipa_dns_ip = None
+        for rdtype in (dns.rdatatype.A, dns.rdatatype.AAAA):
+            try:
+                ans = dns.resolver.query(ipa_dns_masters[0], rdtype)
+            except dns.exception.DNSException:
+                continue
+            else:
+                ipa_dns_ip = ans.rrset.items[0]
+                break
+
+        if not ipa_dns_ip:
+            self.log.error("Cannot resolve %s hostname", ipa_dns_masters[0])
+            return
+
+        # Test if IPA is able to receive replies from forwarders
+        try:
+            validate_dnssec_zone_forwarder_step2(ipa_dns_ip, fwzone,
+                                                 self.log)
+        except DNSSECValidationError as e:
+            messages.add_message(
+                options['version'],
+                result, messages.DNSSECValidationFailingWarning(
+                    name=fwzone,
+                )
+            )
 
 @register()
 class dnsforwardzone_add(DNSZoneBase_add):
     __doc__ = _('Create new DNS forward zone.')
 
+    def interactive_prompt_callback(self, kw):
+        if kw.get('idnsforwarders', False):
+            self.Backend.textui.print_plain("Server will check forwarder(s).")
+            self.Backend.textui.print_plain("This may take some time, please "
+                                            "wait ...")
+
     def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
         assert isinstance(dn, DN)
 
@@ -4359,6 +4438,9 @@ class dnsforwardzone_add(DNSZoneBase_add):
     def execute(self, *keys, **options):
         result = super(dnsforwardzone_add, self).execute(*keys, **options)
         self.obj._warning_fw_zone_is_not_effective(result, *keys, **options)
+        if options.get('idnsforwarders'):
+            self.obj._warning_if_forwarders_do_not_work(result, *keys,
+                                                        **options)
         return result
 
 
@@ -4373,6 +4455,12 @@ class dnsforwardzone_del(DNSZoneBase_del):
 class dnsforwardzone_mod(DNSZoneBase_mod):
     __doc__ = _('Modify DNS forward zone.')
 
+    def interactive_prompt_callback(self, kw):
+        if kw.get('idnsforwarders', False):
+            self.Backend.textui.print_plain("Server will check forwarder(s).")
+            self.Backend.textui.print_plain("This may take some time, please "
+                                            "wait ...")
+
     def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
         try:
             entry = ldap.get_entry(dn)
@@ -4401,6 +4489,12 @@ class dnsforwardzone_mod(DNSZoneBase_mod):
 
         return dn
 
+    def execute(self, *keys, **options):
+        result = super(dnsforwardzone_mod, self).execute(*keys, **options)
+        if options.get('idnsforwarders'):
+            self.obj._warning_if_forwarders_do_not_work(result, *keys,
+                                                        **options)
+        return result
 
 @register()
 class dnsforwardzone_find(DNSZoneBase_find):
diff --git a/ipalib/util.py b/ipalib/util.py
index 92c23959bd92e0cf947a1693155e44d6b4a09732..0926f920e7b2e306cec7c339fc0bf4f405348d00 100644
--- a/ipalib/util.py
+++ b/ipalib/util.py
@@ -580,12 +580,17 @@ class DNSSECSignatureMissingError(ForwarderValidationError):
     format = _("server %(ip)s does not support DNSSEC")
 
 
+class DNSSECValidationError(ForwarderValidationError):
+    format = _("record '%(owner)s %(rtype)s @%(ip)s' is not DNSSEC valid")
+
+
 def _resolve_record(owner, rtype, nameserver_ip=None, edns0=False,
-                    dnssec=False, timeout=10):
+                    dnssec=False, flag_cd=False, timeout=10):
     """
     :param nameserver_ip: if None, default resolvers will be used
     :param edns0: enables EDNS0
     :param dnssec: enabled EDNS0, flags: DO
+    :param flag_cd: requires dnssec=True, adds flag CD
     :raise DNSException: if error occurs
     """
     assert isinstance(nameserver_ip, basestring)
@@ -602,7 +607,10 @@ def _resolve_record(owner, rtype, nameserver_ip=None, edns0=False,
 
     if dnssec:
         res.use_edns(0, dns.flags.DO, 4096)
-        res.set_flags(dns.flags.RD)
+        flags = dns.flags.RD
+        if flag_cd:
+            flags = flags | dns.flags.CD
+        res.set_flags(flags)
     elif edns0:
         res.use_edns(0, 0, 4096)
 
@@ -673,6 +681,46 @@ def validate_dnssec_global_forwarder(ip_addr, log=None, timeout=10):
         raise DNSSECSignatureMissingError(ip=ip_addr)
 
 
+def validate_dnssec_zone_forwarder_step1(ip_addr, fwzone, log, timeout=10):
+    """
+    Only forwarders in forward zones can be validated in this way
+    :raise UnresolvableRecordError: record cannot be resolved
+    :raise EDNS0UnsupportedError: ENDS0 is not supported by forwarder
+    """
+    _validate_ends0_forwarder(fwzone, "SOA", ip_addr, log=log, timeout=timeout)
+
+
+def validate_dnssec_zone_forwarder_step2(ipa_ip_addr, fwzone, log, timeout=10):
+    """
+    This step must be executed after forwarders is added into LDAP, and only
+    when we are sure the forwarders work.
+    Query will be send to IPA DNS server, to verify if reply passed,
+    or DNSSEC validation failed.
+    Only forwarders in forward zones can be validated in this way
+    :raise UnresolvableRecordError: record cannot be resolved
+    :raise DNSSECValidationError: response from forwarder is not DNSSEC valid
+    """
+    rtype = "SOA"
+    try:
+        _resolve_record(fwzone, rtype, nameserver_ip=ipa_ip_addr, edns0=True,
+                        timeout=timeout)
+    except DNSException as e:
+        if log:
+            log.debug("Resolving record '%s %s @%s' with EDNS0 returned "
+                  "error: %s",  fwzone, rtype, ipa_ip_addr, e)
+            log.debug("Retrying query with CD flag")
+    else:
+        return
+
+    try:
+        _resolve_record(fwzone, rtype, nameserver_ip=ipa_ip_addr, dnssec=True,
+                        flag_cd=True, timeout=timeout)
+    except DNSException as e:
+        if log:
+            log.debug("Resolving record '%s %s @%s' with DNSSEC+CD returned "
+                      "error: %s",  fwzone, rtype, ipa_ip_addr, e)
+        raise DNSSECValidationError(owner=fwzone, rtype=rtype, ip=ipa_ip_addr)
+
 
 def validate_idna_domain(value):
     """
-- 
2.1.0

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to