From 3ae8b103974d7e6aca4e2c5f093e63b67f25a6dd Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcritten@redhat.com>
Date: Fri, 18 Mar 2022 16:53:20 -0400
Subject: [PATCH] Add support for the DNS URI type
URI records are not required but if they exist they are
validated.
https://github.com/freeipa/freeipa-healthcheck/issues/222
Signed-off-by: Rob Crittenden <rcritten@redhat.com>
---
src/ipahealthcheck/ipa/idns.py | 60 ++++++++-
tests/test_ipa_dns.py | 228 +++++++++++++++++++++++++++------
2 files changed, 242 insertions(+), 46 deletions(-)
diff --git a/src/ipahealthcheck/ipa/idns.py b/src/ipahealthcheck/ipa/idns.py
index 4aa7008..64d5ddd 100644
--- a/src/ipahealthcheck/ipa/idns.py
+++ b/src/ipahealthcheck/ipa/idns.py
@@ -11,12 +11,21 @@ from ipahealthcheck.core.plugin import Result, duration
from ipahealthcheck.core import constants
from ipalib import api
-from dns import resolver
+from dns.resolver import query as resolve
logger = logging.getLogger()
+def query_uri(uri):
+ try:
+ answers = resolve(uri, rdatatype.URI)
+ except DNSException as e:
+ logger.debug("DNS record not found: %s", e.__class__.__name__)
+ answers = []
+ return answers
+
+
@registry
class IPADNSSystemRecordsCheck(IPAPlugin):
"""
@@ -32,6 +41,10 @@ class IPADNSSystemRecordsCheck(IPAPlugin):
"""Combine the SRV record and target into a unique name."""
return srv + ":" + target
+ def uri_to_name(self, uri, target):
+ """Combine the SRV record and target into a unique name."""
+ return uri + ":" + target
+
@duration
def check(self):
from ipapython.dnsutil import query_srv
@@ -43,6 +56,7 @@ class IPADNSSystemRecordsCheck(IPAPlugin):
# collect the list of expected values
txt_rec = dict()
srv_rec = dict()
+ uri_rec = dict()
a_rec = list()
aaaa_rec = list()
@@ -63,6 +77,15 @@ class IPADNSSystemRecordsCheck(IPAPlugin):
a_rec.append(rd.to_text())
elif rd.rdtype == rdatatype.AAAA:
aaaa_rec.append(rd.to_text())
+ elif rd.rdtype == rdatatype.URI:
+ if name.ToASCII() in uri_rec:
+ uri_rec[name.ToASCII()].append(
+ rd.target.decode('utf-8')
+ )
+ else:
+ uri_rec[name.ToASCII()] = [
+ rd.target.decode('utf-8')
+ ]
else:
logger.error("Unhandler rdtype %d", rd.rdtype)
@@ -95,10 +118,39 @@ class IPADNSSystemRecordsCheck(IPAPlugin):
msg='Expected SRV record missing',
key=self.srv_to_name(srv, host))
+ for uri in uri_rec:
+ logger.debug("Search DNS for URI record of %s", uri)
+ answers = query_uri(uri)
+ hosts = uri_rec[uri]
+ for answer in answers:
+ logger.debug("DNS record found: %s", answer)
+ try:
+ hosts.remove(answer.target.decode('utf-8'))
+ yield Result(
+ self, constants.SUCCESS,
+ key=self.uri_to_name(
+ uri, answer.target.decode('utf-8')
+ )
+ )
+ except ValueError:
+ yield Result(
+ self, constants.WARNING,
+ msg='Unexpected URI entry in DNS',
+ key=self.uri_to_name(
+ uri, answer.target.decode('utf-8')
+ )
+ )
+ for host in hosts:
+ yield Result(
+ self, constants.WARNING,
+ msg='Expected URI record missing',
+ key=self.uri_to_name(uri, host)
+ )
+
for txt in txt_rec:
logger.debug("Search DNS for TXT record of %s", txt)
try:
- answers = resolver.query(txt, rdatatype.TXT)
+ answers = resolve(txt, rdatatype.TXT)
except DNSException as e:
logger.debug("DNS record not found: %s", e.__class__.__name__)
answers = []
@@ -121,7 +173,7 @@ class IPADNSSystemRecordsCheck(IPAPlugin):
qname = "ipa-ca." + api.env.domain + "."
logger.debug("Search DNS for A record of %s", qname)
try:
- answers = resolver.query(qname, rdatatype.A)
+ answers = resolve(qname, rdatatype.A)
except DNSException as e:
logger.debug("DNS record not found: %s", e.__class__.__name__)
answers = []
@@ -155,7 +207,7 @@ class IPADNSSystemRecordsCheck(IPAPlugin):
qname = "ipa-ca." + api.env.domain + "."
logger.debug("Search DNS for AAAA record of %s", qname)
try:
- answers = resolver.query(qname, rdatatype.AAAA)
+ answers = resolve(qname, rdatatype.AAAA)
except DNSException as e:
logger.debug("DNS record not found: %s", e.__class__.__name__)
answers = []
diff --git a/tests/test_ipa_dns.py b/tests/test_ipa_dns.py
index 91b15c2..28243b6 100644
--- a/tests/test_ipa_dns.py
+++ b/tests/test_ipa_dns.py
@@ -27,6 +27,15 @@ from ipaserver.dns_data_management import (
IPA_DEFAULT_ADTRUST_SRV_REC
)
+try:
+ # pylint: disable=unused-import
+ from ipaserver.dns_data_management import IPA_DEFAULT_MASTER_URI_REC # noqa
+except ImportError:
+ has_uri_support = False
+else:
+ has_uri_support = True
+
+
try:
# pylint: disable=unused-import
from ipaserver.install.installutils import resolve_rrsets_nss # noqa: F401
@@ -79,6 +88,45 @@ def query_srv(qname, ad_records=False):
return rdlist
+def query_uri(hosts):
+ """
+ Return a list containing two answers, one for each uri type
+ """
+ answers = []
+ if version.MAJOR < 2 or (version.MAJOR == 2 and version.MINOR == 0):
+ m = message.Message()
+ elif version.MAJOR == 2 and version.MINOR > 0:
+ m = message.QueryMessage() # pylint: disable=E1101
+ m = message.make_response(m) # pylint: disable=E1101
+
+ rdtype = rdatatype.URI
+ for name in ('_kerberos.', '_kpasswd.'):
+ qname = DNSName(name + m_api.env.domain)
+ qname = qname.make_absolute()
+ if version.MAJOR < 2:
+ # pylint: disable=unexpected-keyword-arg
+ answer = Answer(qname, rdataclass.IN, rdtype, m,
+ raise_on_no_answer=False)
+ # pylint: enable=unexpected-keyword-arg
+ else:
+ if version.MAJOR == 2 and version.MINOR > 0:
+ question = rrset.RRset(qname, rdataclass.IN, rdtype)
+ m.question = [question]
+ answer = Answer(qname, rdataclass.IN, rdtype, m)
+
+ rl = []
+ for host in hosts:
+ rlist = rrset.from_text_list(
+ qname, 86400, rdataclass.IN,
+ rdatatype.URI, ['0 100 "krb5srv:m:tcp:%s."' % host,
+ '0 100 "krb5srv:m:udp:%s."' % host, ]
+ )
+ rl.extend(rlist)
+ answer.rrset = rl
+ answers.append(answer)
+ return answers
+
+
def gen_addrs(rdtype=rdatatype.A, num=1):
"""Generate sequential IP addresses for the ipa-ca A record lookup"""
ips = []
@@ -188,16 +236,19 @@ class TestDNSSystemRecords(BaseTest):
1. The query_srv() override returns the set of configured
servers for each type of SRV record.
- 2. fake_query() overrides dns.resolver.query to simulate
+ 2. fake_query() overrides ipahealthcheck.ipa.idns.resolve to simulate
A, AAAA and TXT record lookups.
"""
@patch(resolve_rrsets_import)
@patch('ipapython.dnsutil.query_srv')
- @patch('dns.resolver.query')
- def test_dnsrecords_single(self, mock_query, mock_query_srv, mock_rrset):
+ @patch('ipahealthcheck.ipa.idns.query_uri')
+ @patch('ipahealthcheck.ipa.idns.resolve')
+ def test_dnsrecords_single(self, mock_query, mock_query_uri,
+ mock_query_srv, mock_rrset):
"""Test single CA master, all SRV records"""
mock_query.side_effect = fake_query_one
mock_query_srv.side_effect = query_srv([m_api.env.host])
+ mock_query_uri.side_effect = query_uri([m_api.env.host])
mock_rrset.side_effect = [
resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA))
]
@@ -219,7 +270,11 @@ class TestDNSSystemRecords(BaseTest):
self.results = capture_results(f)
- assert len(self.results) == 10
+ if has_uri_support:
+ expected = 14
+ else:
+ expected = 10
+ assert len(self.results) == expected
for result in self.results.results:
assert result.result == constants.SUCCESS
@@ -228,13 +283,19 @@ class TestDNSSystemRecords(BaseTest):
@patch(resolve_rrsets_import)
@patch('ipapython.dnsutil.query_srv')
- @patch('dns.resolver.query')
- def test_dnsrecords_two(self, mock_query, mock_query_srv, mock_rrset):
+ @patch('ipahealthcheck.ipa.idns.query_uri')
+ @patch('ipahealthcheck.ipa.idns.resolve')
+ def test_dnsrecords_two(self, mock_query, mock_query_uri,
+ mock_query_srv, mock_rrset):
"""Test two CA masters, all SRV records"""
mock_query_srv.side_effect = query_srv([
m_api.env.host,
'replica.' + m_api.env.domain
])
+ mock_query_uri.side_effect = query_uri([
+ m_api.env.host,
+ 'replica.' + m_api.env.domain
+ ])
mock_query.side_effect = fake_query_two
mock_rrset.side_effect = [
resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
@@ -267,7 +328,11 @@ class TestDNSSystemRecords(BaseTest):
self.results = capture_results(f)
- assert len(self.results) == 19
+ if has_uri_support:
+ expected = 27
+ else:
+ expected = 19
+ assert len(self.results) == expected
for result in self.results.results:
assert result.result == constants.SUCCESS
@@ -276,14 +341,21 @@ class TestDNSSystemRecords(BaseTest):
@patch(resolve_rrsets_import)
@patch('ipapython.dnsutil.query_srv')
- @patch('dns.resolver.query')
- def test_dnsrecords_three(self, mock_query, mock_query_srv, mock_rrset):
+ @patch('ipahealthcheck.ipa.idns.query_uri')
+ @patch('ipahealthcheck.ipa.idns.resolve')
+ def test_dnsrecords_three(self, mock_query, mock_query_uri,
+ mock_query_srv, mock_rrset):
"""Test three CA masters, all SRV records"""
mock_query_srv.side_effect = query_srv([
m_api.env.host,
'replica.' + m_api.env.domain,
'replica2.' + m_api.env.domain
])
+ mock_query_uri.side_effect = query_uri([
+ m_api.env.host,
+ 'replica.' + m_api.env.domain,
+ 'replica2.' + m_api.env.domain
+ ])
mock_query.side_effect = fake_query_three
mock_rrset.side_effect = [
resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
@@ -325,7 +397,11 @@ class TestDNSSystemRecords(BaseTest):
self.results = capture_results(f)
- assert len(self.results) == 28
+ if has_uri_support:
+ expected = 40
+ else:
+ expected = 28
+ assert len(self.results) == expected
for result in self.results.results:
assert result.result == constants.SUCCESS
@@ -334,15 +410,21 @@ class TestDNSSystemRecords(BaseTest):
@patch(resolve_rrsets_import)
@patch('ipapython.dnsutil.query_srv')
- @patch('dns.resolver.query')
- def test_dnsrecords_three_mixed(self, mock_query, mock_query_srv,
- mock_rrset):
+ @patch('ipahealthcheck.ipa.idns.query_uri')
+ @patch('ipahealthcheck.ipa.idns.resolve')
+ def test_dnsrecords_three_mixed(self, mock_query, mock_query_uri,
+ mock_query_srv, mock_rrset):
"""Test three masters, only one with a CA, all SRV records"""
mock_query_srv.side_effect = query_srv([
m_api.env.host,
'replica.' + m_api.env.domain,
'replica2.' + m_api.env.domain
])
+ mock_query_uri.side_effect = query_uri([
+ m_api.env.host,
+ 'replica.' + m_api.env.domain,
+ 'replica2.' + m_api.env.domain
+ ])
mock_query.side_effect = fake_query_one
mock_rrset.side_effect = [
resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
@@ -382,7 +464,11 @@ class TestDNSSystemRecords(BaseTest):
self.results = capture_results(f)
- assert len(self.results) == 24
+ if has_uri_support:
+ expected = 36
+ else:
+ expected = 24
+ assert len(self.results) == expected
for result in self.results.results:
assert result.result == constants.SUCCESS
@@ -390,9 +476,10 @@ class TestDNSSystemRecords(BaseTest):
@patch(resolve_rrsets_import)
@patch('ipapython.dnsutil.query_srv')
- @patch('dns.resolver.query')
- def test_dnsrecords_missing_server(self, mock_query, mock_query_srv,
- mock_rrset):
+ @patch('ipahealthcheck.ipa.idns.query_uri')
+ @patch('ipahealthcheck.ipa.idns.resolve')
+ def test_dnsrecords_missing_server(self, mock_query, mock_query_uri,
+ mock_query_srv, mock_rrset):
"""Drop one of the masters from query_srv
This will simulate missing SRV records and cause a number of
@@ -403,6 +490,11 @@ class TestDNSSystemRecords(BaseTest):
'replica.' + m_api.env.domain
# replica2 is missing
])
+ mock_query_uri.side_effect = query_uri([
+ m_api.env.host,
+ 'replica.' + m_api.env.domain,
+ 'replica2.' + m_api.env.domain
+ ])
mock_query.side_effect = fake_query_three
mock_rrset.side_effect = [
resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
@@ -444,21 +536,30 @@ class TestDNSSystemRecords(BaseTest):
self.results = capture_results(f)
- assert len(self.results) == 28
+ if has_uri_support:
+ expected = 40
+ else:
+ expected = 28
+ assert len(self.results) == expected
ok = get_results_by_severity(self.results.results, constants.SUCCESS)
warn = get_results_by_severity(self.results.results, constants.WARNING)
- assert len(ok) == 21
- assert len(warn) == 7
+ if has_uri_support:
+ assert len(ok) == 33
+ assert len(warn) == 7
+ else:
+ assert len(ok) == 21
+ assert len(warn) == 7
for result in warn:
assert result.kw.get('msg') == 'Expected SRV record missing'
@patch(resolve_rrsets_import)
@patch('ipapython.dnsutil.query_srv')
- @patch('dns.resolver.query')
- def test_dnsrecords_missing_ipa_ca(self, mock_query, mock_query_srv,
- mock_rrset):
+ @patch('ipahealthcheck.ipa.idns.query_uri')
+ @patch('ipahealthcheck.ipa.idns.resolve')
+ def test_dnsrecords_missing_ipa_ca(self, mock_query, mock_query_uri,
+ mock_query_srv, mock_rrset):
"""Drop one of the masters from query_srv
This will simulate missing SRV records and cause a number of
@@ -469,6 +570,11 @@ class TestDNSSystemRecords(BaseTest):
'replica.' + m_api.env.domain,
'replica2.' + m_api.env.domain
])
+ mock_query_uri.side_effect = query_uri([
+ m_api.env.host,
+ 'replica.' + m_api.env.domain,
+ 'replica2.' + m_api.env.domain
+ ])
mock_query.side_effect = fake_query_two
mock_rrset.side_effect = [
resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
@@ -510,12 +616,20 @@ class TestDNSSystemRecords(BaseTest):
self.results = capture_results(f)
- assert len(self.results) == 28
+ if has_uri_support:
+ expected = 40
+ else:
+ expected = 28
+ assert len(self.results) == expected
ok = get_results_by_severity(self.results.results, constants.SUCCESS)
warn = get_results_by_severity(self.results.results, constants.WARNING)
- assert len(ok) == 26
- assert len(warn) == 2
+ if has_uri_support:
+ assert len(ok) == 38
+ assert len(warn) == 2
+ else:
+ assert len(ok) == 26
+ assert len(warn) == 2
for result in warn:
assert re.match(
@@ -527,9 +641,10 @@ class TestDNSSystemRecords(BaseTest):
@patch(resolve_rrsets_import)
@patch('ipapython.dnsutil.query_srv')
- @patch('dns.resolver.query')
- def test_dnsrecords_extra_srv(self, mock_query, mock_query_srv,
- mock_rrset):
+ @patch('ipahealthcheck.ipa.idns.query_uri')
+ @patch('ipahealthcheck.ipa.idns.resolve')
+ def test_dnsrecords_extra_srv(self, mock_query, mock_query_uri,
+ mock_query_srv, mock_rrset):
"""An extra SRV record set exists, report it.
Add an extra master to the query_srv() which will generate
@@ -541,6 +656,11 @@ class TestDNSSystemRecords(BaseTest):
'replica2.' + m_api.env.domain,
'replica3.' + m_api.env.domain
])
+ mock_query_uri.side_effect = query_uri([
+ m_api.env.host,
+ 'replica.' + m_api.env.domain,
+ 'replica2.' + m_api.env.domain,
+ ])
mock_query.side_effect = fake_query_three
mock_rrset.side_effect = [
resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA)),
@@ -584,12 +704,20 @@ class TestDNSSystemRecords(BaseTest):
self.results = capture_results(f)
- assert len(self.results) == 35
+ if has_uri_support:
+ expected = 47
+ else:
+ expected = 35
+ assert len(self.results) == expected
ok = get_results_by_severity(self.results.results, constants.SUCCESS)
warn = get_results_by_severity(self.results.results, constants.WARNING)
- assert len(ok) == 28
- assert len(warn) == 7
+ if has_uri_support:
+ assert len(ok) == 40
+ assert len(warn) == 7
+ else:
+ assert len(ok) == 28
+ assert len(warn) == 7
for result in warn:
assert result.kw.get('msg') == \
@@ -597,12 +725,14 @@ class TestDNSSystemRecords(BaseTest):
@patch(resolve_rrsets_import)
@patch('ipapython.dnsutil.query_srv')
- @patch('dns.resolver.query')
- def test_dnsrecords_bad_realm(self, mock_query, mock_query_srv,
- mock_rrset):
+ @patch('ipahealthcheck.ipa.idns.query_uri')
+ @patch('ipahealthcheck.ipa.idns.resolve')
+ def test_dnsrecords_bad_realm(self, mock_query, mock_query_uri,
+ mock_query_srv, mock_rrset):
"""Unexpected Kerberos TXT record"""
mock_query.side_effect = fake_query_one_txt
mock_query_srv.side_effect = query_srv([m_api.env.host])
+ mock_query_uri.side_effect = query_uri([m_api.env.host])
mock_rrset.side_effect = [
resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA))
]
@@ -624,12 +754,20 @@ class TestDNSSystemRecords(BaseTest):
self.results = capture_results(f)
- assert len(self.results) == 10
+ if has_uri_support:
+ expected = 14
+ else:
+ expected = 10
+ assert len(self.results) == expected
ok = get_results_by_severity(self.results.results, constants.SUCCESS)
warn = get_results_by_severity(self.results.results, constants.WARNING)
- assert len(ok) == 9
- assert len(warn) == 1
+ if has_uri_support:
+ assert len(ok) == 13
+ assert len(warn) == 1
+ else:
+ assert len(ok) == 9
+ assert len(warn) == 1
result = warn[0]
assert result.kw.get('msg') == 'expected realm missing'
@@ -637,11 +775,13 @@ class TestDNSSystemRecords(BaseTest):
@patch(resolve_rrsets_import)
@patch('ipapython.dnsutil.query_srv')
- @patch('dns.resolver.query')
- def test_dnsrecords_one_with_ad(self, mock_query, mock_query_srv,
- mock_rrset):
+ @patch('ipahealthcheck.ipa.idns.query_uri')
+ @patch('ipahealthcheck.ipa.idns.resolve')
+ def test_dnsrecords_one_with_ad(self, mock_query, mock_query_uri,
+ mock_query_srv, mock_rrset):
mock_query.side_effect = fake_query_one
mock_query_srv.side_effect = query_srv([m_api.env.host], True)
+ mock_query_uri.side_effect = query_uri([m_api.env.host])
mock_rrset.side_effect = [
resolve_rrsets(m_api.env.host, (rdatatype.A, rdatatype.AAAA))
]
@@ -664,7 +804,11 @@ class TestDNSSystemRecords(BaseTest):
self.results = capture_results(f)
- assert len(self.results) == 16
+ if has_uri_support:
+ expected = 20
+ else:
+ expected = 16
+ assert len(self.results) == expected
for result in self.results.results:
assert result.result == constants.SUCCESS
--
2.31.1