Add an option to dns_rfc2136 plugin to specify an authorative base domain. (#7029)

* Add an option to dns_rfc2136 plugin to explicitly specify an authorative base domain.

* Updated CHANGELOG mentioning added base domain option

* Made the comment on the new option more clear on auto-detection

* Updated comment on how the authorative base domain is determined
This commit is contained in:
Andreas Vogler
2019-05-16 10:40:17 +02:00
committed by Adrien Ferrand
parent 9a7f774706
commit 5ab6a597b0
4 changed files with 68 additions and 21 deletions
+3 -1
View File
@@ -6,7 +6,9 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
### Added
*
* dns_rfc2136 plugin now supports explicitly specifing an authorative
base domain for cases when the automatic method does not work (e.g.
Split horizon DNS)
### Changed
@@ -21,8 +21,8 @@ Credentials
-----------
Use of this plugin requires a configuration file containing the target DNS
server and optional port that supports RFC 2136 Dynamic Updates, the name
of the TSIG key, the TSIG key secret itself and the algorithm used if it's
server, optional authorative domain and optional port that supports RFC 2136 Dynamic Updates,
the name of the TSIG key, the TSIG key secret itself and the algorithm used if it's
different to HMAC-MD5.
.. code-block:: ini
@@ -33,6 +33,8 @@ different to HMAC-MD5.
dns_rfc2136_server = 192.0.2.1
# Target DNS port
dns_rfc2136_port = 53
# Authorative domain (optional, will try to auto-detect if missing)
dns_rfc2136_base_domain = example.com
# TSIG key name
dns_rfc2136_name = keyname.
# TSIG key secret
@@ -79,25 +79,33 @@ class Authenticator(dns_common.DNSAuthenticator):
self._get_rfc2136_client().del_txt_record(validation_name, validation)
def _get_rfc2136_client(self):
key = _RFC2136Key(self.credentials.conf('name'),
self.credentials.conf('secret'),
self.ALGORITHMS.get(self.credentials.conf('algorithm'),
dns.tsig.HMAC_MD5))
return _RFC2136Client(self.credentials.conf('server'),
int(self.credentials.conf('port') or self.PORT),
self.credentials.conf('name'),
self.credentials.conf('secret'),
self.ALGORITHMS.get(self.credentials.conf('algorithm'),
dns.tsig.HMAC_MD5))
key,
self.credentials.conf('base-domain'))
class _RFC2136Key(object):
def __init__(self, name, secret, algorithm):
self.name = name
self.secret = secret
self.algorithm = algorithm
class _RFC2136Client(object):
"""
Encapsulates all communication with the target DNS server.
"""
def __init__(self, server, port, key_name, key_secret, key_algorithm):
def __init__(self, server, port, base_domain, key):
self.server = server
self.port = port
self.keyring = dns.tsigkeyring.from_text({
key_name: key_secret
key.name: key.secret
})
self.algorithm = key_algorithm
self.algorithm = key.algorithm
self.base_domain = base_domain
def add_txt_record(self, record_name, record_content, record_ttl):
"""
@@ -171,23 +179,33 @@ class _RFC2136Client(object):
def _find_domain(self, record_name):
"""
Find the closest domain with an SOA record for a given domain name.
If 'base_domain' option is specified check if the requested domain matches this base domain
and return it. If not explicitly specified find the closest domain with an SOA record for
the given domain name.
:param str record_name: The record name for which to find the closest SOA record.
:param str record_name: The record name for which to find the base domain.
:returns: The domain, if found.
:rtype: str
:raises certbot.errors.PluginError: if no SOA record can be found.
"""
domain_name_guesses = dns_common.base_domain_name_guesses(record_name)
if self.base_domain:
if not record_name.endswith(self.base_domain):
raise errors.PluginError('Requested domain {0} does not match specified base '
'domain {1}.'
.format(record_name, self.base_domain))
else:
return self.base_domain
else:
domain_name_guesses = dns_common.base_domain_name_guesses(record_name)
# Loop through until we find an authoritative SOA record
for guess in domain_name_guesses:
if self._query_soa(guess):
return guess
# Loop through until we find an authoritative SOA record
for guess in domain_name_guesses:
if self._query_soa(guess):
return guess
raise errors.PluginError('Unable to determine base domain for {0} using names: {1}.'
.format(record_name, domain_name_guesses))
raise errors.PluginError('Unable to determine base domain for {0} using names: {1}.'
.format(record_name, domain_name_guesses))
def _query_soa(self, domain_name):
"""
@@ -73,9 +73,12 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
class RFC2136ClientTest(unittest.TestCase):
def setUp(self):
from certbot_dns_rfc2136.dns_rfc2136 import _RFC2136Client
from certbot_dns_rfc2136.dns_rfc2136 import _RFC2136Client, _RFC2136Key
self.rfc2136_client = _RFC2136Client(SERVER, PORT, NAME, SECRET, dns.tsig.HMAC_MD5)
self.rfc2136_client = _RFC2136Client(SERVER,
PORT,
None,
_RFC2136Key(NAME, SECRET, dns.tsig.HMAC_MD5))
@mock.patch("dns.query.tcp")
def test_add_txt_record(self, query_mock):
@@ -162,6 +165,28 @@ class RFC2136ClientTest(unittest.TestCase):
self.rfc2136_client._find_domain,
'foo.bar.'+DOMAIN)
def test_find_domain_with_base(self):
# _query_soa | pylint: disable=protected-access
self.rfc2136_client._query_soa = mock.MagicMock(side_effect=[False, False, True])
self.rfc2136_client.base_domain = 'bar.' + DOMAIN
# _find_domain | pylint: disable=protected-access
domain = self.rfc2136_client._find_domain('foo.bar.' + DOMAIN)
self.assertTrue(domain == 'bar.' + DOMAIN)
def test_find_domain_with_wrong_base(self):
# _query_soa | pylint: disable=protected-access
self.rfc2136_client._query_soa = mock.MagicMock(side_effect=[False, False, True])
self.rfc2136_client.base_domain = 'wrong.' + DOMAIN
self.assertRaises(
errors.PluginError,
# _find_domain | pylint: disable=protected-access
self.rfc2136_client._find_domain,
'foo.bar.' + DOMAIN)
@mock.patch("dns.query.udp")
def test_query_soa_found(self, query_mock):
query_mock.return_value = mock.MagicMock(answer=[mock.MagicMock()], flags=dns.flags.AA)