Spf.DNS (Spfcheck v0.1.0) View Source
A simple DNS caching resolver for SPF evaluations.
During an SPF evaluation all DNS responses are cached. Since the cache lasts
only for the duration of the evaluation, TTL values are ignored. The cache
allows for reporting on DNS data acquired during the evaluation. By
preloading the cache, using Spf.DNS.load/2
, new records can be tested.
The caching resolver also tracks the number of DNS queries made and the number of void queries seen.
Example
iex> zonedata = """
...> example.com TXT v=spf1 +all
...> """
iex> ctx = Spf.Context.new("example.com", dns: zonedata)
iex> {_ctx, result} = Spf.DNS.resolve(ctx, "example.com", type: :txt)
iex> result
{:ok, ["v=spf1 +all"]}
Link to this section Summary
Functions
Finds a domain name
's start of authority and contact.
Checks validity of a domain name and returns {:ok, name}
or {:error, reason}
Filters the dns_result/0
, keeps only the rrdata's for which fun
returns
a truthy value.
Returns a cached dns_result/0
for given name
, type
and context
or a cache_miss/0
.
Populates the dns cache of given context
, with dns
's zonedata.
Normalize a domain name by trimming, downcasing and removing any trailing dot.
Resolves a query, updates the cache and returns a {ctx
, dns_result/0
}tuple.
Return all acquired DNS RR's in a flat list of printable lines.
Link to this section Types
Specs
cache_miss() :: {:error, :cache_miss}
An {:error, :cache_miss}
-tuple
Specs
A DNS result in the form of an ok/error-tuple.
Link to this section Functions
Specs
Finds a domain name
's start of authority and contact.
SPF evaluation might require evaluating multiple records of different
domains. This function allows for reporting the owner and contact for each
SPF record encountered. CNAME's are ignored since the goal is to find the
authoritative zone for a given (sub)domain name
.
Returns
{:ok, domain, authority, contact}
, or{:error, :err_code}
The given name
does not need to actually exist, the aim is to find the
owner of the domain the name
belongs to.
Examples
iex> Spf.Context.new("example.com")
...> |> Spf.DNS.authority("non-existing.example.com")
{:ok, "non-existing.example.com", "example.com", "noc@dns.icann.org"}
Specs
Checks validity of a domain name and returns {:ok, name}
or {:error, reason}
Checks that the domain name:
- is an ascii string
- is less than 254 chars long
- has labels that are 1..63 chars long, and
- has at least 2 labels
- has a valid ldh-toplabel
Examples
iex> check_domain("com")
{:error, "not multi-label"}
iex> check_domain(".example.com")
{:error, "empty label"}
iex> check_domain("example..com")
{:error, "empty label"}
iex> check_domain(<<128>> <> ".com")
{:error, "contains non-ascii characters"}
iex> check_domain("example.-com")
{:error, "tld starts with hyphen"}
iex> check_domain("example.com-")
{:error, "tld ends with hyphen"}
iex> check_domain("example.c%m")
{:error, "tld not ldh"}
iex> check_domain("example.c0m.")
{:ok, "example.c0m"}
Specs
filter(dns_result(), function()) :: dns_result()
Filters the dns_result/0
, keeps only the rrdata's for which fun
returns
a truthy value.
If the dns_result
is actually an error, it is returned untouched.
Examples
iex> zonedata = """
...> example.com TXT v=spf1 -all
...> example.com TXT another txt record
...> """
iex> ctx = Spf.Context.new("example.com", dns: zonedata)
iex> {_ctx, dns_result} = resolve(ctx, "example.com", type: :txt)
iex>
iex> dns_result
{:ok, ["another txt record", "v=spf1 -all"]}
iex>
iex> filter(dns_result, &Spf.Eval.spf?/1)
{:ok, ["v=spf1 -all"]}
iex> Spf.DNS.filter({:error, :nxdomain}, &Spf.Eval.spf?/1)
{:error, :nxdomain}
Specs
from_cache(Spf.Context.t(), binary(), atom()) :: dns_result()
Returns a cached dns_result/0
for given name
, type
and context
or a cache_miss/0
.
The result returned is one of:
{:error, :cache_miss}
, for a cache miss{:error, reason}
, for a cache hit (of a previous negative result){:ok, rrs}
, for a cache hit (whererrs
is a list of rrdata's).
Where reason
includes:
:nxdomain
:servfail
:timeout
:zero_answers
Note that this function normalizes given name
and unrolls CNAME(s) and does
not make any actual DNS requests nor does it do any statistics.
Example
iex> zonedata = """
...> example.net CNAME example.com
...> EXAMPLE.COM A 1.2.3.4
...> """
iex> Spf.Context.new("some.domain.tld", dns: zonedata)
...> |> Spf.DNS.from_cache("example.net", :a)
{:ok, ["1.2.3.4"]}
Specs
load(Spf.Context.t(), nil | binary() | [binary()]) :: Spf.Context.t()
Populates the dns cache of given context
, with dns
's zonedata.
dns
can be a path to an existing file, a multi-line binary containing
individual RR-records per line or a list thereof. The cache is held in the
context under the :dns
key and is a simple map: {name, rrtype}
->
[rdata]
.
Lines should be formatted as
name rr-type data
, orname error
where
rr-type
isa
,aaaa
,cname
,mx
,ns
,ptr
,soa
,spf
, ortxt
error
isformerr
,nxdomain
,servfail
,timeout
orzero_answers
The second format will set the rdata for given name
to given error
for
all known rr-type
's.
Unknown rr-types are ignored and logged as a warning during preloading.
Example
iex> zonedata = """
...> example.com TXT v=spf1 +all
...> example.com A timeout
...> EXAMPLE.NET servfail
...> """
iex> ctx = Spf.Context.new("some.domain.tld")
...> |> Spf.DNS.load(zonedata)
iex>
iex> {_ctx, result} = Spf.DNS.resolve(ctx, "example.com", type: :txt)
iex> result
{:ok, ["v=spf1 +all"]}
iex>
iex> Spf.DNS.resolve(ctx, "example.com", type: :a) |> elem(1)
{:error, :timeout}
iex>
iex> Spf.DNS.resolve(ctx, "example.net", type: :a) |> elem(1)
{:error, :servfail}
iex>
iex> ctx.dns
%{{"example.com", :a} => [error: :timeout],
{"example.com", :txt} => ["v=spf1 +all"],
{"example.net", :a} => [error: :servfail],
{"example.net", :aaaa} => [error: :servfail],
{"example.net", :cname} => [error: :servfail],
{"example.net", :mx} => [error: :servfail],
{"example.net", :ns} => [error: :servfail],
{"example.net", :ptr} => [error: :servfail],
{"example.net", :soa} => [error: :servfail],
{"example.net", :spf} => [error: :servfail],
{"example.net", :txt} => [error: :servfail]
}
Specs
Normalize a domain name by trimming, downcasing and removing any trailing dot.
The validity of the domain name is not checked.
Examples
iex> normalize("Example.COM.")
"example.com"
iex> normalize("EXAMPLE.C%M")
"example.c%m"
Specs
resolve(Spf.Context.t(), binary(), Keyword.t()) :: {Spf.Context.t(), dns_result()}
Resolves a query, updates the cache and returns a {ctx
, dns_result/0
}tuple.
Returns:
{ctx, {:error, reason}}
if a DNS error occurred, or{ctx, {:ok, [rrs]}}
where rrs is a list of rrdata's
Although a result with ZERO answers is technically not a DNS error, it will be reported as an error. Error reasons include:
:zero_answers
:illegal_name
:timeout
:nxdomain
:servfail
- other
Options include:
type:
, which defaults to:a
stats
, which defaults totrue
When stats
is false
, void DNS responses (:nxdomain
or :zero_answers
)
are not counted.
Specs
to_list(Spf.Context.t(), Keyword.t()) :: [binary()]
Return all acquired DNS RR's in a flat list of printable lines.
Note that RR's with multiple entries in their rrdata are listed individually, so the output can be copy/paste'd into a local dns.txt pre-cache to facilitate experimentation with RR records.
The lines are sorted such that domains and subdomains are kept together as much as possible.
Example
iex> zonedata = """
...> example.com TXT v=spf1 -all
...> a.example.com A 1.2.3.4
...> b.example.com AaAa timeout
...> """
iex> ctx = Spf.Context.new("example.com", dns: zonedata)
iex> Spf.DNS.to_list(ctx, valid: :true)
...> |> Enum.map(fn x -> String.replace(x, ~r/\s+/, " ") end)
[
"example.com TXT \"v=spf1 -all\"",
"a.example.com A 1.2.3.4"
]
iex> Spf.DNS.to_list(ctx, valid: false)
...> |> Enum.map(fn x -> String.replace(x, ~r/\s+/, " ") end)
[
"b.example.com AAAA TIMEOUT"
]
iex>
iex> Spf.DNS.to_list(ctx)
...> |> Enum.map(fn x -> String.replace(x, ~r/\s+/, " ") end)
[
"example.com TXT \"v=spf1 -all\"",
"a.example.com A 1.2.3.4",
"b.example.com AAAA TIMEOUT"
]