Current section

Files

Jump to
erldns src erldns_query_throttle.erl
Raw

src/erldns_query_throttle.erl

%% Copyright (c) 2012-2020, DNSimple Corporation
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(erldns_query_throttle).
-moduledoc """
Stateful query throttling. Currently only throttles `ANY` queries.
We should_throttle ANY queries to discourage use of our authoritative name servers
for reflection/amplification attacks.
## Configuration
```erlang
{erldns, [
{query_throttle, #{
enabled => boolean(), %% defaults to true
ttl => non_neg_integer(), %% Seconds, defaults to 30
}}
]}
```
## Telemetry events
See `m:segmented_cache` for telemetry events under this module name.
Also emits the following telemetry events:
- `[erldns, pipeline, throttle]` with `host` in the metadata.
""".
-include_lib("dns_erlang/include/dns_records.hrl").
-export([start_link/0, throttle/2, clear/0, merger/2]).
-export_type([host/0, throttle_result/0, throttle_hit_count/0]).
-type host() :: inet:ip_address() | inet:hostname().
-type throttle_hit_count() :: non_neg_integer().
-type throttle_result() :: ok | throttled.
-define(DEFAULT_LIMIT, 1).
-define(DEFAULT_BUCKETS, 3).
-define(DEFAULT_CACHE_TTL, 60).
-doc false.
-spec start_link() -> any().
start_link() ->
case enabled() of
false ->
ignore;
true ->
Config = #{
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.
-doc false.
-spec merger(non_neg_integer(), non_neg_integer()) -> non_neg_integer().
merger(_A, B) ->
B + 1.
-doc "Throttle the given message if necessary.".
-spec throttle(dns:message(), Context :: {term(), Host :: host()}) ->
ok | throttled.
throttle(Msg, {udp, Host}) ->
case should_throttle(Msg, Host, ?DEFAULT_LIMIT) of
{true, ReqCount} ->
Metadata = #{protocol => udp, host => Host},
telemetry:execute([erldns, pipeline, throttle], #{count => ReqCount}, Metadata),
throttled;
false ->
ok
end;
throttle(_Message, {tcp, _Host}) ->
ok.
-spec should_throttle(dns:message(), host(), non_neg_integer()) -> false | {true, non_neg_integer()}.
should_throttle(Msg, Host, Limit) ->
HasAny = lists:any(fun(#dns_query{type = T}) -> T =:= ?DNS_TYPE_ANY end, Msg#dns_message.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 + 1),
false;
ReqCount when is_integer(ReqCount), ReqCount >= Limit ->
segmented_cache:put_entry(?MODULE, Host, ReqCount + 1),
{true, ReqCount}
end.
-doc "Clear the cache".
-spec clear() -> any().
clear() ->
gen_server:cast(?MODULE, clear).
-spec enabled() -> boolean().
enabled() ->
case application:get_env(erldns, query_throttle, #{}) of
#{enabled := Bool} when is_boolean(Bool) ->
Bool;
_ ->
true
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.