erldns_pipeline behaviour (erldns v10.0.0-rc4)

View Source

The pipeline specification.

It declares a pipeline of sequential transformations to apply to the incoming query until a response is constructed.

This module is responsible for handling the pipeline of pipes that will be executed when a DNS message is received. Handlers in this pipeline will be executed sequentially, accumulating the result of each handler and passing it to the next. This designs a pluggable framework where new behaviour can be injected as a new pipe handler in the right order.

Default pipes

The following are enabled by default, see their documentation for details:

Types of pipelines

There are two kind of pipes: function pipes and module pipes.

Function pipes

A function pipe is by definition any function that receives a dns:message/0 and a set of options and returns a dns:message/0. Function pipes must have the following type signature:

-type pipe() :: fun((dns:message(), opts()) -> return()

Module pipes

The preferred mechanism, a module pipe is an extension of the function pipe.

It is a module that exports:

  • a prepare/1 function which takes a set of options and initializes it, or disables the pipe.
  • a call/2 function with the signature defined as in the function pipe.

The API expected by a module pipe is defined as a behaviour by this module.

Configuration

{erldns, [
    {packet_pipeline, [
        erldns_questions,
        erldns_edns_max_payload_size,
        erldns_query_throttle,
        erldns_packet_cache,
        erldns_resolver_recursive,
        erldns_resolver,
        erldns_dnssec,
        erldns_sorter,
        erldns_section_counter,
        erldns_packet_cache,
        erldns_empty_verification
    ]},
]}

Telemetry events

Emits the following telemetry events:

[erldns, pipeline, error]

  • Measurements:
    count := non_neg_integer()
  • Metadata: If it is an exception, the metadata will contain:
    kind => exit | error | throw
    reason => term()
    stacktrace => [term()]
    otherwise, it will contain:
    reason => term()

Examples

Here's an example of a function pipe that arbitrarily sets the truncated bit on a message if the query is directed to the "example.com" domain:

-module(erldns_packet_pipe_example_set_truncated).
-behaviour(erldns_pipeline).

-export([prepare/1, call/2]).

-spec prepare(erldns_pipeline:opts()) -> disabled | erldns_pipeline:opts().
prepare(Opts) ->
    case enabled() of
        false -> disabled;
        true -> Opts
    end.

-spec call(dns:message(), erldns_pipeline:opts()) -> erldns_pipeline:return().
call(#dns_message{questions = [#dns_query{name = <<"example.com">>} | _]} = Msg, _Opts) ->
    Msg#dns_message{tc = true}.
call(Msg, _Opts) ->
    Msg.

Summary

Types

The dependencies of a pipe module.

The host that originated the request.

Options that can be passed and accumulated to the pipeline.

A pipe in the pipeline, either a module or a function.

The return type of a pipe.

The underlying request transport protocol. All requests come either through UDP or TCP.

Callbacks

Trigger the pipeline at run-time.

Declare dependencies on other pipes.

Initialise the pipe handler, triggering side-effects and preparing any necessary metadata.

Functions

Call the main application packet pipeline with the pipes configured in the system configuration.

Call a custom pipeline by name.

Remove a custom pipeline from storage.

Check if a pipe is configured in the main pipeline.

Check if a pipe is configured in a specific pipeline. Returns false if the pipeline doesn't exist.

Verify and store a custom pipeline.

Types

deps()

-type deps() :: #{prerequisites => [module()], dependents => [module()]}.

The dependencies of a pipe module.

Contains the following keys:

  • prerequisites is a list of module pipes that must appear earlier in the pipeline
  • dependents is a list of module pipes that must appear later in the pipeline

host()

-type host() :: inet:ip_address() | inet:hostname().

The host that originated the request.

opts()

-type opts() ::
          #{query_labels := dns:labels(),
            query_type := dns:type(),
            monotonic_time := integer(),
            resolved := boolean(),
            transport := transport(),
            port := inet:port_number(),
            host := host(),
            socket := gen_tcp:socket() | {gen_udp:socket(), inet:port_number()},
            atom() => dynamic()}.

Options that can be passed and accumulated to the pipeline.

pipe()

-type pipe() :: module() | fun((dns:message(), opts()) -> return()).

A pipe in the pipeline, either a module or a function.

See Module pipes and Function pipes for details.

return()

-type return() :: halt | dns:message() | {dns:message(), opts()} | {stop, dns:message()}.

The return type of a pipe.

It can return halt, a new dns:message/0, with or without new opts/0, or put a stop to the pipeline execution.

transport()

-type transport() :: tcp | udp.

The underlying request transport protocol. All requests come either through UDP or TCP.

Callbacks

call/2

-callback call(dns:message(), opts()) ->
                  halt | dns:message() | {dns:message(), opts()} | {stop, dns:message()}.

Trigger the pipeline at run-time.

This callback can return

  • a possibly new dns:message/0;
  • a tuple containing a new dns:message/0 and a new set of opts/0;
  • a {stop, t:dns:message/0} tuple to stop the pipeline execution altogether.
  • a halt atom, in which case the pipeline will be halted and no further pipes will be executed. The socket workers won't respond nor trigger any events, and it's fully the responsibility of a handler to deal with all the edge cases. This could be useful for either dropping the request entirely, or for stealing the request from a given worker to answer separately. Note that the pipe options will contain the UDP or TCP socket to answer to, so in the case of UDP the client can be answered using gen_udp:send/4 with the socket, host and port; and in the case of TCP it would be required to first steal the socket using gen_tcp:controlling_process/2 so that the connection is not closed.

deps()

(optional)
-callback deps() -> deps().

Declare dependencies on other pipes.

This pipe will only work correctly if the listed pipes appear earlier in the pipeline configuration. The pipeline will fail to start if dependencies are not satisfied.

Example:

-module(erldns_dnssec).
-behaviour(erldns_pipeline).
-export([deps/0, prepare/1, call/2]).

-spec deps() -> deps().
deps() ->
    #{prerequisites => [erldns_questions, erldns_resolver], dependents => []}.

prepare/1

(optional)
-callback prepare(opts()) -> disabled | opts().

Initialise the pipe handler, triggering side-effects and preparing any necessary metadata.

This will be called during the pipeline initialisation phase, which should happen at application startup provided you added the pipeline to your application's supervision tree. This will be called only once during application startup and therefore it is an opportunity to do any necessary preparations that can reduce the amount of work at runtime and therefore improve performance.

This callback can return disabled, and then the call/2 callback won't be added to the pipeline.

Functions

call(Msg, Opts)

-spec call(dns:message(), #{atom() => dynamic()}) -> halt | dns:message().

Call the main application packet pipeline with the pipes configured in the system configuration.

call_custom(Msg, Opts, PipelineName)

-spec call_custom(dns:message(), #{atom() => dynamic()}, dynamic()) -> halt | dns:message().

Call a custom pipeline by name.

The pipeline should have been verifiend and stored previously with store_pipeline/2.

delete_pipeline(PipelineName)

-spec delete_pipeline(term()) -> boolean().

Remove a custom pipeline from storage.

Should be used to clean up a custom pipeline stored with store_pipeline/2.

is_pipe_configured(Pipe)

-spec is_pipe_configured(pipe()) -> boolean().

Check if a pipe is configured in the main pipeline.

is_pipe_configured/2

-spec is_pipe_configured(pipe(), term()) -> boolean().

Check if a pipe is configured in a specific pipeline. Returns false if the pipeline doesn't exist.

store_pipeline(PipelineName, Pipes)

-spec store_pipeline(term(), [pipe()]) -> ok.

Verify and store a custom pipeline.

Can be used to prepare a custom pipeline that can be triggered using call_custom/3.

Validates that pipe dependencies (declared via deps/0) are satisfied by the given order. Raises an error if a pipe's dependencies don't appear earlier in the pipeline.