summaryrefslogtreecommitdiffstats
path: root/ipalib
diff options
context:
space:
mode:
authorMartin Basti <mbasti@redhat.com>2015-04-22 15:29:21 +0200
committerPetr Vobornik <pvoborni@redhat.com>2015-06-11 13:12:31 +0200
commit9aa6124b39267148c4c1b9a8ee4209fb859b9c42 (patch)
treee92fce6095a192fae928e4ba64b022d68878ab6e /ipalib
parentc9cbb1493a8c9e10020c7f2104a345cd43535259 (diff)
downloadfreeipa-9aa6124b39267148c4c1b9a8ee4209fb859b9c42.tar.gz
freeipa-9aa6124b39267148c4c1b9a8ee4209fb859b9c42.tar.xz
freeipa-9aa6124b39267148c4c1b9a8ee4209fb859b9c42.zip
DNSSEC: Improve global forwarders validation
Validation now provides more detailed information and less false positives failures. https://fedorahosted.org/freeipa/ticket/4657 Reviewed-By: David Kupka <dkupka@redhat.com> Reviewed-By: Petr Spacek <pspacek@redhat.com>
Diffstat (limited to 'ipalib')
-rw-r--r--ipalib/messages.py23
-rw-r--r--ipalib/plugins/dns.py63
-rw-r--r--ipalib/util.py130
3 files changed, 164 insertions, 52 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):