diff --git a/data/hooks/diagnosis/12-dnsrecords.py b/data/hooks/diagnosis/12-dnsrecords.py index 247a3bf4d..4f1f89ef7 100644 --- a/data/hooks/diagnosis/12-dnsrecords.py +++ b/data/hooks/diagnosis/12-dnsrecords.py @@ -8,11 +8,10 @@ from publicsuffixlist import PublicSuffixList from moulinette.utils.process import check_output -from yunohost.utils.network import dig +from yunohost.utils.dns import dig, YNH_DYNDNS_DOMAINS from yunohost.diagnosis import Diagnoser from yunohost.domain import domain_list, _build_dns_conf, _get_maindomain -YNH_DYNDNS_DOMAINS = ["nohost.me", "noho.st", "ynh.fr"] SPECIAL_USE_TLDS = ["local", "localhost", "onion", "test"] diff --git a/data/hooks/diagnosis/24-mail.py b/data/hooks/diagnosis/24-mail.py index 63f685a26..50b8dc12e 100644 --- a/data/hooks/diagnosis/24-mail.py +++ b/data/hooks/diagnosis/24-mail.py @@ -12,7 +12,7 @@ from moulinette.utils.filesystem import read_yaml from yunohost.diagnosis import Diagnoser from yunohost.domain import _get_maindomain, domain_list from yunohost.settings import settings_get -from yunohost.utils.network import dig +from yunohost.utils.dns import dig DEFAULT_DNS_BLACKLIST = "/usr/share/yunohost/other/dnsbl_list.yml" diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index c8249e439..071e93059 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -38,7 +38,8 @@ from moulinette.utils.network import download_json from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.domain import _get_maindomain, _build_dns_conf -from yunohost.utils.network import get_public_ip, dig +from yunohost.utils.network import get_public_ip +from yunohost.utils.dns import dig from yunohost.log import is_unit_operation from yunohost.regenconf import regen_conf diff --git a/src/yunohost/utils/dns.py b/src/yunohost/utils/dns.py index 1d67d73d0..ef89c35c5 100644 --- a/src/yunohost/utils/dns.py +++ b/src/yunohost/utils/dns.py @@ -18,11 +18,82 @@ along with this program; if not, see http://www.gnu.org/licenses """ +import dns.resolver from publicsuffixlist import PublicSuffixList -from yunohost.utils.network import dig +from moulinette.utils.filesystem import read_file YNH_DYNDNS_DOMAINS = ["nohost.me", "noho.st", "ynh.fr"] +# Lazy dev caching to avoid re-reading the file multiple time when calling +# dig() often during same yunohost operation +external_resolvers_ = [] + + +def external_resolvers(): + + global external_resolvers_ + + if not external_resolvers_: + resolv_dnsmasq_conf = read_file("/etc/resolv.dnsmasq.conf").split("\n") + external_resolvers_ = [ + r.split(" ")[1] for r in resolv_dnsmasq_conf if r.startswith("nameserver") + ] + # We keep only ipv4 resolvers, otherwise on IPv4-only instances, IPv6 + # will be tried anyway resulting in super-slow dig requests that'll wait + # until timeout... + external_resolvers_ = [r for r in external_resolvers_ if ":" not in r] + + return external_resolvers_ + + +def dig( + qname, rdtype="A", timeout=5, resolvers="local", edns_size=1500, full_answers=False +): + """ + Do a quick DNS request and avoid the "search" trap inside /etc/resolv.conf + """ + + # It's very important to do the request with a qname ended by . + # If we don't and the domain fail, dns resolver try a second request + # by concatenate the qname with the end of the "hostname" + if not qname.endswith("."): + qname += "." + + if resolvers == "local": + resolvers = ["127.0.0.1"] + elif resolvers == "force_external": + resolvers = external_resolvers() + else: + assert isinstance(resolvers, list) + + resolver = dns.resolver.Resolver(configure=False) + resolver.use_edns(0, 0, edns_size) + resolver.nameservers = resolvers + # resolver.timeout is used to trigger the next DNS query on resolvers list. + # In python-dns 1.16, this value is set to 2.0. However, this means that if + # the 3 first dns resolvers in list are down, we wait 6 seconds before to + # run the DNS query to a DNS resolvers up... + # In diagnosis dnsrecords, with 10 domains this means at least 12min, too long. + resolver.timeout = 1.0 + # resolver.lifetime is the timeout for resolver.query() + # By default set it to 5 seconds to allow 4 resolvers to be unreachable. + resolver.lifetime = timeout + try: + answers = resolver.query(qname, rdtype) + except ( + dns.resolver.NXDOMAIN, + dns.resolver.NoNameservers, + dns.resolver.NoAnswer, + dns.exception.Timeout, + ) as e: + return ("nok", (e.__class__.__name__, e)) + + if not full_answers: + answers = [answer.to_text() for answer in answers] + + return ("ok", answers) + + def get_public_suffix(domain): """get_public_suffix("www.example.com") -> "example.com" @@ -40,6 +111,7 @@ def get_public_suffix(domain): return public_suffix + def get_dns_zone_from_domain(domain): # TODO Check if this function is YNH_DYNDNS_DOMAINS compatible """ diff --git a/src/yunohost/utils/network.py b/src/yunohost/utils/network.py index 88ea5e5f6..4474af14f 100644 --- a/src/yunohost/utils/network.py +++ b/src/yunohost/utils/network.py @@ -22,7 +22,6 @@ import os import re import logging import time -import dns.resolver from moulinette.utils.filesystem import read_file, write_to_file from moulinette.utils.network import download_text @@ -124,75 +123,6 @@ def get_gateway(): return addr.popitem()[1] if len(addr) == 1 else None -# Lazy dev caching to avoid re-reading the file multiple time when calling -# dig() often during same yunohost operation -external_resolvers_ = [] - - -def external_resolvers(): - - global external_resolvers_ - - if not external_resolvers_: - resolv_dnsmasq_conf = read_file("/etc/resolv.dnsmasq.conf").split("\n") - external_resolvers_ = [ - r.split(" ")[1] for r in resolv_dnsmasq_conf if r.startswith("nameserver") - ] - # We keep only ipv4 resolvers, otherwise on IPv4-only instances, IPv6 - # will be tried anyway resulting in super-slow dig requests that'll wait - # until timeout... - external_resolvers_ = [r for r in external_resolvers_ if ":" not in r] - - return external_resolvers_ - - -def dig( - qname, rdtype="A", timeout=5, resolvers="local", edns_size=1500, full_answers=False -): - """ - Do a quick DNS request and avoid the "search" trap inside /etc/resolv.conf - """ - - # It's very important to do the request with a qname ended by . - # If we don't and the domain fail, dns resolver try a second request - # by concatenate the qname with the end of the "hostname" - if not qname.endswith("."): - qname += "." - - if resolvers == "local": - resolvers = ["127.0.0.1"] - elif resolvers == "force_external": - resolvers = external_resolvers() - else: - assert isinstance(resolvers, list) - - resolver = dns.resolver.Resolver(configure=False) - resolver.use_edns(0, 0, edns_size) - resolver.nameservers = resolvers - # resolver.timeout is used to trigger the next DNS query on resolvers list. - # In python-dns 1.16, this value is set to 2.0. However, this means that if - # the 3 first dns resolvers in list are down, we wait 6 seconds before to - # run the DNS query to a DNS resolvers up... - # In diagnosis dnsrecords, with 10 domains this means at least 12min, too long. - resolver.timeout = 1.0 - # resolver.lifetime is the timeout for resolver.query() - # By default set it to 5 seconds to allow 4 resolvers to be unreachable. - resolver.lifetime = timeout - try: - answers = resolver.query(qname, rdtype) - except ( - dns.resolver.NXDOMAIN, - dns.resolver.NoNameservers, - dns.resolver.NoAnswer, - dns.exception.Timeout, - ) as e: - return ("nok", (e.__class__.__name__, e)) - - if not full_answers: - answers = [answer.to_text() for answer in answers] - - return ("ok", answers) - def _extract_inet(string, skip_netmask=False, skip_loopback=True): """ Extract IP addresses (v4 and/or v6) from a string limited to one