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.queryis the IP address or the domain, andcodesthe matching answers.[:sovite, :abuse, :dnsbl, :error]-%{},%{zone, query, reason}, for each query that failed.reasonis:timeoutif 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.
What to look up: a client address or a domain.
Functions
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
@opaque handle()
A background scoring started by async_score/4.
@type hit() :: %{ zone: String.t(), weight: integer(), codes: [:inet.ip4_address(), ...] }
A list that lists the target, and its matching answers.
A list zone, the weight it adds, and the reply codes it counts.
@type pattern() :: {octet_spec(), octet_spec(), octet_spec(), octet_spec()}
A reply-code pattern, from parse_code/1. Treat it as opaque.
The result of score/4.
@type target() :: {:ip, :inet.ip_address()} | {:domain, String.t()}
What to look up: a client address or a domain.
Functions
@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.
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.
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"}
@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}}.
@spec match?([:inet.ip4_address()], [pattern()]) :: boolean()
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.
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
@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"
@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.