Sovite.Abuse.DNSBL (sovite v0.6.0)

Copy Markdown View Source

Scores a client address or a domain against DNS block and allow lists (RFC 5782): IP lists (DNSBL, DNSWL) and domain lists (RHSBL).

lists = [
  %{zone: "zen.spamhaus.org", weight: 3, codes: []},
  %{zone: "b.barracudacentral.org", weight: 2, codes: []},
  %{zone: "list.dnswl.org", weight: -2, codes: [pattern]}
]

DNSBL.score(resolver, {:ip, ip}, lists)
#=> %{score: 3, hits: [%{zone: "zen.spamhaus.org", weight: 3, codes: [{127, 0, 0, 2}]}], errors: []}

Each list that lists the target adds its weight to the score. A negative weight makes an allow list. codes holds reply-code patterns from parse_code/1: the list counts only if one of its answers matches one of them. With no patterns, any answer counts. Lists of the same zone share one query, so one zone can be scored per reply code.

Lists that fail or time out add nothing to the score and are reported in errors.

Options

  • :timeout - milliseconds for all queries together. Defaults to 10 seconds.

Telemetry

  • [:sovite, :abuse, :dnsbl, :listed] - %{weight}, %{zone, query, codes}, for each list that lists the target. query is the IP address or the domain, and codes the matching answers.
  • [:sovite, :abuse, :dnsbl, :error] - %{}, %{zone, query, reason}, for each query that failed. reason is :timeout if it timed out.

Summary

Types

A background scoring started by async_score/4.

A list that lists the target, and its matching answers.

A list zone, the weight it adds, and the reply codes it counts.

A reply-code pattern, from parse_code/1. Treat it as opaque.

The result of score/4.

What to look up: a client address or a domain.

Functions

Starts score/4 in the background, for example when a client connects, and returns a handle for await/2, for example at RCPT.

Waits up to timeout milliseconds for the result of async_score/4.

Returns the name to query for domain in a domain list zone (RFC 5782 §2.3), or :error if domain is empty, an address literal, or too long for the result to be a domain name.

Looks up name in a list.

Returns whether any of addresses matches any of the codes patterns. With no patterns, any address matches.

Parses a reply-code pattern, in Postfix postscreen syntax.

Returns the name to query for ip in an IP list zone: the octets reversed for IPv4 (RFC 5782 §2.1), or the 32 nibbles reversed for IPv6 (§2.4). IPv4-mapped IPv6 addresses are queried as IPv4.

Looks up target in all lists at once and adds up the weights of the lists that list it. See the module documentation for the options.

Types

handle()

@opaque handle()

A background scoring started by async_score/4.

hit()

@type hit() :: %{
  zone: String.t(),
  weight: integer(),
  codes: [:inet.ip4_address(), ...]
}

A list that lists the target, and its matching answers.

list_spec()

@type list_spec() :: %{zone: String.t(), weight: integer(), codes: [pattern()]}

A list zone, the weight it adds, and the reply codes it counts.

pattern()

@type pattern() :: {octet_spec(), octet_spec(), octet_spec(), octet_spec()}

A reply-code pattern, from parse_code/1. Treat it as opaque.

result()

@type result() :: %{score: integer(), hits: [hit()], errors: [String.t()]}

The result of score/4.

target()

@type target() :: {:ip, :inet.ip_address()} | {:domain, String.t()}

What to look up: a client address or a domain.

Functions

async_score(resolver, target, lists, opts \\ [])

@spec async_score(Sovite.DNS.resolver(), target(), [list_spec()], keyword()) ::
  handle()

Starts score/4 in the background, for example when a client connects, and returns a handle for await/2, for example at RCPT.

The background process is not linked to the caller and sends it nothing until await/2 asks, so it suits a process that traps exits. It exits when the caller exits, and on its own if await/2 has not asked within 5 seconds after the :timeout option.

await(map, timeout)

@spec await(handle(), timeout()) :: {:ok, result()} | {:error, :timeout}

Waits up to timeout milliseconds for the result of async_score/4.

Returns {:error, :timeout} if the result is not ready in time or the background process is gone. The background process is then killed, and no message from it is left in the caller's mailbox. Each handle can be awaited only once.

domain_query_name(domain, zone)

@spec domain_query_name(String.t(), String.t()) :: {:ok, String.t()} | :error

Returns the name to query for domain in a domain list zone (RFC 5782 §2.3), or :error if domain is empty, an address literal, or too long for the result to be a domain name.

iex> Sovite.Abuse.DNSBL.domain_query_name("Example.COM.", "rhsbl.example")
{:ok, "example.com.rhsbl.example"}

lookup(resolver, name)

@spec lookup(Sovite.DNS.resolver(), String.t()) ::
  {:ok, [:inet.ip4_address()]} | {:error, term()}

Looks up name in a list.

A name that is not listed returns {:ok, []}. Only answers in 127.0.0.0/8 are returned (RFC 5782 §2.1). Answers in 127.255.255.0/24 are errors from the list operator, such as Spamhaus refusing queries from public resolvers, and return {:error, {:list_error, ip}}.

match?(addresses, codes)

@spec match?([:inet.ip4_address()], [pattern()]) :: boolean()

Returns whether any of addresses matches any of the codes patterns. With no patterns, any address matches.

parse_code(code)

@spec parse_code(String.t()) :: {:ok, pattern()} | {:error, String.t()}

Parses a reply-code pattern, in Postfix postscreen syntax.

A pattern is four dot-separated octets. Each is a number from 0 to 255, or a bracketed list of numbers and N..M ranges separated by ;. The error is a message for the administrator.

iex> {:ok, pattern} = Sovite.Abuse.DNSBL.parse_code("127.0.0.[2..11;20]")
iex> Sovite.Abuse.DNSBL.match?([{127, 0, 0, 20}], [pattern])
true

query_name(ip, zone)

@spec query_name(:inet.ip_address(), String.t()) :: String.t()

Returns the name to query for ip in an IP list zone: the octets reversed for IPv4 (RFC 5782 §2.1), or the 32 nibbles reversed for IPv6 (§2.4). IPv4-mapped IPv6 addresses are queried as IPv4.

iex> Sovite.Abuse.DNSBL.query_name({192, 0, 2, 99}, "dnsbl.example.")
"99.2.0.192.dnsbl.example"

score(resolver, target, lists, opts \\ [])

@spec score(Sovite.DNS.resolver(), target(), [list_spec()], keyword()) :: result()

Looks up target in all lists at once and adds up the weights of the lists that list it. See the module documentation for the options.

A domain that cannot be queried (see domain_query_name/2) scores 0.