Current section

Files

Jump to
erldns src pipes erldns_query_throttle.erl
Raw

src/pipes/erldns_query_throttle.erl

-module(erldns_query_throttle).
-moduledoc """
Stateful query throttling. Currently only throttles `ANY` queries.
We should throttle `ANY` and `RRSIG` queries to discourage use of our authoritative name servers
for reflection/amplification attacks.
## Configuration
```erlang
{erldns, [
{query_throttle, #{
enabled := boolean(), %% defaults to true
limit := non_neg_integer(), %% Number of queries to allow, defaults to 1
ttl := non_neg_integer(), %% Seconds, defaults to 30
}}
]}
```
## Telemetry events
### `[erldns, pipeline, throttle]`
Emitted when the query throttle pipe stops the request (e.g. UDP client over limit);
response is truncated.
- **Measurements:** `#{count => non_neg_integer()}` — request count that triggered throttle
- **Metadata:** `#{transport => udp, host => host()}`
""".
-include_lib("dns_erlang/include/dns.hrl").
-behaviour(erldns_pipeline).
-export([prepare/1, call/2]).
-export([start_link/0, clear/0, merger/2]).
-type host() :: inet:ip_address() | inet:hostname().
-define(DEFAULT_LIMIT, 1).
-define(DEFAULT_BUCKETS, 2).
-define(DEFAULT_CACHE_TTL, 60).
-doc "`c:erldns_pipeline:prepare/1` callback.".
-spec prepare(erldns_pipeline:opts()) -> disabled | erldns_pipeline:opts().
prepare(Opts) ->
case enabled() of
false -> disabled;
true -> Opts#{packet_throttle_limit => default_limit()}
end.
-doc "`c:erldns_pipeline:call/2` callback.".
-spec call(dns:message(), erldns_pipeline:opts()) -> erldns_pipeline:return().
call(Msg, #{transport := udp, host := Host, packet_throttle_limit := Limit}) ->
case should_throttle(Msg, Host, Limit) of
{true, ReqCount} ->
Metadata = #{transport => udp, host => Host},
telemetry:execute([erldns, pipeline, throttle], #{count => ReqCount}, Metadata),
{stop, Msg#dns_message{
tc = true,
aa = true,
rc = ?DNS_RCODE_NOERROR
}};
false ->
Msg
end;
call(Msg, _) ->
Msg.
-doc false.
-spec start_link() -> ignore | gen_server:start_ret().
start_link() ->
case enabled() of
false ->
ignore;
true ->
Config = #{
prefix => [erldns, pipeline, throttle],
scope => erldns,
strategy => lru,
merger_fun => fun ?MODULE:merger/2,
segment_num => ?DEFAULT_BUCKETS,
ttl => {seconds, default_ttl() div ?DEFAULT_BUCKETS}
},
segmented_cache:start_link(?MODULE, Config)
end.
-spec should_throttle(dns:message(), host(), non_neg_integer()) ->
false | {true, non_neg_integer()}.
should_throttle(#dns_message{questions = Questions}, Host, Limit) ->
HasAny = lists:any(
fun
(#dns_query{type = ?DNS_TYPE_ANY}) -> true;
(#dns_query{type = ?DNS_TYPE_RRSIG}) -> true;
(#dns_query{type = _}) -> false
end,
Questions
),
HasAny andalso should_throttle(Host, Limit).
-spec should_throttle(host(), non_neg_integer()) -> false | {true, non_neg_integer()}.
should_throttle({127, 0, 0, 1}, _) ->
false;
should_throttle({0, 0, 0, 0, 0, 0, 0, 1}, _) ->
false;
should_throttle(Host, Limit) ->
case segmented_cache:get_entry(?MODULE, Host) of
not_found ->
segmented_cache:put_entry(?MODULE, Host, 1),
false;
ReqCount when is_integer(ReqCount), ReqCount < Limit ->
segmented_cache:put_entry(?MODULE, Host, ReqCount),
false;
ReqCount when is_integer(ReqCount), ReqCount >= Limit ->
segmented_cache:put_entry(?MODULE, Host, ReqCount),
{true, ReqCount}
end.
-doc "Clear the cache".
-spec clear() -> true.
clear() ->
segmented_cache:delete_pattern(?MODULE, '_').
-doc false.
-spec merger(non_neg_integer(), non_neg_integer()) -> non_neg_integer().
merger(_A, B) ->
B + 1.
-spec enabled() -> boolean().
enabled() ->
case application:get_env(erldns, query_throttle, #{}) of
#{enabled := Value} when is_boolean(Value) ->
Value;
_ ->
true
end.
-spec default_limit() -> pos_integer().
default_limit() ->
case application:get_env(erldns, query_throttle, #{}) of
#{limit := Value} when is_integer(Value), 0 < Value ->
Value;
_ ->
?DEFAULT_LIMIT
end.
-spec default_ttl() -> pos_integer().
default_ttl() ->
case application:get_env(erldns, query_throttle, #{}) of
#{ttl := Value} when is_integer(Value), 0 < Value ->
Value;
_ ->
?DEFAULT_CACHE_TTL
end.