DNSBL checks for domains.

see #879
This commit is contained in:
Antoine Nguyen
2016-06-08 17:08:08 +02:00
parent ae8c6bffbe
commit 7f233b2f26
16 changed files with 312 additions and 6 deletions
+83
View File
@@ -0,0 +1,83 @@
"""Admin constants."""
DNSBL_PROVIDERS = [
"aspews.ext.sorbs.net",
"b.barracudacentral.org",
"bad.psky.me",
"bl.deadbeef.com",
"bl.emailbasura.org",
"bl.spamcannibal.org",
"bl.spamcop.net",
"blackholes.five-ten-sg.com",
"blacklist.woody.ch",
"bogons.cymru.com",
"cbl.abuseat.org",
"cdl.anti-spam.org.cn",
"combined.abuse.ch",
"combined.rbl.msrbl.net",
"db.wpbl.info",
"dnsbl-1.uceprotect.net",
"dnsbl-2.uceprotect.net",
"dnsbl-3.uceprotect.net",
"dnsbl.cyberlogic.net",
"dnsbl.inps.de",
"dnsbl.njabl.org",
"dnsbl.sorbs.net",
"drone.abuse.ch",
"duinv.aupads.org",
"dul.dnsbl.sorbs.net",
"dul.ru",
"dyna.spamrats.com",
"dynip.rothen.com",
"http.dnsbl.sorbs.net"
"images.rbl.msrbl.net",
"ips.backscatterer.org",
"ix.dnsbl.manitu.net",
"korea.services.net",
"misc.dnsbl.sorbs.net",
"noptr.spamrats.com",
"ohps.dnsbl.net.au",
"omrs.dnsbl.net.au",
"orvedb.aupads.org",
"osps.dnsbl.net.au",
"osrs.dnsbl.net.au",
"owfs.dnsbl.net.au",
"owps.dnsbl.net.au"
"pbl.spamhaus.org",
"phishing.rbl.msrbl.net",
"probes.dnsbl.net.au"
"proxy.bl.gweep.ca",
"proxy.block.transip.nl",
"psbl.surriel.com",
"rbl.interserver.net",
"rdts.dnsbl.net.au",
"relays.bl.gweep.ca",
"relays.bl.kundenserver.de",
"relays.nether.net",
"residential.block.transip.nl",
"ricn.dnsbl.net.au",
"rmst.dnsbl.net.au",
"sbl.spamhaus.org",
"short.rbl.jp",
"smtp.dnsbl.sorbs.net",
"socks.dnsbl.sorbs.net",
"spam.abuse.ch",
"spam.dnsbl.sorbs.net",
"spam.rbl.msrbl.net",
"spam.spamrats.com",
"spamlist.or.kr",
"spamrbl.imp.ch",
"t3direct.dnsbl.net.au",
"tor.dnsbl.sectoor.de",
"torserver.tor.dnsbl.sectoor.de",
"ubl.lashback.com",
"ubl.unsubscore.com",
"virbl.bit.nl",
"virus.rbl.jp",
"virus.rbl.msrbl.net",
"web.dnsbl.sorbs.net",
"wormrbl.imp.ch",
"xbl.spamhaus.org",
"zen.spamhaus.org",
"zombie.dnsbl.sorbs.net",
]
+2 -1
View File
@@ -84,7 +84,8 @@ def get_domains(user, domfilter=None, searchquery=None, **extrafilters):
:rtype: list
:return: a list of domains and/or relay domains
"""
domains = Domain.objects.get_for_admin(user)
domains = (
Domain.objects.get_for_admin(user).prefetch_related("domainalias_set"))
if domfilter:
domains = domains.filter(type=domfilter)
if searchquery is not None:
@@ -2,6 +2,7 @@
from subcommand.base import SubcommandCommand
from .subcommands._dnsbl import CheckDNSBLCommand
from .subcommands._export import ExportCommand
from .subcommands._import import ImportCommand
@@ -17,4 +18,5 @@ class Command(SubcommandCommand):
subcommands = {
"export": ExportCommand,
"import": ImportCommand,
"check_dnsbl": CheckDNSBLCommand,
}
@@ -0,0 +1,75 @@
"""Management command to check defined domains against DNSBL providers."""
import ipaddress
import dns.resolver
import gevent
from gevent import socket
from django.core.management.base import BaseCommand
from modoboa.admin import constants
from modoboa.admin import models
class CheckDNSBLCommand(BaseCommand):
"""Command class."""
help = "Check defined domains against DNSBL providers."
def add_arguments(self, parser):
"""Add extra arguments to command."""
parser.add_argument(
"--timeout", type=int, default=3,
help="Timeout used for queries.")
def query(self, ip_list, provider):
"""Check given IP against given provider."""
results = {}
for ip in ip_list:
reverse = ".".join(reversed(ip.split(".")))
pattern = "{}.{}".format(reverse, provider)
try:
results[ip] = socket.gethostbyname(pattern)
except socket.gaierror:
results[ip] = False
return provider, results
def check_domain(self, domain, timeout):
"""Check specified domain."""
resolver = dns.resolver.Resolver()
answers = resolver.query(domain.name, "MX")
ip_list = []
for answer in answers:
address = None
try:
ipaddress.ip_address(str(answer.exchange))
except ValueError:
try:
address = socket.gethostbyname(str(answer.exchange))
except socket.gaierror:
pass
else:
address = str(answer.exchange)
finally:
if address is not None:
ip_list.append(address)
jobs = [
gevent.spawn(self.query, ip_list, provider)
for provider in constants.DNSBL_PROVIDERS]
gevent.joinall(jobs, timeout)
for job in jobs:
if not job.successful():
continue
provider, results = job.value
for ip in results.keys():
result = "" if not results[ip] else results[ip]
models.DNSBLResult.objects.update_or_create(
domain=domain, provider=provider, mx=ip,
status=result)
def handle(self, *args, **options):
"""Command entry point."""
for domain in models.Domain.objects.filter(enabled=True):
self.check_domain(domain, options.get("timeout"))
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-06-08 12:23
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('admin', '0003_auto_20151118_1215'),
]
operations = [
migrations.CreateModel(
name='DNSBLResult',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('provider', models.CharField(max_length=254)),
('mx', models.GenericIPAddressField()),
('status', models.CharField(blank=True, max_length=45)),
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='admin.Domain')),
],
),
migrations.AlterUniqueTogether(
name='dnsblresult',
unique_together=set([('domain', 'provider', 'mx')]),
),
]
+2 -2
View File
@@ -1,10 +1,10 @@
from .base import AdminObject
from .domain import Domain
from .domain import Domain, DNSBLResult
from .domain_alias import DomainAlias
from .mailbox import Mailbox, Quota, MailboxOperation
from .alias import Alias, AliasRecipient
__all__ = [
'Domain', 'DomainAlias', 'Mailbox', 'Quota', 'Alias',
'Domain', 'DNSBLResult', 'DomainAlias', 'Mailbox', 'Quota', 'Alias',
'MailboxOperation', 'AdminObject', 'AliasRecipient'
]
+27
View File
@@ -101,6 +101,10 @@ class Domain(AdminObject):
def aliases(self):
return self.domainalias_set
def is_in_dnsbl(self):
"""Shortcut to DNSBL results."""
return self.dnsblresult_set.blacklisted().exists()
def add_admin(self, account):
"""Add a new administrator to this domain.
@@ -207,3 +211,26 @@ class Domain(AdminObject):
domalias.post_create(creator)
reversion.register(Domain)
class DNSBLQuerySet(models.QuerySet):
"""Custom manager for DNSBLResultManager."""
def blacklisted(self):
"""Return blacklisted results."""
return self.exclude(status="")
class DNSBLResult(models.Model):
"""Store a DNSBL query result."""
domain = models.ForeignKey(Domain)
provider = models.CharField(max_length=254)
mx = models.GenericIPAddressField()
status = models.CharField(max_length=45, blank=True)
objects = models.Manager.from_queryset(DNSBLQuerySet)()
class Meta:
app_label = "admin"
unique_together = [("domain", "provider", "mx")]
@@ -0,0 +1,30 @@
{% extends "common/generic_modal.html" %}
{% load i18n %}
{% block modalbody %}
{% if domain.is_in_dnsbl %}
<table class="table">
<thead>
<tr>
<th>{% trans "Provider" %}</th>
<th>{% trans "Address" %}</th>
<th>{% trans "Status" %}</th>
</tr>
</thead>
<tbody>
{% for result in domain.dnsblresult_set.blacklisted %}
<tr>
<td>{{ result.provider }}</td>
<td>{{ result.mx }}</td>
<td>
{% if result.status %}<span class="label label-danger">{% trans "listed" %}</span>{% else %}<span class="label label-success"></span>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="alert alert-success">
{% trans "Everything is fine, this domain is not listed." %}</div>
{% endif %}
{% endblock %}
@@ -17,9 +17,17 @@
<div class="col-xs-12 col-sm-6">
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">{% trans "Information" %}</h3></div>
<div class="panel-heading">
<h3 class="panel-title">{% trans "Information" %}</h3>
</div>
<div class="panel-body">
<div class="row"><span class="title">{% trans "Creation date" %}</span><span>{{ domain.creation }}</span></div>
<div class="row">
<span class="title">{% trans "Authentication" %}</span>
<span class="label label-{% if domain.is_in_dnsbl %}danger{% else %}success{% endif %}"><a href="{% url 'admin:dnsbl_domain_detail' domain.pk %}" data-toggle="ajaxmodal">DNSBL</a></span>
</div>
<div class="row">
<span class="title">{% trans "Creation date" %}</span><span>{{ domain.creation }}</span>
</div>
<div class="row"><span class="title">{% trans "Last modification date" %}</span><span>{{ domain.last_modification }}</span></div>
<div class="row"><span class="title">{% trans "Domain aliases" %}</span><span>{{ domain.domainalias_count }}</span></div>
<div class="row"><span class="title"><a href="{% url 'admin:identity_list' %}#list/?searchquery=@{{ object.name }}">{% trans "Mailboxes" %}</a></span><span>{{ domain.mailbox_count }}</span></div>
@@ -25,5 +25,6 @@ $(document).ready(function() {
<th id="name" class="sortable" data-sort_order="name" width="">{% trans "Name" %}</th>
<th id="aliases" width="">{% trans "Alias(es)" %}</th>
<th id="tags" class="sortable" data-sort_order="tags" width="">{% trans "Tags" %}</th>
<th id="tags" width="">{% trans "Authentication" %}</th>
<th id="actions" width="">{% trans "Actions" %}</th>
{% endblock %}
@@ -5,6 +5,11 @@
<td name="name"><a href="{% url 'admin:domain_detail' domain.id %}">{{ domain }}</a>{% if not domain.enabled %} ({% trans "disabled" %}){% endif %}</td>
<td name="aliases">{% domain_aliases domain %} </td>
<td name="tags">{% render_tags domain.tags %}</td>
<td name="auth">
<small>
<a href="{% url 'admin:dnsbl_domain_detail' domain.pk %}" data-toggle="ajaxmodal"><span class="label label-mini label-{% if domain.is_in_dnsbl %}danger{% else %}success{% endif %}">DNSBL</span></a>
</small>
</td>
<td name="actions">{% domain_actions user domain %}</td>
</tr>
{% endfor %}
+2
View File
@@ -12,6 +12,8 @@ urlpatterns = patterns(
url(r'^domains/$', 'domains', name="domain_list"),
url(r'^domains/(?P<pk>\d+)/$', views.DomainDetailView.as_view(),
name="domain_detail"),
url(r'^domains/(?P<pk>\d+)/dnsbl/$', views.DNSBLDomainDetailView.as_view(),
name="dnsbl_domain_detail"),
url(r'^domains/list/$', '_domains', name="_domain_list"),
url(r'^domains/flatlist/$', 'domains_list', name="domain_flat_list"),
url(r'^domains/stats/$', 'domain_statistics', name="domain_statistics"),
+2
View File
@@ -1,8 +1,10 @@
"""Shortcuts."""
from .dnsbl import DNSBLDomainDetailView
from .domain import DomainDetailView
__all__ = [
"DNSBLDomainDetailView",
"DomainDetailView",
]
+32
View File
@@ -0,0 +1,32 @@
"""DNSBL related views."""
from django.views import generic
from django.utils.translation import ugettext as _
from django.contrib.auth import mixins as auth_mixins
from .. import models
class DNSBLDomainDetailView(
auth_mixins.PermissionRequiredMixin, generic.DetailView):
"""DetailView for Domain."""
model = models.Domain
permission_required = "admin.view_domain"
template_name = "admin/dnsbl_domain_detail.html"
def get_queryset(self):
"""Add some prefetching."""
return (
super(DNSBLDomainDetailView, self).get_queryset()
.prefetch_related("dnsblresult_set"))
def get_context_data(self, **kwargs):
"""Add extra variables."""
context = super(DNSBLDomainDetailView, self).get_context_data(**kwargs)
context.update({
"title": _("DNSBL summary for {}").format(self.object.name)
})
return context
+7
View File
@@ -164,6 +164,13 @@ class DomainDetailView(
model = Domain
permission_required = "admin.view_domain"
def get_queryset(self):
"""Add some prefetching."""
return (
super(DomainDetailView, self).get_queryset()
.prefetch_related("domainalias_set", "mailbox_set", "alias_set")
)
def get_context_data(self, **kwargs):
"""Include extra widgets."""
context = super(DomainDetailView, self).get_context_data(**kwargs)
+2 -1
View File
@@ -15,4 +15,5 @@ progressbar==2.3
djangorestframework==3.3.1
django-rest-swagger==0.3.5
django-subcommand2==0.1.1
gevent==1.1.1
ipaddress