DNSTap logging library for Elixir - capture and export DNS query/response data using the DNSTap protocol and Frame Streams format.
# 1. Add the dependency in mix.exs
def deps do
[{:elixir_dnstap, "~> 0.1.0"}]
end# 2. In config/config.exs, pick an output (minimal example: file output)
config :elixir_dnstap,
enabled: true,
output: [type: :file, path: "log/dnstap.fstrm"]# 3. ElixirDnstap.Application checks `enabled?` at boot and starts the
# supervisor automatically, so the host only has to call
# log_client_query/6 when a DNS packet arrives.
:ok =
ElixirDnstap.log_client_query(
query_packet,
{192, 168, 1, 100}, # client_addr
54_321, # client_port
{127, 0, 0, 1}, # server_addr
5353, # server_port
:udp
)The resulting log/dnstap.fstrm can be read with the dnstap command-line tool or dnscollector. See Reading DNSTap Files below for details.
- π¦ Frame Streams Protocol - Full implementation of uni-directional and bi-directional Frame Streams
- π Multiple Output Types - File, Unix socket, and TCP output support
- β‘ High Performance - Built on GenStage with backpressure control
- π Automatic Reconnection - Exponential backoff for TCP/Unix socket connections
- π― Protocol Buffers - Efficient DNSTap message encoding
- π Production Ready - Comprehensive test coverage and error handling
Add elixir_dnstap to your list of dependencies in mix.exs:
def deps do
[
{:elixir_dnstap, "~> 0.1.0"}
]
endConfigure DNSTap output in your config/config.exs:
config :elixir_dnstap,
enabled: true,
output: [
type: :file,
path: "log/dnstap.fstrm"
]config :elixir_dnstap,
enabled: true,
output: [
type: :tcp,
host: "127.0.0.1",
port: 6000,
timeout: 5000,
bidirectional: true,
reconnect: true,
reconnect_interval: 1000,
max_reconnect_interval: 60_000,
max_reconnect_attempts: :infinity
]config :elixir_dnstap,
enabled: true,
output: [
type: :unix_socket,
path: "/tmp/dnstap.sock"
]elixir_dnstap is itself an OTP application (mod: {ElixirDnstap.Application, []} in mix.exs). When the host application starts, ElixirDnstap.Application.start/2 runs automatically as part of dependency resolution and starts ElixirDnstap.Supervisor whenever :elixir_dnstap, :enabled is true. No supervision-tree wiring on the host side is required β adding the dep and setting enabled: true is enough.
The setting is consulted in two places: ElixirDnstap.Application.start/2 decides whether to start ElixirDnstap.Supervisor at all, and ElixirDnstap.Supervisor.init/1 decides whether to bring up the GenStage pipeline. Starting ElixirDnstap.Supervisor manually while enabled is still false therefore produces an empty supervision tree, and subsequent log_client_query/6 calls return {:error, :producer_not_available}. To enable logging at runtime, set enabled: true (via Application.put_env/3 or your release config) before the supervisor starts.
log_client_query/6 takes positional arguments; log_client_response/1 takes a keyword list that includes the original query packet plus the query timestamp captured at receive time.
# Log a DNS client query
:ok =
ElixirDnstap.log_client_query(
query_packet,
{127, 0, 0, 1}, # client_addr
12_345, # client_port
{8, 8, 8, 8}, # server_addr
53, # server_port
:udp # :udp | :tcp
)
# Log a DNS client response
:ok =
ElixirDnstap.log_client_response(
query_packet: query_packet,
response_packet: response_packet,
client_addr: {127, 0, 0, 1},
client_port: 12_345,
server_addr: {8, 8, 8, 8},
server_port: 53,
socket_protocol: :udp,
query_time_sec: query_time_sec,
query_time_nsec: query_time_nsec
)Both functions return {:error, :producer_not_available} if the supervision tree has not been started.
ElixirDnstap uses a GenStage pipeline for efficient message processing:
graph LR
Host["Host app<br/>(log_client_query/6)"]
Producer["Producer<br/>(GenStage :producer)"]
BufferStage["BufferStage<br/>(GenStage :producer_consumer)"]
WriterConsumer["WriterConsumer<br/>(GenStage :consumer)"]
Writer["Writer.{File,TCP,UnixSocket}<br/>(GenServer)"]
Output["File / TCP / Unix socket"]
Producer --> BufferStage
BufferStage --> WriterConsumer
WriterConsumer --> Writer
Host -. "enqueue (GenStage.cast)" .-> Producer
WriterConsumer -. "write/1" .-> Writer
Writer -. "Frame Streams I/O" .-> Output
Solid arrows (-->) are the GenStage subscription chain (downstream stage subscribes to upstream); dotted arrows are runtime relationships (message enqueue, write delegation, and I/O). All three GenStage stages plus the selected writer are started under ElixirDnstap.Supervisor (:one_for_one).
- Producer (
:producer) - Receives DNS messages from the host viaenqueue/2(GenStage.cast) and manages demand-based backpressure with an internal queue. - BufferStage (
:producer_consumer) - Encodes each message to a DNSTap Protocol Buffers payload viaElixirDnstap.Encoder(stateless transformation). - WriterConsumer (
:consumer) - Delegates each encoded payload to the configured writer module viawriter_module.write/1. - Writer (
Writer.File/Writer.TCP/Writer.UnixSocket) - GenServers that wrap payloads in Frame Streams frames and perform the actual File / TCP / Unix-socket I/O.
Note: Protocol Buffers encoding happens in BufferStage, while Frame Streams framing is delegated to the Writer (see
ElixirDnstap.BufferStage's design note) β these are two distinct encoding steps, not one.
ElixirDnstap implements the Frame Streams protocol:
START β DATA* β STOP
Receiver: READY
Sender: ACCEPT β START β DATA* β FINISH
# Get dependencies
mix deps.get
# Install lefthook git hooks
lefthook installThis project uses Lefthook for git hooks. On commit, the following checks are automatically run:
mix format- Auto-format codemix test --cover- Run tests with coveragemix credo --strict- Check code quality
To skip hooks temporarily:
LEFTHOOK=0 git commit -m "message"# Run tests
mix test
# Run tests with coverage
mix test --cover
# Check code quality
mix credo --strict
# Type checking
mix dialyzer
# Generate documentation
mix docsUse the dnstap command-line tool to read DNSTap files:
# Read Frame Streams file
dnstap -r log/dnstap.fstrm
# Listen on TCP socket
dnstap -l 127.0.0.1:6000 -w output.fstrmElixirDnstap is designed for high-throughput DNS logging:
- Backpressure control prevents memory overflow
- Batch processing of frames
- Asynchronous I/O operations
- Automatic reconnection with exponential backoff
MIT License - see LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
This library implements the DNSTap protocol specification and Frame Streams format for capturing DNS traffic data