diff options
-rw-r--r-- | ipalib/messages.py | 23 | ||||
-rw-r--r-- | ipalib/plugins/dns.py | 63 | ||||
-rw-r--r-- | ipalib/util.py | 130 | ||||
-rw-r--r-- | ipaserver/install/bindinstance.py | 32 | ||||
-rw-r--r-- | ipatests/test_xmlrpc/test_dns_plugin.py | 5 |
5 files changed, 188 insertions, 65 deletions
diff --git a/ipalib/messages.py b/ipalib/messages.py index b44beca72..236b683b3 100644 --- a/ipalib/messages.py +++ b/ipalib/messages.py @@ -179,14 +179,14 @@ class OptionSemanticChangedWarning(PublicMessage): u"%(hint)s") -class DNSServerNotRespondingWarning(PublicMessage): +class DNSServerValidationWarning(PublicMessage): """ - **13006** Used when a DNS server is not responding to queries + **13006** Used when a DNS server is not to able to resolve query """ errno = 13006 type = "warning" - format = _(u"DNS server %(server)s not responding.") + format = _(u"DNS server %(server)s: %(error)s.") class DNSServerDoesNotSupportDNSSECWarning(PublicMessage): @@ -196,10 +196,11 @@ class DNSServerDoesNotSupportDNSSECWarning(PublicMessage): errno = 13007 type = "warning" - format = _(u"DNS server %(server)s does not support DNSSEC. " + format = _(u"DNS server %(server)s does not support DNSSEC: %(error)s.\n" u"If DNSSEC validation is enabled on IPA server(s), " u"please disable it.") + class ForwardzoneIsNotEffectiveWarning(PublicMessage): """ **13008** Forwardzone is not effective, forwarding will not work because @@ -214,6 +215,20 @@ class ForwardzoneIsNotEffectiveWarning(PublicMessage): u"\"%(ns_rec)s\" to parent zone \"%(authzone)s\".") +class DNSServerDoesNotSupportEDNS0Warning(PublicMessage): + """ + **13009** Used when a DNS server does not support EDNS0, required for + DNSSEC support + """ + + errno = 13009 + type = "warning" + format = _(u"DNS server %(server)s does not support EDNS0 (RFC 6891): " + u"%(error)s.\n" + 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 f589ab5b7..c9dc1e547 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 @@ -4261,41 +4264,47 @@ class dnsconfig_mod(LDAPUpdate): __doc__ = _('Modify global DNS configuration.') def interactive_prompt_callback(self, kw): + + # show informative message on client side + # server cannot send messages asynchronous 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 ...") + self.Backend.textui.print_plain( + _("Server will check DNS forwarder(s).")) + self.Backend.textui.print_plain( + _("This may take some time, please wait ...")) def execute(self, *keys, **options): # test dnssec forwarders - non_dnssec_forwarders = [] - not_responding_forwarders = [] forwarders = options.get('idnsforwarders') - 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, - ) - ) + if forwarders: + for forwarder in forwarders: + try: + validate_dnssec_global_forwarder(forwarder, log=self.log) + except DNSSECSignatureMissingError as e: + messages.add_message( + options['version'], + result, messages.DNSServerDoesNotSupportDNSSECWarning( + server=forwarder, error=e, + ) + ) + except EDNS0UnsupportedError as e: + messages.add_message( + options['version'], + result, messages.DNSServerDoesNotSupportEDNS0Warning( + server=forwarder, error=e, + ) + ) + except UnresolvableRecordError as e: + messages.add_message( + options['version'], + result, messages.DNSServerValidationWarning( + server=forwarder, error=e + ) + ) return result diff --git a/ipalib/util.py b/ipalib/util.py index 2c17d80a0..8d7b66638 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,126 @@ 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 = _("query '%(owner)s %(rtype)s': %(error)s") + + +class EDNS0UnsupportedError(ForwarderValidationError): + format = _("query '%(owner)s %(rtype)s' with EDNS0: %(error)s") + + +class DNSSECSignatureMissingError(ForwarderValidationError): + format = _("answer to query '%(owner)s %(rtype)s' is missing DNSSEC " + "signatures (no RRSIG data)") + + +def _log_response(log, e): """ - ip_addr = str(ip_addr) + If exception contains response from server, log this response to debug log + :param log: if log is None, do not log + :param e: DNSException + """ + assert isinstance(e, DNSException) + if log is not None: + response = getattr(e, 'kwargs', {}).get('response') + if response: + log.debug("DNSException: %s; server response: %s", e, response) + + +def _resolve_record(owner, rtype, nameserver_ip=None, edns0=False, + dnssec=False, timeout=10): + """ + :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 + + # Recursion Desired, + # this option prevents to get answers in authority section instead of answer + res.set_flags(dns.flags.RD) + + 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) - # enable Authenticated Data + Checking Disabled flags - res.set_flags(dns.flags.AD | dns.flags.CD) - # enable EDNS v0 + enable DNSSEC-Ok flag - res.use_edns(0, dns.flags.DO, 0) +def _validate_edns0_forwarder(owner, rtype, ip_addr, log=None, timeout=10): + """ + Validate if forwarder supports EDNS0 + + :raise UnresolvableRecordError: record cannot be resolved + :raise EDNS0UnsupportedError: EDNS0 is not supported by forwarder + """ + + try: + _resolve_record(owner, rtype, nameserver_ip=ip_addr, timeout=timeout) + except DNSException as e: + _log_response(log, e) + raise UnresolvableRecordError(owner=owner, rtype=rtype, ip=ip_addr, + error=e) + + try: + _resolve_record(owner, rtype, nameserver_ip=ip_addr, edns0=True, + timeout=timeout) + except DNSException as e: + _log_response(log, e) + raise EDNS0UnsupportedError(owner=owner, rtype=rtype, ip=ip_addr, + error=e) + + +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: EDNS0 is not supported by forwarder + :raise DNSSECSignatureMissingError: did not receive RRSIG for root zone + """ + + ip_addr = str(ip_addr) + owner = "." + rtype = "SOA" + + _validate_edns0_forwarder(owner, rtype, ip_addr, log=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: + _log_response(log, e) + raise UnresolvableRecordError(owner=owner, rtype=rtype, ip=ip_addr, + error=e) 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 + raise DNSSECSignatureMissingError(owner=owner, rtype=rtype, ip=ip_addr) - return True def validate_idna_domain(value): diff --git a/ipaserver/install/bindinstance.py b/ipaserver/install/bindinstance.py index 4c1bfa600..77ff342d7 100644 --- a/ipaserver/install/bindinstance.py +++ b/ipaserver/install/bindinstance.py @@ -42,7 +42,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 @@ -463,23 +464,32 @@ def check_reverse_zones(ip_addresses, reverse_zones, options, unattended, search return ret_reverse_zones def check_forwarders(dns_forwarders, logger): - print "Checking forwarders, please wait ..." + print "Checking DNS forwarders, please wait ..." 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: + logger.debug("Checking DNS server: %s", forwarder) + try: + validate_dnssec_global_forwarder(forwarder, log=logger) + except DNSSECSignatureMissingError as e: forwarders_dnssec_valid = False - logger.warning("DNS forwarder %s does not return DNSSEC signatures in answers", forwarder) + logger.warning("DNS server %s does not support DNSSEC: %s", + forwarder, e) logger.warning("Please fix forwarder configuration to enable DNSSEC support.\n" "(For BIND 9 add directive \"dnssec-enable yes;\" to \"options {}\")") - print ("WARNING: DNS forwarder %s does not return DNSSEC " - "signatures in answers" % forwarder) + print "DNS server %s: %s" % (forwarder, e) print "Please fix forwarder configuration to enable DNSSEC support." print "(For BIND 9 add directive \"dnssec-enable yes;\" to \"options {}\")" + except EDNS0UnsupportedError as e: + forwarders_dnssec_valid = False + logger.warning("DNS server %s does not support ENDS0 " + "(RFC 6891): %s", forwarder, e) + logger.warning("Please fix forwarder configuration. " + "DNSSEC support cannot be enabled without EDNS0") + print ("WARNING: DNS server %s does not support EDNS0 " + "(RFC 6891): %s" % (forwarder, e)) + except UnresolvableRecordError as e: + logger.error("DNS server %s: %s", forwarder, e) + raise RuntimeError("DNS server %s: %s" % (forwarder, e)) return forwarders_dnssec_valid diff --git a/ipatests/test_xmlrpc/test_dns_plugin.py b/ipatests/test_xmlrpc/test_dns_plugin.py index a226c8048..50f46bc52 100644 --- a/ipatests/test_xmlrpc/test_dns_plugin.py +++ b/ipatests/test_xmlrpc/test_dns_plugin.py @@ -1751,10 +1751,11 @@ class test_dns(Declarative): 'value': None, 'summary': None, u'messages': ( - {u'message': u'DNS server 172.16.31.80 not responding.', + {u'message': lambda x: x.startswith( + u"DNS server %s: query '. SOA':" % fwd_ip), u'code': 13006, u'type':u'warning', - u'name': u'DNSServerNotRespondingWarning'}, + u'name': u'DNSServerValidationWarning'}, ), 'result': { 'idnsforwarders': [fwd_ip], |