Current section

Files

Jump to
erldns src erldns_handler.erl
Raw

src/erldns_handler.erl

-module(erldns_handler).
-moduledoc """
The module that handles the resolution of a single DNS question.
The meat of the resolution occurs in erldns_resolver:resolve/3
## Configuration
```erlang
{erldns, [
{packet_handlers, [
{my_custom_handler_module, [?DNS_TYPE_A, ?DNS_TYPE_AAAA], 3}
]},
]}
```
Record types can be given in their integer codes or binary representations,
meaning, the following are equivalent:
```erlang
{my_custom_handler_module, [?DNS_TYPE_A, ?DNS_TYPE_AAAA], 3}
...
{my_custom_handler_module, [?DNS_TYPE_A, ~"AAAA"], 3}
...
{my_custom_handler_module, [~"A", ~"AAAA"], 3}
```
The minimum supported version is `2`.
Version 2's handler signature is
```erlang
handle(dns:message(), dns:labels(), dns:type(), [dns:rr()]) -> [dns:rr()]
```.
Version 3's handler signature is
```erlang
handle(dns:dname(), dns:type(), [dns:rr()], dns:message()) -> [dns:rr()].
```.
""".
-behaviour(gen_server).
-include_lib("kernel/include/logger.hrl").
-include_lib("dns_erlang/include/dns.hrl").
-define(MINIMUM_HANDLER_VERSION, 2).
-define(DEFAULT_HANDLER_VERSION, 3).
-define(TIMEOUT, 5000).
-export([
register_handler/2,
register_handler/3,
get_versioned_handlers/0,
call_filters/1,
call_handlers/4,
call_map_nsec_rr_types/1,
call_map_nsec_rr_types/2,
%% Standalone NSEC type mapper registration (for pipeline modules)
register_nsec_type_mapper/2,
unregister_nsec_type_mapper/2
]).
-export([start_link/0, init/1, handle_call/3, handle_cast/2, terminate/2]).
-record(handlers_state, {
handlers = [] :: [versioned_handler()],
mappers = [] :: [nsec_type_mapper()]
}).
-opaque state() :: #handlers_state{}.
-type versioned_handler() :: {
fun((dns:message(), dns:labels(), dns:type(), [dns:rr()]) -> [dns:rr()]),
fun(([dns:rr()]) -> [dns:rr()]),
fun((dns:type(), dns:type()) -> [dns:type()]),
module(),
[dns:type()],
integer()
}.
-type handler() :: {module(), [dns:type()]}.
-type handler_config() :: {module(), [dns:type()], integer()}.
-type nsec_type_mapper() :: {[dns:type()], fun((dns:type(), dns:type()) -> [dns:type()])}.
-export_type([state/0, versioned_handler/0, handler/0, handler_config/0, nsec_type_mapper/0]).
-doc "Filter the given record set, returning replacement records.".
-callback filter([dns:rr()]) -> [dns:rr()].
-doc "Filter out records not related to the given handler".
-callback handle(dns:message(), dns:labels(), dns:type(), [dns:rr()]) -> [dns:rr()].
-doc "Map handler's record types to NSEC bit types.".
-callback nsec_rr_type_mapper(dns:type(), dns:type()) -> [dns:type()].
-doc "Register a record handler with the default version of 1".
-spec register_handler([dns:type()], module()) -> ok.
register_handler(RecordTypes, Module) ->
register_handler(RecordTypes, Module, ?DEFAULT_HANDLER_VERSION).
-doc "Register a record handler with version".
-spec register_handler([dns:type()], module(), integer()) -> ok.
register_handler(RecordTypes, Module, Version) ->
gen_server:call(?MODULE, {register_handler, {Module, RecordTypes, Version}}, ?TIMEOUT).
-doc "Get all registered handlers along with the DNS types they handle and associated versions".
-spec get_versioned_handlers() -> [versioned_handler()].
get_versioned_handlers() ->
{Handlers, _Mappers} = persistent_term:get(?MODULE, {[], []}),
Handlers.
-doc "Filter records through registered handlers.".
-spec call_filters([dns:rr()]) -> [dns:rr()].
call_filters(Records) ->
filter_records(Records, get_versioned_handlers()).
filter_records(Records, []) ->
Records;
filter_records(Records, [{_, Filter, _, _, _, _} | Rest]) ->
filter_records(Filter(Records), Rest).
-doc "Call all registered handlers.".
-spec call_handlers(dns:message(), dns:labels(), dns:type(), [dns:rr()]) -> [dns:rr()].
call_handlers(Message, QLabels, QType, Records) ->
Handlers = get_versioned_handlers(),
lists:flatmap(call_handlers_fun(Message, QLabels, QType, Records), Handlers).
-spec call_handlers_fun(dns:message(), dns:labels(), dns:type(), [dns:rr()]) ->
fun((versioned_handler()) -> [dns:rr()]).
call_handlers_fun(Message, QLabels, ?DNS_TYPE_ANY, Records) ->
fun
({Handler, _, _, _, _, ?MINIMUM_HANDLER_VERSION}) ->
Handler(dns_domain:join(QLabels), ?DNS_TYPE_ANY, Records, Message);
({Handler, _, _, _, _, ?DEFAULT_HANDLER_VERSION}) ->
Handler(Message, QLabels, ?DNS_TYPE_ANY, Records)
end;
call_handlers_fun(Message, QLabels, QType, Records) ->
fun
({Handler, _, _, _, Types, ?MINIMUM_HANDLER_VERSION}) ->
case lists:member(QType, Types) of
true -> Handler(dns_domain:join(QLabels), QType, Records, Message);
false -> []
end;
({Handler, _, _, _, Types, ?DEFAULT_HANDLER_VERSION}) ->
case lists:member(QType, Types) of
true -> Handler(Message, QLabels, QType, Records);
false -> []
end
end.
-spec call_map_nsec_rr_types([dns:type()]) -> [dns:type()].
call_map_nsec_rr_types(Types) ->
Handlers = get_versioned_handlers(),
Mappers = get_nsec_type_mappers(),
MappedTypes = lists:flatmap(
fun(Type) ->
map_single_type_arity1(Type, Handlers, Mappers)
end,
Types
),
lists:usort(MappedTypes).
-spec call_map_nsec_rr_types(dns:type(), [dns:type()]) -> [dns:type()].
call_map_nsec_rr_types(QType, Types) ->
Handlers = get_versioned_handlers(),
Mappers = get_nsec_type_mappers(),
MappedTypes = map_nsec_rr_types(QType, Types, Handlers, Mappers),
lists:usort(MappedTypes).
%% Map a single type using arity-1 mapper (for call_map_nsec_rr_types/1)
-spec map_single_type_arity1(dns:type(), [versioned_handler()], [nsec_type_mapper()]) ->
[dns:type()].
map_single_type_arity1(Type, Handlers, Mappers) ->
%% First check versioned handlers
case lists:keyfind([Type], 5, Handlers) of
{_, _, _, M, _, _} ->
M:nsec_rr_type_mapper(Type);
false ->
%% Check standalone mappers
case find_standalone_mapper(Type, Mappers) of
{ok, MapperFun} ->
MapperFun(Type, Type);
not_found ->
[Type]
end
end.
-spec map_nsec_rr_types(dns:type(), [dns:type()], [versioned_handler()], [nsec_type_mapper()]) ->
[dns:type()].
map_nsec_rr_types(_QType, Types, [], []) ->
Types;
map_nsec_rr_types(QType, Types, Handlers, Mappers) ->
lists:flatmap(
fun(Type) ->
map_single_type(Type, QType, Handlers, Mappers)
end,
Types
).
%% Map a single type using arity-2 mapper (for call_map_nsec_rr_types/2)
-spec map_single_type(dns:type(), dns:type(), [versioned_handler()], [nsec_type_mapper()]) ->
[dns:type()].
map_single_type(Type, QType, Handlers, Mappers) ->
%% First check versioned handlers
case lists:keyfind([Type], 5, Handlers) of
{_, _, Mapper, _, _, _} ->
Mapper(Type, QType);
false ->
%% Check standalone mappers
case find_standalone_mapper(Type, Mappers) of
{ok, MapperFun} ->
MapperFun(Type, QType);
not_found ->
[Type]
end
end.
-spec find_standalone_mapper(dns:type(), [nsec_type_mapper()]) ->
{ok, fun((dns:type(), dns:type()) -> [dns:type()])} | not_found.
find_standalone_mapper(_Type, []) ->
not_found;
find_standalone_mapper(Type, [{RecordTypes, MapperFun} | Rest]) ->
case lists:member(Type, RecordTypes) of
true -> {ok, MapperFun};
false -> find_standalone_mapper(Type, Rest)
end.
-doc "Start the handler registry process".
-spec start_link() -> gen_server:start_ret().
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, noargs, [{hibernate_after, 0}]).
% gen_server callbacks
-doc false.
-spec init(noargs) -> {ok, state()}.
init(noargs) ->
process_flag(trap_exit, true),
Handlers = prepare_handlers(),
persistent_term:put(?MODULE, {Handlers, []}),
{ok, #handlers_state{handlers = Handlers, mappers = []}}.
-doc false.
-spec handle_call
({register_handler, {module(), [dns:type()], integer()}}, gen_server:from(), state()) ->
{reply, ok, state()};
(
{register_nsec_type_mapper, [dns:type()], fun((dns:type(), dns:type()) -> [dns:type()])},
gen_server:from(),
state()
) ->
{reply, ok, state()};
(
{unregister_nsec_type_mapper, [dns:type()], fun((dns:type(), dns:type()) -> [dns:type()])},
gen_server:from(),
state()
) ->
{reply, ok, state()};
(dynamic(), gen_server:from(), state()) ->
{reply, not_implemented, state()}.
handle_call({register_handler, Handler}, _, State) ->
NewHandlers = prepare_handlers([Handler], State#handlers_state.handlers),
persistent_term:put(?MODULE, {NewHandlers, State#handlers_state.mappers}),
{reply, ok, State#handlers_state{handlers = NewHandlers}};
handle_call({register_nsec_type_mapper, RecordTypes, MapperFun}, _, State) ->
NewMappers = [{RecordTypes, MapperFun} | State#handlers_state.mappers],
persistent_term:put(?MODULE, {State#handlers_state.handlers, NewMappers}),
{reply, ok, State#handlers_state{mappers = NewMappers}};
handle_call({unregister_nsec_type_mapper, RecordTypes, MapperFun}, _, State) ->
NewMappers = lists:delete({RecordTypes, MapperFun}, State#handlers_state.mappers),
persistent_term:put(?MODULE, {State#handlers_state.handlers, NewMappers}),
{reply, ok, State#handlers_state{mappers = NewMappers}};
handle_call(_, _, State) ->
{reply, not_implemented, State}.
-doc false.
-spec handle_cast(term(), state()) -> {noreply, state()}.
handle_cast(_, State) ->
{noreply, State}.
-doc false.
-spec terminate(term(), state()) -> term().
terminate(_, _) ->
persistent_term:erase(?MODULE).
-spec get_nsec_type_mappers() -> [nsec_type_mapper()].
get_nsec_type_mappers() ->
{_Handlers, Mappers} = persistent_term:get(?MODULE, {[], []}),
Mappers.
%% NSEC Type Mapper Registry
%% Allows pipeline modules to register type mappers without implementing full handler interface.
-doc """
Register a standalone NSEC type mapper for specific record types.
This allows pipeline modules to provide NSEC type mapping without implementing
the full handler interface (handle/4, filter/1).
Example:
```erlang
erldns_handler:register_nsec_type_mapper([30003], fun my_module:nsec_rr_type_mapper/2)
```
""".
-spec register_nsec_type_mapper([dns:type()], fun((dns:type(), dns:type()) -> [dns:type()])) -> ok.
register_nsec_type_mapper(RecordTypes, MapperFun) when is_function(MapperFun, 2) ->
gen_server:call(?MODULE, {register_nsec_type_mapper, RecordTypes, MapperFun}, ?TIMEOUT).
-doc "Unregister NSEC type mappers for specific record types.".
-spec unregister_nsec_type_mapper([dns:type()], fun((dns:type(), dns:type()) -> [dns:type()])) ->
ok.
unregister_nsec_type_mapper(RecordTypes, MapperFun) ->
gen_server:call(?MODULE, {unregister_nsec_type_mapper, RecordTypes, MapperFun}, ?TIMEOUT).
-spec prepare_handlers() -> [versioned_handler()].
prepare_handlers() ->
Handlers = application:get_env(erldns, packet_handlers, []),
prepare_handlers(Handlers, []).
-spec prepare_handlers([dynamic()], [versioned_handler()]) -> [versioned_handler()].
prepare_handlers([], Acc) ->
lists:reverse(Acc);
prepare_handlers([{Module, RecordTypes, Version} | Rest], Acc) ->
?LOG_INFO(
#{what => registered_handler, module => Module, types => RecordTypes, version => Version},
#{domain => [erldns, pipeline]}
),
maybe
{module, Module} ?= code:ensure_loaded(Module),
true ?= erlang:function_exported(Module, handle, 4),
true ?= erlang:function_exported(Module, filter, 1),
true ?= erlang:function_exported(Module, nsec_rr_type_mapper, 2),
true ?= Version >= ?MINIMUM_HANDLER_VERSION orelse {error, {version, Version}},
{ok, RecordTypesNums} ?= ensure_valid_record_types(RecordTypes, []),
Prepared = {
fun Module:handle/4,
fun Module:filter/1,
fun Module:nsec_rr_type_mapper/2,
Module,
RecordTypesNums,
Version
},
prepare_handlers(Rest, [Prepared | Acc])
else
{error, Reason} ->
erlang:error({badhandler, Module, Reason});
false ->
erlang:error({badhandler, Module, module_does_not_export_call})
end.
ensure_valid_record_types([], Acc) ->
{ok, lists:reverse(Acc)};
ensure_valid_record_types([Type | Rest], Acc) when is_integer(Type) ->
ensure_valid_record_types(Rest, [Type | Acc]);
ensure_valid_record_types([TypeBin | Rest], Acc) when is_binary(TypeBin) ->
case dns_names:name_type(TypeBin) of
undefined ->
{error, {record_type, TypeBin}};
Type ->
ensure_valid_record_types(Rest, [Type | Acc])
end.