From 5ab6a597b0e23b604e4d6e339a9d3784abc57217 Mon Sep 17 00:00:00 2001 From: Andreas Vogler Date: Thu, 16 May 2019 10:40:17 +0200 Subject: [PATCH] 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 --- CHANGELOG.md | 4 +- .../certbot_dns_rfc2136/__init__.py | 6 ++- .../certbot_dns_rfc2136/dns_rfc2136.py | 50 +++++++++++++------ .../certbot_dns_rfc2136/dns_rfc2136_test.py | 29 ++++++++++- 4 files changed, 68 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac7f34a09..2f699feec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py index 12b360959..cebff2841 100644 --- a/certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py +++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/__init__.py @@ -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 diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py index 2061374e0..5db8c3020 100644 --- a/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py +++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py @@ -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): """ diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py index d800f1ec7..bed3445b6 100644 --- a/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py +++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py @@ -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)