From d8feb1b72ae605100e8656f39e874209fa43172f Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 7 Apr 2020 01:53:05 +0200 Subject: [PATCH] [enh] Add RBL check --- data/hooks/diagnosis/24-mail.py | 89 ++++++++++++++++++++++++++++++++- locales/en.json | 3 ++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/data/hooks/diagnosis/24-mail.py b/data/hooks/diagnosis/24-mail.py index 3f9517bb0..731267593 100644 --- a/data/hooks/diagnosis/24-mail.py +++ b/data/hooks/diagnosis/24-mail.py @@ -1,9 +1,42 @@ #!/usr/bin/env python import os +import dns.resolver + +from moulinette.utils.network import download_text from yunohost.diagnosis import Diagnoser +DEFAULT_BLACKLIST = [ + ('zen.spamhaus.org' , 'Spamhaus SBL, XBL and PBL' ), + ('dnsbl.sorbs.net' , 'SORBS aggregated' ), + ('safe.dnsbl.sorbs.net' , "'safe' subset of SORBS aggregated"), + ('ix.dnsbl.manitu.net' , 'Heise iX NiX Spam' ), + ('babl.rbl.webiron.net' , 'Bad Abuse' ), + ('cabl.rbl.webiron.net' , 'Chronicly Bad Abuse' ), + ('truncate.gbudb.net' , 'Exclusively Spam/Malware' ), + ('dnsbl-1.uceprotect.net' , 'Trapserver Cluster' ), + ('cbl.abuseat.org' , 'Net of traps' ), + ('dnsbl.cobion.com' , 'used in IBM products' ), + ('psbl.surriel.com' , 'passive list, easy to unlist' ), + ('dnsrbl.org' , 'Real-time black list' ), + ('db.wpbl.info' , 'Weighted private' ), + ('bl.spamcop.net' , 'Based on spamcop users' ), + ('dyna.spamrats.com' , 'Dynamic IP addresses' ), + ('spam.spamrats.com' , 'Manual submissions' ), + ('auth.spamrats.com' , 'Suspicious authentications' ), + ('dnsbl.inps.de' , 'automated and reported' ), + ('bl.blocklist.de' , 'fail2ban reports etc.' ), + ('srnblack.surgate.net' , 'feeders' ), + ('all.s5h.net' , 'traps' ), + ('rbl.realtimeblacklist.com' , 'lists ip ranges' ), + ('b.barracudacentral.org' , 'traps' ), + ('hostkarma.junkemailfilter.com', 'Autotected Virus Senders' ), + ('rbl.megarbl.net' , 'Curated Spamtraps' ), + ('ubl.unsubscore.com' , 'Collected Opt-Out Addresses' ), + ('0spam.fusionzero.com' , 'Spam Trap' ), +] + class MailDiagnoser(Diagnoser): @@ -14,6 +47,7 @@ class MailDiagnoser(Diagnoser): def run(self): # Is outgoing port 25 filtered somehow ? + self.logger_debug("Running outgoing 25 port check") if os.system('/bin/nc -z -w2 yunohost.org 25') == 0: yield dict(meta={"test": "ougoing_port_25"}, status="SUCCESS", @@ -23,9 +57,22 @@ class MailDiagnoser(Diagnoser): status="ERROR", summary="diagnosis_mail_ougoing_port_25_blocked") + # Is Reverse DNS well configured ? - # Mail blacklist using dig requests (c.f. ljf's code) + # Are IPs blacklisted ? + self.logger_debug("Running RBL detection") + blacklisted_details = tuple(self.check_blacklisted(self.get_public_ip(4))) + blacklisted_details += tuple(self.check_blacklisted(self.get_public_ip(6))) + if blacklisted_details: + yield dict(meta={}, + status="ERROR", + summary=("diagnosis_mail_blacklist_nok", {}), + details=blacklisted_details) + else: + yield dict(meta={}, + status="SUCCESS", + summary=("diagnosis_mail_blacklist_ok", {})) # SMTP reachability (c.f. check-smtp to be implemented on yunohost's remote diagnoser) @@ -37,6 +84,46 @@ class MailDiagnoser(Diagnoser): # check for unusual failed sending attempt being refused in the logs ? + def check_blacklisted(self, ip): + """ Check with dig onto blacklist DNS server + """ + if ip is None: + return + + for blacklist, description in DEFAULT_BLACKLIST: + + # Determine if we are listed on this RBL + try: + rev = dns.reversename.from_address(ip) + query = str(rev.split(3)[0]) + '.' + blacklist + # TODO add timeout lifetime + dns.resolver.query(query, "A") + except (dns.resolver.NXDOMAIN, dns.resolver.NoNameservers, dns.resolver.NoAnswer, + dns.exception.Timeout): + continue + + # Try to get the reason + reason = "not explained" + try: + reason = str(dns.resolver.query(query, "TXT")[0]) + except Exception: + pass + + yield ('diagnosis_mail_blacklisted_by', + (ip, blacklist, reason)) + + def get_public_ip(self, protocol=4): + # TODO we might call this function from another side + assert protocol in [4, 6], "Invalid protocol version, it should be either 4 or 6 and was '%s'" % repr(protocol) + + url = 'https://ip%s.yunohost.org' % ('6' if protocol == 6 else '') + + try: + return download_text(url, timeout=30).strip() + except Exception as e: + self.logger_debug("Could not get public IPv%s : %s" % (str(protocol), str(e))) + return None + def main(args, env, loggers): return MailDiagnoser(args, env, loggers).diagnose() diff --git a/locales/en.json b/locales/en.json index 800a1d696..dbce8f367 100644 --- a/locales/en.json +++ b/locales/en.json @@ -186,6 +186,9 @@ "diagnosis_swap_ok": "The system has {total} of swap!", "diagnosis_mail_ougoing_port_25_ok": "Outgoing port 25 is not blocked and email can be sent to other servers.", "diagnosis_mail_ougoing_port_25_blocked": "Outgoing port 25 appears to be blocked. You should try to unblock it in your internet service provider (or hosting provider) configuration panel. Meanwhile, the server won't be able to send emails to other servers.", + "diagnosis_mail_blacklist_ok": "Your server public IP are not listed on email blacklist.", + "diagnosis_mail_blacklist_nok": "Your server public IPs are listed on email blacklist.", + "diagnosis_mail_blacklisted_by": "{0} is listed on {1}. Reason: {2}", "diagnosis_regenconf_allgood": "All configurations files are in line with the recommended configuration!", "diagnosis_regenconf_manually_modified": "Configuration file {file} appears to have been manually modified.", "diagnosis_regenconf_manually_modified_details": "This is probably OK if you know what you're doing! YunoHost will stop updating this file automatically... But beware that YunoHost upgrades could contain important recommended changes. If you want to, you can inspect the differences with yunohost tools regen-conf {category} --dry-run --with-diff and force the reset to the recommended configuration with yunohost tools regen-conf {category} --force",