Mini Shell
"""
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 {}
Zerion Mini Shell 1.0