Spf.DNS (Spfcheck v0.10.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
Types
An opaque datastructure as returned by :inet_res.resolve/3
as part of its
response.
A DNS result in the form of an ok/error-tuple.
A domain
is a simply an ascii binary.
A dns result as returned by :inet_res.resolve/3
.
An rrtype
denoted by an atom.
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 an updated context, a normalized name and a cache result in a 3-tuple
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
dns_msg() :: any()
An opaque datastructure as returned by :inet_res.resolve/3
as part of its
response.
Interpretation is done using the (erlang internal) :inet_dns
functions.
Specs
A DNS result in the form of an ok/error-tuple.
In case of succes, this is a list of
dns_data()
that is normalized (e.g. charlists are converted to strings as are ip address
tuples). Interpretation by caller depends on the rrtype
used.
Specs
domain() :: binary()
A domain
is a simply an ascii binary.
Specs
A dns result as returned by :inet_res.resolve/3
.
Specs
rrtype() :: atom()
An rrtype
denoted by an atom.
See also
inet_res.rr_type
.
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.
Returns
{:ok, domain, authority, contact}
, or{:error, reason}
The given name
does not need to actually exist, the aim is to find the
owner of the zone the name
belongs to. Note that CNAME's are ignored.
This function should be used after evaluation has completed, since it may cause void DNS responses. The soa-record is searched by querying for the soa-record and dropping the front-label (possibly mulitple times) while trying again.
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"}
iex> zonedata = "
...> www.example.com CNAME example.org
...> "
iex> Spf.Context.new("some.tld", dns: zonedata)
...> |> Spf.DNS.authority("www.example.com")
{:ok, "www.example.com", "example.com", "noc@dns.icann.org"}
Specs
Checks validity of a domain name and returns {:ok, name}
or {:error, reason}
Given domain
can be a binary or a charlist. It is normalized (downcase'd,
trailing dot removed and, if applicable, charlist is converted to a binary)
and checked that it:
- is an ascii string
- is less than 254 chars long
- has labels that are 1..63 chars long
- has no empty labels
- 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"}
# trailing dot is dropped
iex> check_domain("example.c0m.")
{:ok, "example.c0m"}
# returned as lowercase binary without the trailing dot
iex> check_domain('example.COM.')
{:ok, "example.com"}
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> dns_result = {:error, :nxdomain}
iex> Spf.DNS.filter(dns_result, &Spf.Eval.spf?/1)
{:error, :nxdomain}
Specs
from_cache(Spf.Context.t(), domain(), rrtype()) :: {Spf.Context.t(), dns_result()}
Returns an updated context, a normalized name and a cache result in a 3-tuple
The cache result can be one of:
{:error, reason}
{:ok, rrs}
, for a cache hit (whererrs
is a list of rrdata's).
Where reason
includes:
:cache_miss
, nothing found in the cache:nxdomain
, a previously cached result:servfail
, a previously cached result or a cname loop was found:timeout
, a previously cached result:zero_answers
, a previously cached result:illegal_name
, name was not a proper domain name
Note that this function does not make any real DNS requests and does not
update any dns counters. The only time the context is updated is when there
was an error in either domain
or the rrtype/0
given.
Example
iex> zonedata = "
...> example.net CNAME example.com
...> EXAMPLE.COM. A 1.2.3.4
...> "
iex> {_ctx, result} = Spf.Context.new("some.domain.tld", dns: zonedata)
...> |> Spf.DNS.from_cache("example.net", :a)
iex> result
{: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
SPF evaluation context
under the :dns
key and is a simple map: {name, rrtype}
-> [rdata]
.
Lines should be formatted as
name rrtype rdata
, orname rrtype error
where
rrtype
is one of: ["a", "aaaa", "cname", "mx", "ns", "ptr", "soa", "spf", "txt"]error
is one of ["formerr", "nxdomain", "servfail", "timeout", "zero_answers"]rdata
text representation of data suitable for givenrrtype
Unknown rr-types or otherwise malformed RR's are ignored and logged as a warning during preloading.
It is possible to load zonedata multiple times, each one adds to the cache. Note that when setting errors, they always override other similar RR's regardless of ordering.
Examples
iex> zonedata = "
...> example.com TXT v=spf1 +all
...> example.com A timeout
...> EXAMPLE.NET AAAA 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: :aaaa) |> elem(1)
{:error, :servfail}
iex>
iex> ctx.dns
%{{"example.com", :a} => [error: :timeout],
{"example.com", :txt} => ["v=spf1 +all"],
{"example.net", :aaaa} => [error: :servfail]
}
iex> zonedata1 = "
...> example.com A 1.2.3.4
...> example.com A timeout
...> example.net A 9.10.11.12
...> "
iex> zonedata2 = "
...> example.com AAAA servfail
...> example.com AAAA acdc:1976::1
...> example.net A 5.6.7.8
...> example.net A 9.10.11.12
...> "
iex> ctx = Spf.Context.new("some.tld")
...> |> Spf.DNS.load(zonedata1)
...> |> Spf.DNS.load(zonedata2)
iex> ctx.dns[{"example.com", :a}]
[{:error, :timeout}]
iex> ctx.dns[{"example.com", :aaaa}]
[{:error, :servfail}]
iex> ctx.dns[{"example.net", :a}]
["5.6.7.8", "9.10.11.12"]
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(), domain(), Keyword.t()) :: {Spf.Context.t(), dns_result()}
Resolves a query, updates the cache and returns a {ctx
,
dns_result/0
}-tuple.
Returns one of:
{ctx, {:error, reason}}
if a DNS error occurred or was cached earlier{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)
...> |> 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"
]
iex> 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> to_list(ctx, valid: false)
...> |> Enum.map(fn x -> String.replace(x, ~r/\s+/, " ") end)
[
"b.example.com AAAA TIMEOUT"
]