def check_ehlo(self): """ Check the server is reachable from outside and it's the good one This check is ran on IPs we could used to send mail. """ for ipversion in self.ipversions: try: r = Diagnoser.remote_diagnosis("check-smtp", data={}, ipversion=ipversion) except Exception as e: yield dict( meta={ "test": "mail_ehlo", "reason": "remote_server_failed", "ipversion": ipversion, }, data={"error": str(e)}, status="WARNING", summary="diagnosis_mail_ehlo_could_not_diagnose", details=["diagnosis_mail_ehlo_could_not_diagnose_details"], ) continue if r["status"] != "ok": summary = r["status"].replace("error_smtp_", "diagnosis_mail_ehlo_") yield dict( meta={ "test": "mail_ehlo", "ipversion": ipversion }, data={}, status="ERROR", summary=summary, details=[summary + "_details"], ) elif r["helo"] != self.ehlo_domain: yield dict( meta={ "test": "mail_ehlo", "ipversion": ipversion }, data={ "wrong_ehlo": r["helo"], "right_ehlo": self.ehlo_domain }, status="ERROR", summary="diagnosis_mail_ehlo_wrong", details=["diagnosis_mail_ehlo_wrong_details"], )
def run(self): # TODO: report a warning if port 53 or 5353 is exposed to the outside world... # This dict is something like : # { 80: "nginx", # 25: "postfix", # 443: "nginx" # ... } ports = {} services = _get_services() for service, infos in services.items(): for port in infos.get("needs_exposed_ports", []): ports[port] = service ipversions = [] ipv4 = Diagnoser.get_cached_report("ip", item={"test": "ipv4"}) or {} if ipv4.get("status") == "SUCCESS": ipversions.append(4) # To be discussed: we could also make this check dependent on the # existence of an AAAA record... ipv6 = Diagnoser.get_cached_report("ip", item={"test": "ipv6"}) or {} if ipv6.get("status") == "SUCCESS": ipversions.append(6) # Fetch test result for each relevant IP version results = {} for ipversion in ipversions: try: r = Diagnoser.remote_diagnosis('check-ports', data={'ports': ports.keys()}, ipversion=ipversion) results[ipversion] = r["ports"] except Exception as e: yield dict( meta={ "reason": "remote_diagnosis_failed", "ipversion": ipversion }, data={"error": str(e)}, status="WARNING", summary="diagnosis_ports_could_not_diagnose", details=["diagnosis_ports_could_not_diagnose_details"]) continue ipversions = results.keys() if not ipversions: return for port, service in sorted(ports.items()): port = str(port) category = services[service].get("category", "[?]") # If both IPv4 and IPv6 (if applicable) are good if all(results[ipversion].get(port) is True for ipversion in ipversions): yield dict(meta={"port": port}, data={ "service": service, "category": category }, status="SUCCESS", summary="diagnosis_ports_ok", details=["diagnosis_ports_needed_by"]) # If both IPv4 and IPv6 (if applicable) are failed elif all(results[ipversion].get(port) is not True for ipversion in ipversions): yield dict(meta={"port": port}, data={ "service": service, "category": category }, status="ERROR", summary="diagnosis_ports_unreachable", details=[ "diagnosis_ports_needed_by", "diagnosis_ports_forwarding_tip" ]) # If only IPv4 is failed or only IPv6 is failed (if applicable) else: passed, failed = (4, 6) if results[4].get(port) is True else (6, 4) # Failing in ipv4 is critical. # If we failed in IPv6 but there's in fact no AAAA record # It's an acceptable situation and we shall not report an # error # If any AAAA record is set, IPv6 is important... def ipv6_is_important(): dnsrecords = Diagnoser.get_cached_report( "dnsrecords") or {} return any(record["data"].get("AAAA:@") in ["OK", "WRONG"] for record in dnsrecords.get("items", [])) if failed == 4 or ipv6_is_important(): yield dict(meta={"port": port}, data={ "service": service, "category": category, "passed": passed, "failed": failed }, status="ERROR", summary="diagnosis_ports_partially_unreachable", details=[ "diagnosis_ports_needed_by", "diagnosis_ports_forwarding_tip" ]) # So otherwise we report a success # And in addition we report an info about the failure in IPv6 # *with a different meta* (important to avoid conflicts when # fetching the other info...) else: yield dict(meta={"port": port}, data={ "service": service, "category": category }, status="SUCCESS", summary="diagnosis_ports_ok", details=["diagnosis_ports_needed_by"]) yield dict(meta={ "test": "ipv6", "port": port }, data={ "service": service, "category": category, "passed": passed, "failed": failed }, status="INFO", summary="diagnosis_ports_partially_unreachable", details=[ "diagnosis_ports_needed_by", "diagnosis_ports_forwarding_tip" ])
def test_http(self, domains, ipversions): results = {} for ipversion in ipversions: try: r = Diagnoser.remote_diagnosis('check-http', data={ 'domains': domains, "nonce": self.nonce }, ipversion=ipversion) results[ipversion] = r["http"] except Exception as e: yield dict( meta={ "reason": "remote_diagnosis_failed", "ipversion": ipversion }, data={"error": str(e)}, status="WARNING", summary="diagnosis_http_could_not_diagnose", details=["diagnosis_http_could_not_diagnose_details"]) continue ipversions = results.keys() if not ipversions: return for domain in domains: # If both IPv4 and IPv6 (if applicable) are good if all(results[ipversion][domain]["status"] == "ok" for ipversion in ipversions): if 4 in ipversions: self.do_hairpinning_test = True yield dict(meta={"domain": domain}, status="SUCCESS", summary="diagnosis_http_ok") # If both IPv4 and IPv6 (if applicable) are failed elif all(results[ipversion][domain]["status"] != "ok" for ipversion in ipversions): detail = results[4 if 4 in ipversions else 6][domain]["status"] yield dict(meta={"domain": domain}, status="ERROR", summary="diagnosis_http_unreachable", details=[ detail.replace("error_http_check", "diagnosis_http") ]) # If only IPv4 is failed or only IPv6 is failed (if applicable) else: passed, failed = ( 4, 6) if results[4][domain]["status"] == "ok" else (6, 4) detail = results[failed][domain]["status"] # Failing in ipv4 is critical. # If we failed in IPv6 but there's in fact no AAAA record # It's an acceptable situation and we shall not report an # error def ipv6_is_important_for_this_domain(): dnsrecords = Diagnoser.get_cached_report( "dnsrecords", item={ "domain": domain, "category": "basic" }) or {} AAAA_status = dnsrecords.get("data", {}).get("AAAA:@") return AAAA_status in ["OK", "WRONG"] if failed == 4 or ipv6_is_important_for_this_domain(): yield dict(meta={"domain": domain}, data={ "passed": passed, "failed": failed }, status="ERROR", summary="diagnosis_http_partially_unreachable", details=[ detail.replace("error_http_check", "diagnosis_http") ]) # So otherwise we report a success (note that this info is # later used to know that ACME challenge is doable) # # And in addition we report an info about the failure in IPv6 # *with a different meta* (important to avoid conflicts when # fetching the other info...) else: self.do_hairpinning_test = True yield dict(meta={"domain": domain}, status="SUCCESS", summary="diagnosis_http_ok") yield dict(meta={ "test": "ipv6", "domain": domain }, data={ "passed": passed, "failed": failed }, status="INFO", summary="diagnosis_http_partially_unreachable", details=[ detail.replace("error_http_check", "diagnosis_http") ])