Merge pull request #1578 from fvandenbulcke/master

feat(useDnsServer): add field in admin view, use in resolver if defin…
This commit is contained in:
Antoine Nguyen
2018-10-16 16:30:32 +02:00
committed by GitHub
6 changed files with 111 additions and 37 deletions
+3 -2
View File
@@ -2,13 +2,14 @@ FROM python:3.6-alpine
MAINTAINER Antoine Nguyen <tonio@ngyn.org>
RUN apk add --update python3-dev libffi-dev gcc musl-dev libxml2-dev libxslt-dev \
RUN apk add --update openssl python3-dev libffi-dev gcc musl-dev libxml2-dev libxslt-dev \
libressl-dev jpeg-dev \
&& rm -rf /var/cache/apk/*
WORKDIR /tmp
COPY requirements.txt /tmp
RUN pip install -r requirements.txt
COPY test-requirements.txt /tmp
RUN pip install -r requirements.txt -r test-requirements.txt
RUN mkdir /code
WORKDIR /code
+9
View File
@@ -10,6 +10,7 @@ from django import forms
from django.conf import settings
from django.utils.encoding import force_text
from django.utils.translation import ugettext as _, ugettext_lazy
from django.forms.fields import GenericIPAddressField
from modoboa.lib.form_utils import SeparatorField, YesNoField
from modoboa.lib.sysutils import exec_cmd
@@ -78,6 +79,14 @@ class AdminParametersForm(param_forms.AdminParametersForm):
)
)
use_specific_dns_server = GenericIPAddressField(
label=ugettext_lazy("Use specific DNS server"),
required=False,
help_text=ugettext_lazy(
"Use a specific DNS server instead of local server configuration"
)
)
mbsep = SeparatorField(label=ugettext_lazy("Mailboxes"))
handle_mailboxes = YesNoField(
+22 -20
View File
@@ -175,6 +175,10 @@ def get_domain_mx_list(domain):
"""Return a list of MX IP address for domain."""
result = []
logger = logging.getLogger("modoboa.admin")
dns_server = param_tools.get_global_parameter("use_specific_dns_server")
if dns_server:
dns.resolver.default_resolver.nameservers = [dns_server]
try:
dns_answers = dns.resolver.query(domain, "MX")
except dns.resolver.NXDOMAIN as e:
@@ -192,28 +196,26 @@ def get_domain_mx_list(domain):
try:
mx_domain = dns_answer.exchange.to_unicode(
omit_final_dot=True, idna_codec=IDNA_2008_UTS_46)
ip_answers = socket.getaddrinfo(
mx_domain, 25, 0, socket.SOCK_STREAM)
except socket.gaierror as e:
logger.warning(
_("Unable to lookup ip addresses for %(domain)s; "
"%(error)s")
% {"domain": mx_domain, "error": str(e)},
exc_info=e)
ip_answers = dns.resolver.query(mx_domain)
except dns.resolver.NXDOMAIN as e:
logger.error(_("No DNS records found without MX for %s")
% domain, exc_info=e)
else:
for ip_answer in ip_answers:
try:
mx_ip = ipaddress.ip_address(
smart_text(ip_answer[4][0]))
except ValueError as e:
logger.warning(
_("Invalid IP address format for %(domain)s; "
"%(addr)s")
% {"domain": mx_domain,
"addr": smart_text(ip_answer[4][0])},
exc_info=e)
else:
result.append((mx_domain, mx_ip))
for answer_set in ip_answer.response.answer:
for address in answer_set:
try:
address_smart = smart_text(address)
mx_ip = ipaddress.ip_address(address_smart)
except ValueError as e:
logger.warning(
_("Invalid IP address format for "
"%(domain)s; %(addr)s")
% {"domain": mx_domain,
"addr": smart_text(address)},
exc_info=e)
else:
result.append((mx_domain, mx_ip))
return result
+16 -6
View File
@@ -110,9 +110,19 @@ class MXTestCase(ModoTestCase):
@mock.patch("socket.getaddrinfo")
@mock.patch.object(dns.resolver.Resolver, "query")
def test_get_domain_mx_list_logging(self, mock_query, mock_getaddrinfo):
def test_get_mx_list_dsn_server(self, mock_query, mock_getaddrinfo):
"""Test to get mx list from specific DNS server."""
mock_query.side_effect = utils.mock_dns_query_result
mock_getaddrinfo.side_effect = utils.mock_ip_query_result
self.set_global_parameter("use_specific_dns_server", "123.45.67.89")
get_domain_mx_list("does-not-exist.example.com")
@mock.patch("ipaddress.ip_address")
@mock.patch.object(dns.resolver.Resolver, "query")
def test_get_domain_mx_list_logging(self, mock_query, mock_ip_address):
"""Test to get error loggins from specific DNS server."""
mock_query.side_effect = utils.mock_dns_query_result
mock_ip_address.side_effect = utils.mock_ip_address_result
with LogCapture("modoboa.admin") as log:
get_domain_mx_list("does-not-exist.example.com")
get_domain_mx_list("no-mx.example.com")
@@ -130,12 +140,12 @@ class MXTestCase(ModoTestCase):
("modoboa.admin", "WARNING",
_("DNS resolution timeout, unable to query %s at the moment")
% "timeout.example.com"),
("modoboa.admin", "ERROR",
_("No DNS records found without MX for %(domain)s")
% {"domain": "no-lookup.example.com"}),
("modoboa.admin", "WARNING",
_("Unable to lookup ip addresses for %(domain)s; %(error)s")
% {"domain": "does-not-exist.example.com", "error": ""}),
("modoboa.admin", "WARNING",
_("Invalid IP address format for %(domain)s; %(addr)s")
% {"domain": "bad-response.example.com", "addr": "BAD"}),
_("Invalid IP address format for %(domain)s; %(addr)s")
% {"domain": "bad-response.example.com", "addr": "000.0.0.0"}),
)
+59 -8
View File
@@ -3,6 +3,7 @@
"""Test utilities."""
from __future__ import unicode_literals
from dns.rrset import RRset
import socket
@@ -10,12 +11,38 @@ from dns.name import Name
from dns.rdtypes.ANY.MX import MX
from dns.resolver import NXDOMAIN, NoAnswer, NoNameservers, Timeout
class RRsetResponse(object):
def __init__(self):
self.answer = [["192.0.2.1"]]
class RRset(object):
def __init__(self):
self.response = RRsetResponse()
class RRsetResponseInvalid(object):
def __init__(self):
self.answer = [["000.0.0.0"]]
class RRsetInvalid(object):
def __init__(self):
self.response = RRsetResponseInvalid()
_MX_RECORD_1 = MX("IN", "MX", 10, Name("mx.example.com".split(".")))
_MX_RECORD_2 = MX("IN", "MX", 10, Name("mx2.example.com".split(".")))
_BAD_MX_RECORD = MX("IN", "MX", 10, Name("bad-response.example.com".split(".")))
_DNE_MX_RECORD = MX("IN", "MX", 10, Name(
"does-not-exist.example.com".split(".")))
_MX_RECORDS = [_MX_RECORD_1]
_IP_SET_RECORDS = [RRset()]
_IPV4_RECORD_1 = (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.1", 25))
_IPV4_RECORD_2 = (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.2", 25))
@@ -33,6 +60,12 @@ _POSSIBLE_DNS_RESULTS = {
"no-lookup.example.com": [_DNE_MX_RECORD],
}
_POSSIBLE_DNS_RESULTS_NO_MX = {
"does-not-exist.example.com": NXDOMAIN(),
"mx2.example.com": NXDOMAIN(),
"no-lookup.example.com": NXDOMAIN(),
"bad-response.example.com": [RRsetInvalid()],
}
_POSSIBLE_IP_RESULTS = {
"test3.com": [_IPV4_RECORD_2],
"mx2.example.com": [_IPV4_RECORD_2],
@@ -40,18 +73,36 @@ _POSSIBLE_IP_RESULTS = {
"does-not-exist.example.com": socket.gaierror(),
"192.0.2.254": socket.gaierror(),
"2001:db8:254": socket.gaierror(),
"000.0.0.0": ValueError(),
}
def mock_dns_query_result(qname, *args, **kwargs):
if qname in _POSSIBLE_DNS_RESULTS:
if isinstance(_POSSIBLE_DNS_RESULTS[qname], Exception):
raise _POSSIBLE_DNS_RESULTS[qname]
if "MX" in args:
if qname in _POSSIBLE_DNS_RESULTS:
return get_mock_dns_query_response(_POSSIBLE_DNS_RESULTS, qname)
return _MX_RECORDS
elif qname in _POSSIBLE_DNS_RESULTS_NO_MX:
return get_mock_dns_query_response(_POSSIBLE_DNS_RESULTS_NO_MX, qname)
else:
return _IP_SET_RECORDS
def get_mock_dns_query_response(responses, attributeName):
if isinstance(responses[attributeName], Exception):
raise responses[attributeName]
else:
return responses[attributeName]
def mock_ip_address_result(host, *args, **kwargs):
if host in _POSSIBLE_IP_RESULTS:
if isinstance(_POSSIBLE_IP_RESULTS[host], Exception):
raise _POSSIBLE_IP_RESULTS[host]
else:
return _POSSIBLE_DNS_RESULTS[qname]
return _MX_RECORDS
return _POSSIBLE_IP_RESULTS[host]
return _IP_RECORDS
def mock_ip_query_result(host, port, *args, **kwargs):
if host in _POSSIBLE_IP_RESULTS:
@@ -59,5 +110,5 @@ def mock_ip_query_result(host, port, *args, **kwargs):
raise _POSSIBLE_IP_RESULTS[host]
else:
return _POSSIBLE_IP_RESULTS[host]
return _IP_RECORDS
+2 -1
View File
@@ -68,7 +68,8 @@ SETTINGS_SAMPLE = {
"core-random_password_length": "8",
"admin-create_alias_on_mbox_rename": False,
"admin-dkim_keys_storage_dir": "",
"admin-dkim_default_key_length": 1024
"admin-dkim_default_key_length": 1024,
"admin-use_specific_dns_server": "193.43.55.67"
}