diff --git a/modoboa/admin/api/v2/serializers.py b/modoboa/admin/api/v2/serializers.py index 49eaba499..e3e44227a 100644 --- a/modoboa/admin/api/v2/serializers.py +++ b/modoboa/admin/api/v2/serializers.py @@ -181,6 +181,7 @@ class AdminGlobalParametersSerializer(serializers.Serializer): enable_mx_checks = serializers.BooleanField(default=True) valid_mxs = serializers.CharField(allow_blank=True) domains_must_have_authorized_mx = serializers.BooleanField(default=False) + enable_ipv6_mx_checks = serializers.BooleanField(default=True) enable_spf_checks = serializers.BooleanField(default=True) enable_dkim_checks = serializers.BooleanField(default=True) enable_dmarc_checks = serializers.BooleanField(default=True) @@ -274,6 +275,8 @@ class AdminGlobalParametersSerializer(serializers.Serializer): raise serializers.ValidationError({ "valid_mxs": _("Define at least one authorized network / address") }) + if not data.get("enable_mx_checks"): + data["enable_ipv6_mx_checks"] = False return data diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py index be18a0ab8..eb97ec8e4 100644 --- a/modoboa/admin/app_settings.py +++ b/modoboa/admin/app_settings.py @@ -50,6 +50,14 @@ class AdminParametersForm(param_forms.AdminParametersForm): ) ) + enable_ipv6_mx_checks = YesNoField( + label=gettext_lazy("Enable IPV6 checks"), + initial=True, + help_text=gettext_lazy( + "Check if MX setup for ipv6 is valid" + ) + ) + enable_spf_checks = YesNoField( label=gettext_lazy("Enable SPF checks"), initial=True, @@ -194,7 +202,8 @@ class AdminParametersForm(param_forms.AdminParametersForm): # Visibility rules visibility_rules = { "valid_mxs": "enable_mx_checks=True", - "domains_must_have_authorized_mx": "enable_mx_checks=True" + "domains_must_have_authorized_mx": "enable_mx_checks=True", + "enable_ipv6_mx_checks": "enable_mx_checks=True" } def __init__(self, *args, **kwargs): @@ -292,6 +301,8 @@ class AdminParametersForm(param_forms.AdminParametersForm): "valid_mxs", _("Define at least one authorized network / address") ) + if not cleaned_data.get("enable_mx_checks"): + cleaned_data["enable_ipv6_mx_checks"] = False return cleaned_data @@ -322,6 +333,13 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([ "does not use one of the defined addresses." ) }), + ("enable_ipv6_mx_checks", { + "label": gettext_lazy("Enable IPV6 checks"), + "display": "enable_mx_checks=true", + "help_text": gettext_lazy( + "Check if MX setup for ipv6 is valid" + ) + }), ("enable_spf_checks", { "label": gettext_lazy("Enable SPF checks"), "help_text": gettext_lazy( diff --git a/modoboa/admin/lib.py b/modoboa/admin/lib.py index cf1254feb..a1100cfc9 100644 --- a/modoboa/admin/lib.py +++ b/modoboa/admin/lib.py @@ -226,7 +226,10 @@ def get_domain_mx_list(domain): for dns_answer in dns_answers: mx_domain = dns_answer.exchange.to_unicode( omit_final_dot=True, idna_codec=IDNA_2008_UTS_46) - for rtype in ["A", "AAAA"]: + rtypes = ["A"] + if param_tools.get_global_parameter("enable_ipv6_mx_checks", app="admin"): + rtypes.append("AAAA") + for rtype in rtypes: ip_answers = get_dns_records(mx_domain, rtype, resolver) if not ip_answers: continue diff --git a/modoboa/admin/tests/test_mx.py b/modoboa/admin/tests/test_mx.py index c15f5ef95..a8718483d 100644 --- a/modoboa/admin/tests/test_mx.py +++ b/modoboa/admin/tests/test_mx.py @@ -10,7 +10,8 @@ from django.test import override_settings from django.urls import reverse from django.utils.translation import gettext as _ -from modoboa.core import factories as core_factories +from modoboa.core import ( + models as core_models, factories as core_factories) from modoboa.lib.tests import ModoTestCase from . import utils from .. import factories, models @@ -171,6 +172,29 @@ class MXTestCase(ModoTestCase): % {"domain": "bad-response.example.com", "addr": "000.0.0.0"}), ) + @mock.patch("ipaddress.ip_address") + @mock.patch.object(dns.resolver.Resolver, "resolve") + def test_ipv6_logging(self, mock_query, mock_ip_address): + """Test to check that AAAA missing record is logged consistently.""" + mock_query.side_effect = utils.mock_dns_query_result + mock_ip_address.side_effect = utils.mock_ip_address_result + localconfig = core_models.LocalConfig.objects.first() + localconfig.parameters.set_value("enable_ipv6_mx_checks", True, "admin") + localconfig.save() + with LogCapture("modoboa.admin") as log: + get_domain_mx_list("test3.com") + log.check( + ("modoboa.admin", "ERROR", + _("No AAAA record for %s") + % "mx3.example.com") + ) + localconfig = core_models.LocalConfig.objects.first() + localconfig.parameters.set_value("enable_ipv6_mx_checks", False, "admin") + localconfig.save() + with LogCapture("modoboa.admin") as log2: + get_domain_mx_list("test3.com") + log2.check() + @override_settings(DNSBL_PROVIDERS=["zen.spamhaus.org"]) class DNSBLTestCase(ModoTestCase): diff --git a/modoboa/admin/tests/utils.py b/modoboa/admin/tests/utils.py index 0bce09b2c..cac41ecf2 100644 --- a/modoboa/admin/tests/utils.py +++ b/modoboa/admin/tests/utils.py @@ -43,7 +43,7 @@ _BAD_IP_RECORD = (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("BAD", 25)) _IP_RECORDS = [_IPV4_RECORD_1, _IPV6_RECORD] _POSSIBLE_DNS_RESULTS = { - "test3.com": [_MX_RECORD_2], + "test3.com": [_MX_RECORD_3], "no-mx.example.com": NoAnswer(), "does-not-exist.example.com": NXDOMAIN(), "timeout.example.com": Timeout(), @@ -79,12 +79,20 @@ _POSSIBLE_IP_RESULTS = { "000.0.0.0": ValueError(), } +_POSSIBLE_DNS_IPV6_RESULTS = { + settings.REDIS_HOST: socket.getaddrinfo(settings.REDIS_HOST, settings.REDIS_PORT), + "mx3.example.com": NoAnswer() + } + def mock_dns_query_result(qname, *args, **kwargs): if "MX" in args: if qname in _POSSIBLE_DNS_RESULTS: return get_mock_dns_query_response(_POSSIBLE_DNS_RESULTS, qname) return _MX_RECORDS + elif "AAAA" in args: + if qname in _POSSIBLE_DNS_IPV6_RESULTS: + return get_mock_dns_query_response(_POSSIBLE_DNS_IPV6_RESULTS, qname) if qname in _POSSIBLE_DNS_RESULTS_NO_MX: return get_mock_dns_query_response(_POSSIBLE_DNS_RESULTS_NO_MX, qname) return _IP_SET_RECORDS diff --git a/modoboa/core/tests/test_views.py b/modoboa/core/tests/test_views.py index da9d7199a..f60106438 100644 --- a/modoboa/core/tests/test_views.py +++ b/modoboa/core/tests/test_views.py @@ -51,6 +51,7 @@ SETTINGS_SAMPLE = { "core-ldap_admin_groups": "", "core-ldap_groups_search_base": "", "admin-enable_mx_checks": "True", + "admin-enable_ipv6_mx_checks": "True", "core-ldap_search_base": "", "admin-valid_mxs": "", "limits-deflt_user_domains_limit": "0", @@ -264,6 +265,15 @@ sha512crypt is not one of the available choices."]}, self.assertIn( "sms_ovh_application_secret", errors["form_errors"]) + def test_mx_check_settings_clean(self): + settings = SETTINGS_SAMPLE.copy() + settings["admin-enable_mx_checks"] = False + url = reverse("core:parameters") + response = self.client.post(url, settings, format="json") + self.assertEqual(response.status_code, 200) + self.assertFalse( + param_tools.get_global_parameter("enable_ipv6_mx_checks", "admin") + ) class UserSettings(param_forms.UserParametersForm): """Stupid user settings form.""" diff --git a/modoboa/dnstools/lib.py b/modoboa/dnstools/lib.py index 5f92ab731..e9edbb7f1 100644 --- a/modoboa/dnstools/lib.py +++ b/modoboa/dnstools/lib.py @@ -5,6 +5,7 @@ import ipaddress from django.utils.translation import gettext as _ from modoboa.admin import lib as admin_lib +from modoboa.parameters import tools as param_tools from . import constants @@ -43,7 +44,10 @@ def get_dmarc_record(domain): def _get_simple_record(name): """We just want to know if name is declared.""" - for rdtype in ["A", "CNAME", "AAAA"]: + rtypes = ["A", "CNAME"] + if param_tools.get_global_parameter("enable_ipv6_mx_checks", app="admin"): + rtypes.append("AAAA") + for rdtype in rtypes: records = admin_lib.get_dns_records(name, rdtype) if records is not None: break