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

Types

An {:error, :cache_miss}-tuple

A DNS result in the form of an ok/error-tuple.

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

dns_result() :: {:ok, [any()]} | {:error, atom()}

A DNS result in the form of an ok/error-tuple.

Link to this section Functions

Specs

authority(Spf.Context.t(), binary()) ::
  {:ok, binary(), binary(), binary()} | {:error, atom()}

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

check_domain(binary()) :: {:ok, binary()} | {:error, binary()}

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}
Link to this function

from_cache(context, name, type)

View Source

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 (where rrs 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, or
  • name error

where

  • rr-type is a, aaaa, cname, mx, ns, ptr, soa, spf, or txt
  • error is formerr, nxdomain, servfail, timeout or zero_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(binary() | list()) :: binary()

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"
Link to this function

resolve(ctx, name, opts \\ [])

View Source

Specs

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 to true

When stats is false, void DNS responses (:nxdomain or :zero_answers) are not counted.

Link to this function

to_list(ctx, opts \\ [])

View Source

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"
]