PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/ |
Server: Linux ngx353.inmotionhosting.com 4.18.0-553.22.1.lve.1.el8.x86_64 #1 SMP Tue Oct 8 15:52:54 UTC 2024 x86_64 IP: 209.182.202.254 |
Dir : //proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/dns.py |
""" Compendium of generic DNS utilities # Examples: dns.lookup(name, rdtype, ...) dns.query(name, rdtype, ...) dns.srv_rec(data) dns.srv_data('my1.example.com', 389, prio=10, weight=100) dns.srv_name('ldap/tcp', 'example.com') """ import base64 import binascii import functools import hashlib import itertools import logging import random import re import shlex import socket import ssl import string import salt.modules.cmdmod import salt.utils.files import salt.utils.network import salt.utils.path import salt.utils.stringutils from salt._compat import ipaddress from salt.utils.odict import OrderedDict # Integrations try: import dns.resolver # pylint: disable=no-name-in-module HAS_DNSPYTHON = True except ImportError: HAS_DNSPYTHON = False try: import tldextract HAS_TLDEXTRACT = True except ImportError: HAS_TLDEXTRACT = False HAS_DIG = salt.utils.path.which("dig") is not None DIG_OPTIONS = "+search +fail +noall +answer +nocl +nottl" HAS_DRILL = salt.utils.path.which("drill") is not None HAS_HOST = salt.utils.path.which("host") is not None HAS_NSLOOKUP = salt.utils.path.which("nslookup") is not None __salt__ = {"cmd.run_all": salt.modules.cmdmod.run_all} log = logging.getLogger(__name__) class RFC: """ Simple holding class for all RFC/IANA registered lists & standards """ # https://tools.ietf.org/html/rfc6844#section-3 CAA_TAGS = ("issue", "issuewild", "iodef") # http://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml SSHFP_ALGO = OrderedDict( ( (1, "rsa"), (2, "dsa"), (3, "ecdsa"), (4, "ed25519"), ) ) SSHFP_HASH = OrderedDict( ( (1, "sha1"), (2, "sha256"), ) ) # http://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml TLSA_USAGE = OrderedDict( ( (0, "pkixta"), (1, "pkixee"), (2, "daneta"), (3, "daneee"), ) ) TLSA_SELECT = OrderedDict( ( (0, "cert"), (1, "spki"), ) ) TLSA_MATCHING = OrderedDict( ( (0, "full"), (1, "sha256"), (2, "sha512"), ) ) SRV_PROTO = ("tcp", "udp", "sctp") @staticmethod def validate(lookup, ref, match=None): if lookup in ref: return lookup elif match == "in": return [code for code, name in ref.items() if lookup in name][-1] else: # OrderedDicts only!(?) return {name: code for code, name in ref.items()}[lookup] def _to_port(port): try: port = int(port) assert 1 <= port <= 65535 return port except (ValueError, AssertionError): raise ValueError(f"Invalid port {port}") def _tree(domain, tld=False): """ Split out a domain in its parents Leverages tldextract to take the TLDs from publicsuffix.org or makes a valiant approximation of that :param domain: dc2.ams2.example.com :param tld: Include TLD in list :return: [ 'dc2.ams2.example.com', 'ams2.example.com', 'example.com'] """ domain = domain.rstrip(".") assert "." in domain, "Provide a decent domain" if not tld: if HAS_TLDEXTRACT: tld = tldextract.extract(domain).suffix else: tld = re.search( r"((?:(?:ac|biz|com?|info|edu|gov|mil|name|net|n[oi]m|org)\.)?[^.]+)$", domain, ).group() log.info( "Without tldextract, dns.util resolves the TLD of %s to %s", domain, tld ) res = [domain] while True: idx = domain.find(".") if idx < 0: break domain = domain[idx + 1 :] if domain == tld: break res.append(domain) return res def _weighted_order(recs): res = [] weights = [rec["weight"] for rec in recs] while weights: rnd = random.random() * sum(weights) for i, w in enumerate(weights): rnd -= w if rnd < 0: res.append(recs.pop(i)["name"]) weights.pop(i) break return res def _cast(rec_data, rec_cast): if isinstance(rec_cast, dict): rec_data = type(next(iter(rec_cast.keys())))(rec_data) res = rec_cast[rec_data] return res elif isinstance(rec_cast, (list, tuple)): return RFC.validate(rec_data, rec_cast) else: return rec_cast(rec_data) def _data2rec(schema, rec_data): """ schema = OrderedDict({ 'prio': int, 'weight': int, 'port': to_port, 'name': str, }) rec_data = '10 20 25 myawesome.nl' res = {'prio': 10, 'weight': 20, 'port': 25 'name': 'myawesome.nl'} """ try: rec_fields = rec_data.split(" ") # spaces in digest fields are allowed assert len(rec_fields) >= len(schema) if len(rec_fields) > len(schema): cutoff = len(schema) - 1 rec_fields = rec_fields[0:cutoff] + ["".join(rec_fields[cutoff:])] if len(schema) == 1: res = _cast(rec_fields[0], next(iter(schema.values()))) else: res = { field_name: _cast(rec_field, rec_cast) for (field_name, rec_cast), rec_field in zip(schema.items(), rec_fields) } return res except (AssertionError, AttributeError, TypeError, ValueError) as e: raise ValueError( 'Unable to cast "{0}" as "{2}": {1}'.format( rec_data, e, " ".join(schema.keys()) ) ) def _data2rec_group(schema, recs_data, group_key): if not isinstance(recs_data, (list, tuple)): recs_data = [recs_data] res = OrderedDict() try: for rdata in recs_data: rdata = _data2rec(schema, rdata) assert rdata and group_key in rdata idx = rdata.pop(group_key) if idx not in res: res[idx] = [] if len(rdata) == 1: rdata = next(iter(rdata.values())) res[idx].append(rdata) return res except (AssertionError, ValueError) as e: raise ValueError( 'Unable to cast "{}" as a group of "{}": {}'.format( ",".join(recs_data), " ".join(schema.keys()), e ) ) def _rec2data(*rdata): return " ".join(rdata) def _data_clean(data): data = data.strip(string.whitespace) if data.startswith(('"', "'")) and data.endswith(('"', "'")): return data[1:-1] else: return data def _lookup_dig(name, rdtype, timeout=None, servers=None, secure=None): """ Use dig to lookup addresses :param name: Name of record to search :param rdtype: DNS record type :param timeout: server response timeout :param servers: [] of servers to use :return: [] of records or False if error """ cmd = f"dig {DIG_OPTIONS} -t {rdtype} " if servers: cmd += "".join([f"@{srv} " for srv in servers]) if timeout is not None: if servers: timeout = int(float(timeout) / len(servers)) else: timeout = int(timeout) cmd += f"+time={timeout} " if secure: cmd += "+dnssec +adflag " cmd = __salt__["cmd.run_all"]( f"{cmd} {name}", python_shell=False, output_loglevel="quiet" ) if "ignoring invalid type" in cmd["stderr"]: raise ValueError(f"Invalid DNS type {rdtype}") elif cmd["retcode"] != 0: log.warning( "dig returned (%s): %s", cmd["retcode"], cmd["stderr"].strip(string.whitespace + ";"), ) return False elif not cmd["stdout"]: return [] validated = False res = [] for line in cmd["stdout"].splitlines(): _, rtype, rdata = line.split(None, 2) if rtype == "CNAME" and rdtype != "CNAME": continue elif rtype == "RRSIG": validated = True continue res.append(_data_clean(rdata)) if res and secure and not validated: return False else: return res def _lookup_drill(name, rdtype, timeout=None, servers=None, secure=None): """ Use drill to lookup addresses :param name: Name of record to search :param rdtype: DNS record type :param timeout: command return timeout :param servers: [] of servers to use :return: [] of records or False if error """ cmd = "drill " if secure: cmd += "-D -o ad " cmd += f"{rdtype} {name} " if servers: cmd += "".join([f"@{srv} " for srv in servers]) cmd = __salt__["cmd.run_all"]( cmd, timeout=timeout, python_shell=False, output_loglevel="quiet" ) if cmd["retcode"] != 0: log.warning("drill returned (%s): %s", cmd["retcode"], cmd["stderr"]) return False lookup_res = iter(cmd["stdout"].splitlines()) validated = False res = [] try: line = "" while "ANSWER SECTION" not in line: line = next(lookup_res) while True: line = next(lookup_res) line = line.strip() if not line or line.startswith(";;"): break l_type, l_rec = line.split(None, 4)[-2:] if l_type == "CNAME" and rdtype != "CNAME": continue elif l_type == "RRSIG": validated = True continue elif l_type != rdtype: raise ValueError(f"Invalid DNS type {rdtype}") res.append(_data_clean(l_rec)) except StopIteration: pass if res and secure and not validated: return False else: return res def _lookup_gai(name, rdtype, timeout=None): """ Use Python's socket interface to lookup addresses :param name: Name of record to search :param rdtype: A or AAAA :param timeout: ignored :return: [] of addresses or False if error """ if rdtype == "A": sock_t = socket.AF_INET elif rdtype == "AAAA": sock_t = socket.AF_INET6 else: raise ValueError(f"Invalid DNS type {rdtype} for gai lookup") if timeout: log.info("Ignoring timeout on gai resolver; fix resolv.conf to do that") try: addresses = [ sock[4][0] for sock in socket.getaddrinfo(name, None, sock_t, 0, socket.SOCK_RAW) ] return addresses except socket.gaierror: return False def _lookup_host(name, rdtype, timeout=None, server=None): """ Use host to lookup addresses :param name: Name of record to search :param server: Server to query :param rdtype: DNS record type :param timeout: server response wait :return: [] of records or False if error """ cmd = f"host -t {rdtype} " if timeout: cmd += f"-W {int(timeout)} " cmd += name if server is not None: cmd += f" {server}" cmd = __salt__["cmd.run_all"](cmd, python_shell=False, output_loglevel="quiet") if "invalid type" in cmd["stderr"]: raise ValueError(f"Invalid DNS type {rdtype}") elif cmd["retcode"] != 0: log.warning("host returned (%s): %s", cmd["retcode"], cmd["stderr"]) return False elif "has no" in cmd["stdout"]: return [] res = [] _stdout = cmd["stdout"] if server is None else cmd["stdout"].split("\n\n")[-1] for line in _stdout.splitlines(): if rdtype != "CNAME" and "is an alias" in line: continue line = line.split(" ", 3)[-1] for prefix in ("record", "address", "handled by", "alias for"): if line.startswith(prefix): line = line[len(prefix) + 1 :] break res.append(_data_clean(line)) return res def _lookup_dnspython(name, rdtype, timeout=None, servers=None, secure=None): """ Use dnspython to lookup addresses :param name: Name of record to search :param rdtype: DNS record type :param timeout: query timeout :param server: [] of server(s) to try in order :return: [] of records or False if error """ resolver = dns.resolver.Resolver() if timeout is not None: resolver.lifetime = float(timeout) if servers: resolver.nameservers = servers if secure: resolver.ednsflags += dns.flags.DO try: res = [ _data_clean(rr.to_text()) for rr in resolver.query(name, rdtype, raise_on_no_answer=False) ] return res except dns.rdatatype.UnknownRdatatype: raise ValueError(f"Invalid DNS type {rdtype}") except ( dns.resolver.NXDOMAIN, dns.resolver.YXDOMAIN, dns.resolver.NoNameservers, dns.exception.Timeout, ): return False def _lookup_nslookup(name, rdtype, timeout=None, server=None): """ Use nslookup to lookup addresses :param name: Name of record to search :param rdtype: DNS record type :param timeout: server response timeout :param server: server to query :return: [] of records or False if error """ cmd = f"nslookup -query={rdtype} {name}" if timeout is not None: cmd += f" -timeout={int(timeout)}" if server is not None: cmd += f" {server}" cmd = __salt__["cmd.run_all"](cmd, python_shell=False, output_loglevel="quiet") if cmd["retcode"] != 0: log.warning( "nslookup returned (%s): %s", cmd["retcode"], cmd["stdout"].splitlines()[-1].strip(string.whitespace + ";"), ) return False lookup_res = iter(cmd["stdout"].splitlines()) res = [] try: line = next(lookup_res) if "unknown query type" in line: raise ValueError(f"Invalid DNS type {rdtype}") while True: if name in line: break line = next(lookup_res) while True: line = line.strip() if not line or line.startswith("*"): break elif rdtype != "CNAME" and "canonical name" in line: name = line.split()[-1][:-1] line = next(lookup_res) continue elif rdtype == "SOA": line = line.split("=") elif line.startswith("Name:"): line = next(lookup_res) line = line.split(":", 1) elif line.startswith(name): if "=" in line: line = line.split("=", 1) else: line = line.split(" ") res.append(_data_clean(line[-1])) line = next(lookup_res) except StopIteration: pass if rdtype == "SOA": return [" ".join(res[1:])] else: return res def lookup( name, rdtype, method=None, servers=None, timeout=None, walk=False, walk_tld=False, secure=None, ): """ Lookup DNS records and return their data :param name: name to lookup :param rdtype: DNS record type :param method: gai (getaddrinfo()), dnspython, dig, drill, host, nslookup or auto (default) :param servers: (list of) server(s) to try in-order :param timeout: query timeout or a valiant approximation of that :param walk: Walk the DNS upwards looking for the record type or name/recordtype if walk='name'. :param walk_tld: Include the final domain in the walk :param secure: return only DNSSEC secured responses :return: [] of record data """ # opts = __opts__.get('dns', {}) opts = {} method = method or opts.get("method", "auto") secure = secure or opts.get("secure", None) servers = servers or opts.get("servers", None) timeout = timeout or opts.get("timeout", False) rdtype = rdtype.upper() query_methods = ( ("gai", _lookup_gai, not any((rdtype not in ("A", "AAAA"), servers, secure))), ("dnspython", _lookup_dnspython, HAS_DNSPYTHON), ("dig", _lookup_dig, HAS_DIG), ("drill", _lookup_drill, HAS_DRILL), ("host", _lookup_host, HAS_HOST and not secure), ("nslookup", _lookup_nslookup, HAS_NSLOOKUP and not secure), ) try: if method == "auto": # The first one not to bork on the conditions becomes the function method, resolver = next( ((rname, rcb) for rname, rcb, rtest in query_methods if rtest) ) else: # The first one not to bork on the conditions becomes the function. And the name must match. resolver = next( ( rcb for rname, rcb, rtest in query_methods if rname == method and rtest ) ) except StopIteration: log.error( "Unable to lookup %s/%s: Resolver method %s invalid, unsupported " "or unable to perform query", method, rdtype, name, ) return False res_kwargs = { "rdtype": rdtype, } if servers: if not isinstance(servers, (list, tuple)): servers = [servers] if method in ("dnspython", "dig", "drill"): res_kwargs["servers"] = servers else: if timeout: timeout /= len(servers) # Inject a wrapper for multi-server behaviour def _multi_srvr(resolv_func): @functools.wraps(resolv_func) def _wrapper(**res_kwargs): for server in servers: s_res = resolv_func(server=server, **res_kwargs) if s_res: return s_res return _wrapper resolver = _multi_srvr(resolver) if not walk: name = [name] else: idx = 0 if rdtype in ("SRV", "TLSA"): # The only RRs I know that have 2 name components idx = name.find(".") + 1 idx = name.find(".", idx) + 1 domain = name[idx:] rname = name[0:idx] name = _tree(domain, walk_tld) if walk == "name": name = [rname + domain for domain in name] if timeout: timeout /= len(name) if secure: res_kwargs["secure"] = secure if timeout: res_kwargs["timeout"] = timeout for rname in name: res = resolver(name=rname, **res_kwargs) if res: return res return res def query( name, rdtype, method=None, servers=None, timeout=None, walk=False, walk_tld=False, secure=None, ): """ Query DNS for information. Where `lookup()` returns record data, `query()` tries to interpret the data and return its results :param name: name to lookup :param rdtype: DNS record type :param method: gai (getaddrinfo()), pydns, dig, drill, host, nslookup or auto (default) :param servers: (list of) server(s) to try in-order :param timeout: query timeout or a valiant approximation of that :param secure: return only DNSSEC secured response :param walk: Walk the DNS upwards looking for the record type or name/recordtype if walk='name'. :param walk_tld: Include the top-level domain in the walk :return: [] of records """ rdtype = rdtype.upper() qargs = { "method": method, "servers": servers, "timeout": timeout, "walk": walk, "walk_tld": walk_tld, "secure": secure, } if rdtype == "PTR" and not name.endswith("arpa"): name = ptr_name(name) if rdtype == "SPF": # 'SPF' has become a regular 'TXT' again qres = [ answer for answer in lookup(name, "TXT", **qargs) if answer.startswith("v=spf") ] if not qres: qres = lookup(name, rdtype, **qargs) else: qres = lookup(name, rdtype, **qargs) rec_map = { "A": a_rec, "AAAA": aaaa_rec, "CAA": caa_rec, "MX": mx_rec, "SOA": soa_rec, "SPF": spf_rec, "SRV": srv_rec, "SSHFP": sshfp_rec, "TLSA": tlsa_rec, } if not qres or rdtype not in rec_map: return qres elif rdtype in ("A", "AAAA", "SSHFP", "TLSA"): res = [rec_map[rdtype](res) for res in qres] elif rdtype in ("SOA", "SPF"): res = rec_map[rdtype](qres[0]) else: res = rec_map[rdtype](qres) return res def host(name, ip4=True, ip6=True, **kwargs): """ Return a list of addresses for name ip6: Return IPv6 addresses ip4: Return IPv4 addresses the rest is passed on to lookup() """ res = {} if ip6: ip6 = lookup(name, "AAAA", **kwargs) if ip6: res["ip6"] = ip6 if ip4: ip4 = lookup(name, "A", **kwargs) if ip4: res["ip4"] = ip4 return res def a_rec(rdata): """ Validate and parse DNS record data for an A record :param rdata: DNS record data :return: { 'address': ip } """ rschema = OrderedDict((("address", ipaddress.IPv4Address),)) return _data2rec(rschema, rdata) def aaaa_rec(rdata): """ Validate and parse DNS record data for an AAAA record :param rdata: DNS record data :return: { 'address': ip } """ rschema = OrderedDict((("address", ipaddress.IPv6Address),)) return _data2rec(rschema, rdata) def caa_rec(rdatas): """ Validate and parse DNS record data for a CAA record :param rdata: DNS record data :return: dict w/fields """ rschema = OrderedDict( ( ("flags", lambda flag: ["critical"] if int(flag) > 0 else []), ("tag", RFC.CAA_TAGS), ("value", lambda val: val.strip("',\"")), ) ) res = _data2rec_group(rschema, rdatas, "tag") for tag in ("issue", "issuewild"): tag_res = res.get(tag, False) if not tag_res: continue for idx, val in enumerate(tag_res): if ";" not in val: continue val, params = val.split(";", 1) params = dict(param.split("=") for param in shlex.split(params)) tag_res[idx] = {val: params} return res def mx_data(target, preference=10): """ Generate MX record data :param target: server :param preference: preference number :return: DNS record data """ return _rec2data(int(preference), target) def mx_rec(rdatas): """ Validate and parse DNS record data for MX record(s) :param rdata: DNS record data :return: dict w/fields """ rschema = OrderedDict( ( ("preference", int), ("name", str), ) ) return _data2rec_group(rschema, rdatas, "preference") def ptr_name(rdata): """ Return PTR name of given IP :param rdata: IP address :return: PTR record name """ try: return ipaddress.ip_address(rdata).reverse_pointer except ValueError: log.error("Unable to generate PTR record; %s is not a valid IP address", rdata) return False def soa_rec(rdata): """ Validate and parse DNS record data for SOA record(s) :param rdata: DNS record data :return: dict w/fields """ rschema = OrderedDict( ( ("mname", str), ("rname", str), ("serial", int), ("refresh", int), ("retry", int), ("expire", int), ("minimum", int), ) ) return _data2rec(rschema, rdata) def spf_rec(rdata): """ Validate and parse DNS record data for SPF record(s) :param rdata: DNS record data :return: dict w/fields """ spf_fields = rdata.split(" ") if not spf_fields.pop(0).startswith("v=spf"): raise ValueError("Not an SPF record") res = OrderedDict() mods = set() for mech_spec in spf_fields: if mech_spec.startswith(("exp", "redirect")): # It's a modifier mod, val = mech_spec.split("=", 1) if mod in mods: raise KeyError(f"Modifier {mod} can only appear once") mods.add(mod) continue # TODO: Should be in something intelligent like an SPF_get # if mod == 'exp': # res[mod] = lookup(val, 'TXT', **qargs) # continue # elif mod == 'redirect': # return query(val, 'SPF', **qargs) mech = {} if mech_spec[0] in ("+", "-", "~", "?"): mech["qualifier"] = mech_spec[0] mech_spec = mech_spec[1:] if ":" in mech_spec: mech_spec, val = mech_spec.split(":", 1) elif "/" in mech_spec: idx = mech_spec.find("/") mech_spec = mech_spec[0:idx] val = mech_spec[idx:] else: val = None res[mech_spec] = mech if not val: continue elif mech_spec in ("ip4", "ip6"): val = ipaddress.ip_interface(val) assert val.version == int(mech_spec[-1]) mech["value"] = val return res def srv_data(target, port, prio=10, weight=10): """ Generate SRV record data :param target: :param port: :param prio: :param weight: :return: """ return _rec2data(prio, weight, port, target) def srv_name(svc, proto="tcp", domain=None): """ Generate SRV record name :param svc: ldap, 389 etc :param proto: tcp, udp, sctp etc. :param domain: name to append :return: """ proto = RFC.validate(proto, RFC.SRV_PROTO) if isinstance(svc, int) or svc.isdigit(): svc = _to_port(svc) if domain: domain = "." + domain return f"_{svc}._{proto}{domain}" def srv_rec(rdatas): """ Validate and parse DNS record data for SRV record(s) :param rdata: DNS record data :return: dict w/fields """ rschema = OrderedDict( ( ("prio", int), ("weight", int), ("port", _to_port), ("name", str), ) ) return _data2rec_group(rschema, rdatas, "prio") def sshfp_data(key_t, hash_t, pub): """ Generate an SSHFP record :param key_t: rsa/dsa/ecdsa/ed25519 :param hash_t: sha1/sha256 :param pub: the SSH public key """ key_t = RFC.validate(key_t, RFC.SSHFP_ALGO, "in") hash_t = RFC.validate(hash_t, RFC.SSHFP_HASH) hasher = hashlib.new(hash_t) hasher.update(base64.b64decode(pub)) ssh_fp = hasher.hexdigest() return _rec2data(key_t, hash_t, ssh_fp) def sshfp_rec(rdata): """ Validate and parse DNS record data for TLSA record(s) :param rdata: DNS record data :return: dict w/fields """ rschema = OrderedDict( ( ("algorithm", RFC.SSHFP_ALGO), ("fp_hash", RFC.SSHFP_HASH), ( "fingerprint", lambda val: val.lower(), ), # resolvers are inconsistent on this one ) ) return _data2rec(rschema, rdata) def tlsa_data(pub, usage, selector, matching): """ Generate a TLSA rec :param pub: Pub key in PEM format :param usage: :param selector: :param matching: :return: TLSA data portion """ usage = RFC.validate(usage, RFC.TLSA_USAGE) selector = RFC.validate(selector, RFC.TLSA_SELECT) matching = RFC.validate(matching, RFC.TLSA_MATCHING) pub = ssl.PEM_cert_to_DER_cert(pub.strip()) if matching == 0: cert_fp = binascii.b2a_hex(pub) else: hasher = hashlib.new(RFC.TLSA_MATCHING[matching]) hasher.update(pub) cert_fp = hasher.hexdigest() return _rec2data(usage, selector, matching, cert_fp) def tlsa_rec(rdata): """ Validate and parse DNS record data for TLSA record(s) :param rdata: DNS record data :return: dict w/fields """ rschema = OrderedDict( ( ("usage", RFC.TLSA_USAGE), ("selector", RFC.TLSA_SELECT), ("matching", RFC.TLSA_MATCHING), ("pub", str), ) ) return _data2rec(rschema, rdata) def service(svc, proto="tcp", domain=None, walk=False, secure=None): """ Find an SRV service in a domain or its parents :param svc: service to find (ldap, 389, etc) :param proto: protocol the service talks (tcp, udp, etc) :param domain: domain to start search in :param walk: walk the parents if domain doesn't provide the service :param secure: only return DNSSEC-validated results :return: [ [ prio1server1, prio1server2 ], [ prio2server1, prio2server2 ], ] (the servers will already be weighted according to the SRV rules) """ qres = query(srv_name(svc, proto, domain), "SRV", walk=walk, secure=secure) if not qres: return False res = [] for _, recs in qres.items(): res.append(_weighted_order(recs)) return res def services(services_file="/etc/services"): """ Parse through system-known services :return: { 'svc': [ { 'port': port 'proto': proto, 'desc': comment }, ], } """ res = {} with salt.utils.files.fopen(services_file, "r") as svc_defs: for svc_def in svc_defs.readlines(): svc_def = salt.utils.stringutils.to_unicode(svc_def.strip()) if not svc_def or svc_def.startswith("#"): continue elif "#" in svc_def: svc_def, comment = svc_def.split("#", 1) comment = comment.strip() else: comment = None svc_def = svc_def.split() port, proto = svc_def.pop(1).split("/") port = int(port) for name in svc_def: svc_res = res.get(name, {}) pp_res = svc_res.get(port, False) if not pp_res: svc = { "port": port, "proto": proto, } if comment: svc["desc"] = comment svc_res[port] = svc else: curr_proto = pp_res["proto"] if isinstance(curr_proto, (list, tuple)): curr_proto.append(proto) else: pp_res["proto"] = [curr_proto, proto] curr_desc = pp_res.get("desc", False) if comment: if not curr_desc: pp_res["desc"] = comment elif comment != curr_desc: pp_res["desc"] = f"{curr_desc}, {comment}" res[name] = svc_res for svc, data in res.items(): if len(data) == 1: res[svc] = data.values().pop() continue else: res[svc] = list(data.values()) return res def parse_resolv(src="/etc/resolv.conf"): """ Parse a resolver configuration file (traditionally /etc/resolv.conf) """ nameservers = [] ip4_nameservers = [] ip6_nameservers = [] search = [] sortlist = [] domain = "" options = [] try: with salt.utils.files.fopen(src) as src_file: # pylint: disable=too-many-nested-blocks for line in src_file: line = salt.utils.stringutils.to_unicode(line).strip().split() try: (directive, arg) = (line[0].lower(), line[1:]) # Drop everything after # or ; (comments) arg = list( itertools.takewhile(lambda x: x[0] not in ("#", ";"), arg) ) if directive == "nameserver": addr = arg[0] try: ip_addr = ipaddress.ip_address(addr) version = ip_addr.version ip_addr = str(ip_addr) if ip_addr not in nameservers: nameservers.append(ip_addr) if version == 4 and ip_addr not in ip4_nameservers: ip4_nameservers.append(ip_addr) elif version == 6 and ip_addr not in ip6_nameservers: ip6_nameservers.append(ip_addr) except ValueError as exc: log.error("%s: %s", src, exc) elif directive == "domain": domain = arg[0] elif directive == "search": search = arg elif directive == "sortlist": # A sortlist is specified by IP address netmask pairs. # The netmask is optional and defaults to the natural # netmask of the net. The IP address and optional # network pairs are separated by slashes. for ip_raw in arg: try: ip_net = ipaddress.ip_network(ip_raw) except ValueError as exc: log.error("%s: %s", src, exc) else: if "/" not in ip_raw: # No netmask has been provided, guess # the "natural" one if ip_net.version == 4: ip_addr = str(ip_net.network_address) # pylint: disable=protected-access mask = salt.utils.network.natural_ipv4_netmask( ip_addr ) ip_net = ipaddress.ip_network( f"{ip_addr}{mask}", strict=False ) if ip_net.version == 6: # TODO pass if ip_net not in sortlist: sortlist.append(ip_net) elif directive == "options": # Options allows certain internal resolver variables to # be modified. if arg[0] not in options: options.append(arg[0]) except IndexError: continue if domain and search: # The domain and search keywords are mutually exclusive. If more # than one instance of these keywords is present, the last instance # will override. log.debug("%s: The domain and search keywords are mutually exclusive.", src) return { "nameservers": nameservers, "ip4_nameservers": ip4_nameservers, "ip6_nameservers": ip6_nameservers, "sortlist": [ip.with_netmask for ip in sortlist], "domain": domain, "search": search, "options": options, } except OSError: return {}